578 lines
17 KiB
Markdown
578 lines
17 KiB
Markdown
# Build 050 — Переключение MarketAnalysisService на канонические Candle
|
||
|
||
## Статус
|
||
|
||
**Завершён**
|
||
|
||
---
|
||
|
||
## Цель Build
|
||
|
||
Переключить основной orchestration-компонент анализа рынка:
|
||
|
||
`MarketAnalysisService`
|
||
|
||
с legacy API получения свечей:
|
||
|
||
ExchangeService.get_klines()
|
||
↓
|
||
KlineBatch
|
||
↓
|
||
list[Kline]
|
||
|
||
на канонический API:
|
||
|
||
ExchangeService.get_candles()
|
||
↓
|
||
tuple[Candle, ...]
|
||
|
||
без изменения торговой логики, алгоритмов анализа рынка, порогов, scoring, HTF-логики и существующих compatibility-контрактов.
|
||
|
||
---
|
||
|
||
## Исходное состояние
|
||
|
||
До Build 050 основной orchestration-компонент:
|
||
|
||
`src/trading/market_analysis/service.py`
|
||
|
||
получал свечи следующим образом:
|
||
|
||
ExchangeService.get_klines()
|
||
↓
|
||
KlineBatch
|
||
↓
|
||
batch.candles
|
||
↓
|
||
list[Kline]
|
||
|
||
При этом после Build 048 следующие вычислительные компоненты уже были переключены на каноническую модель `Candle`:
|
||
|
||
src/trading/market_analysis/indicators/volatility.py
|
||
src/trading/market_analysis/quality.py
|
||
src/trading/market_analysis/structure.py
|
||
|
||
Таким образом возникло переходное несоответствие типов:
|
||
|
||
MarketAnalysisService
|
||
↓
|
||
list[Kline]
|
||
|
||
market_structure()
|
||
candle_noise_score()
|
||
ATR calculations
|
||
↓
|
||
Sequence[Candle]
|
||
|
||
Build 050 устраняет это несоответствие для основного `MarketAnalysisService`.
|
||
|
||
---
|
||
|
||
## Объём изменений
|
||
|
||
В Build 050 изменены:
|
||
|
||
src/trading/market_analysis/service.py
|
||
tests/unit/trading/market_analysis/test_market_analysis_service_candles.py
|
||
docs/migrations/build_050.md
|
||
|
||
Build 050 намеренно не изменяет:
|
||
|
||
src/trading/market_analysis/htf.py
|
||
src/integrations/exchange/service.py
|
||
src/integrations/exchange/models.py
|
||
src/market_data/acquisition/
|
||
|
||
Также не изменяются:
|
||
|
||
ExchangeService.get_klines()
|
||
Kline
|
||
KlineBatch
|
||
_kline_from_candle()
|
||
|
||
---
|
||
|
||
## Переключение MarketAnalysisService на get_candles()
|
||
|
||
До Build 050 основной путь получения свечей выглядел так:
|
||
|
||
MarketAnalysisService.analyze()
|
||
↓
|
||
ExchangeService.get_klines()
|
||
↓
|
||
KlineBatch
|
||
↓
|
||
batch.candles
|
||
|
||
После Build 050:
|
||
|
||
MarketAnalysisService.analyze()
|
||
↓
|
||
ExchangeService.get_candles()
|
||
↓
|
||
tuple[Candle, ...]
|
||
|
||
Таким образом основной orchestration-компонент больше не зависит от:
|
||
|
||
Kline
|
||
KlineBatch
|
||
batch.candles
|
||
batch.symbol
|
||
|
||
---
|
||
|
||
## Каноническая модель данных
|
||
|
||
После Build 050 `MarketAnalysisService` непосредственно получает:
|
||
|
||
tuple[Candle, ...]
|
||
|
||
где каждая свеча представлена канонической моделью:
|
||
|
||
`src.market_data.acquisition.models.candle.Candle`
|
||
|
||
Основные числовые поля модели:
|
||
|
||
open_price: Decimal
|
||
high_price: Decimal
|
||
low_price: Decimal
|
||
close_price: Decimal
|
||
volume: Decimal
|
||
|
||
Таким образом основной Market Analysis orchestration теперь работает на той же канонической модели свечи, что и вычислительные компоненты, переключённые в Build 048.
|
||
|
||
---
|
||
|
||
## Определение analysis_symbol
|
||
|
||
Ранее symbol результата получался из:
|
||
|
||
batch.symbol
|
||
|
||
После удаления зависимости от `KlineBatch` введена локальная переменная:
|
||
|
||
analysis_symbol = candles[0].symbol if candles else symbol
|
||
|
||
Логика:
|
||
|
||
- если получена хотя бы одна каноническая свеча, используется `Candle.symbol`;
|
||
- если набор свечей пуст, сохраняется исходный запрошенный symbol.
|
||
|
||
Все прежние обращения:
|
||
|
||
batch.symbol
|
||
|
||
в `MarketAnalysisService` заменены на:
|
||
|
||
analysis_symbol
|
||
|
||
Это устраняет зависимость основного orchestration от legacy-контейнера `KlineBatch`.
|
||
|
||
---
|
||
|
||
## Числовая граница Decimal → float
|
||
|
||
Каноническая модель `Candle` хранит цены как:
|
||
|
||
Decimal
|
||
|
||
При этом существующие индикаторы и часть вычислительного pipeline работают с:
|
||
|
||
float
|
||
|
||
Поэтому в `MarketAnalysisService` введена явная числовая граница преобразования цен закрытия:
|
||
|
||
Candle.close_price
|
||
↓
|
||
safe_float()
|
||
↓
|
||
float
|
||
↓
|
||
list[float]
|
||
|
||
После Build 050:
|
||
|
||
closes: list[float] = []
|
||
|
||
for candle in candles:
|
||
close_price_value = safe_float(candle.close_price)
|
||
|
||
...
|
||
|
||
closes.append(close_price_value)
|
||
|
||
Это сохраняет существующий числовой контракт индикаторов и одновременно позволяет основному orchestration работать непосредственно с каноническими `Candle`.
|
||
|
||
---
|
||
|
||
## Проверка невалидных цен закрытия
|
||
|
||
При преобразовании `Candle.close_price` выполняется проверка:
|
||
|
||
- результат `safe_float()` не должен быть `None`;
|
||
- результат должен быть конечным числом;
|
||
- `NaN` не допускается;
|
||
- положительная или отрицательная бесконечность не допускается.
|
||
|
||
Для проверки конечности используется:
|
||
|
||
math.isfinite()
|
||
|
||
Если обнаружена некорректная цена закрытия, анализ безопасно завершается результатом `UNKNOWN` с причиной:
|
||
|
||
Получены некорректные цены закрытия свечей.
|
||
|
||
При этом сохраняется полное соответствие:
|
||
|
||
candles[index] ↔ closes[index]
|
||
|
||
Невалидные значения не пропускаются выборочно, поскольку это нарушило бы временное соответствие последовательностей свечей и цен.
|
||
|
||
---
|
||
|
||
## Сохранение недостаточного количества свечей
|
||
|
||
Проверка минимального количества свечей сохранена.
|
||
|
||
Если:
|
||
|
||
len(candles) < 60
|
||
|
||
анализ завершается безопасным результатом `UNKNOWN` с причиной:
|
||
|
||
Недостаточно свечей для анализа рынка.
|
||
|
||
При пустом наборе свечей используется исходный запрошенный symbol.
|
||
|
||
При непустом наборе используется canonical symbol первой свечи.
|
||
|
||
---
|
||
|
||
## Сохранение обработки ошибок ExchangeService
|
||
|
||
Вызов:
|
||
|
||
ExchangeService.get_candles()
|
||
|
||
остаётся внутри существующей обработки исключений.
|
||
|
||
Если получение свечей завершается ошибкой, `MarketAnalysisService` возвращает безопасный результат `UNKNOWN` с причиной, содержащей сообщение исходной ошибки.
|
||
|
||
Build 050 не изменяет общую стратегию обработки ошибок анализа рынка.
|
||
|
||
---
|
||
|
||
## Сохранение вычислительной логики
|
||
|
||
Build 050 не изменяет:
|
||
|
||
- EMA;
|
||
- ATR;
|
||
- RSI;
|
||
- momentum;
|
||
- market phase;
|
||
- market structure;
|
||
- candle noise score;
|
||
- price position score;
|
||
- scoring;
|
||
- confidence;
|
||
- payload;
|
||
- причины решений;
|
||
- пороги;
|
||
- HTF calculations;
|
||
- индексы текущей и закрытой свечи;
|
||
- торговые решения.
|
||
|
||
Изменяется только источник и тип входных свечей основного orchestration-компонента:
|
||
|
||
Было:
|
||
KlineBatch → list[Kline]
|
||
|
||
Стало:
|
||
tuple[Candle, ...]
|
||
|
||
---
|
||
|
||
## HTF намеренно не изменён
|
||
|
||
В Build 050 файл:
|
||
|
||
`src/trading/market_analysis/htf.py`
|
||
|
||
не изменяется.
|
||
|
||
В нём остаются два legacy-вызова:
|
||
|
||
ExchangeService().get_klines(...)
|
||
|
||
Это намеренное переходное состояние.
|
||
|
||
HTF должен быть переключён на канонический `Candle` отдельным Build после анализа его фактических контрактов и зависимостей.
|
||
|
||
---
|
||
|
||
## Новый тестовый файл
|
||
|
||
Добавлен:
|
||
|
||
`tests/unit/trading/market_analysis/test_market_analysis_service_candles.py`
|
||
|
||
Изначально тестовый файл имел имя:
|
||
|
||
test_service_candles.py
|
||
|
||
Однако такое имя уже использовалось другим тестовым модулем:
|
||
|
||
`tests/unit/integrations/exchange/test_service_candles.py`
|
||
|
||
Из-за одинакового basename pytest обнаружил import mismatch.
|
||
|
||
Новый Market Analysis тест был переименован в:
|
||
|
||
test_market_analysis_service_candles.py
|
||
|
||
После переименования конфликт модулей устранён.
|
||
|
||
---
|
||
|
||
## Покрытие специализированных тестов
|
||
|
||
Новый тестовый файл проверяет:
|
||
|
||
1. вызов `ExchangeService.get_candles()` с точными аргументами;
|
||
2. использование canonical symbol первой полученной свечи;
|
||
3. использование исходного запрошенного symbol при пустом наборе;
|
||
4. безопасный `UNKNOWN` при ошибке получения свечей;
|
||
5. отклонение нечислового конечного значения `NaN`;
|
||
6. преобразование `Decimal`-цен закрытия в `float` до передачи индикаторам.
|
||
|
||
---
|
||
|
||
## Targeted tests
|
||
|
||
Выполнена команда:
|
||
|
||
python -m pytest -q \
|
||
tests/unit/trading/market_analysis/test_market_analysis_service_candles.py
|
||
|
||
Результат:
|
||
|
||
5 passed in 0.12s
|
||
|
||
---
|
||
|
||
## Regression-набор Market Analysis и стратегий
|
||
|
||
До финальной полной проверки был выполнен набор:
|
||
|
||
python -m pytest -q \
|
||
tests/unit/trading/market_analysis \
|
||
tests/unit/trading/strategies/test_scalp_quote.py \
|
||
tests/unit/trading/strategies/test_trend_quote.py
|
||
|
||
Результат:
|
||
|
||
27 passed in 0.14s
|
||
|
||
---
|
||
|
||
## Полный regression suite
|
||
|
||
После устранения конфликта имён тестовых модулей выполнена команда:
|
||
|
||
python -m pytest -q
|
||
|
||
Результат:
|
||
|
||
794 passed in 2.97s
|
||
|
||
Регрессий не обнаружено.
|
||
|
||
---
|
||
|
||
## Архитектурная проверка legacy get_klines()
|
||
|
||
Выполнена команда:
|
||
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
"\.get_klines(" \
|
||
src/trading/market_analysis
|
||
|
||
Результат:
|
||
|
||
src/trading/market_analysis/htf.py:55: batch = ExchangeService().get_klines(
|
||
src/trading/market_analysis/htf.py:153: batch = ExchangeService().get_klines(
|
||
|
||
Таким образом:
|
||
|
||
- основной `MarketAnalysisService` больше не использует `get_klines()`;
|
||
- остаются ровно два legacy-вызова;
|
||
- оба находятся в `htf.py`;
|
||
- их миграция отложена на отдельный Build.
|
||
|
||
---
|
||
|
||
## Архитектурная проверка canonical get_candles()
|
||
|
||
Выполнена команда:
|
||
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
"\.get_candles(" \
|
||
src/trading/market_analysis \
|
||
tests/unit/trading/market_analysis
|
||
|
||
Production-вызов обнаружен в:
|
||
|
||
src/trading/market_analysis/service.py
|
||
|
||
Это подтверждает переключение основного orchestration-компонента на канонический API.
|
||
|
||
---
|
||
|
||
## Проверка отсутствия legacy-моделей в MarketAnalysisService
|
||
|
||
Выполнена команда:
|
||
|
||
grep -nE \
|
||
"Kline|KlineBatch|batch\.candles|batch\.symbol" \
|
||
src/trading/market_analysis/service.py
|
||
|
||
Вывод отсутствует.
|
||
|
||
Это подтверждает отсутствие в основном orchestration-файле зависимостей от:
|
||
|
||
Kline
|
||
KlineBatch
|
||
batch.candles
|
||
batch.symbol
|
||
|
||
---
|
||
|
||
## Проверка форматирования
|
||
|
||
Выполнена команда:
|
||
|
||
git diff --check
|
||
|
||
Вывод отсутствует.
|
||
|
||
Whitespace-ошибок не обнаружено.
|
||
|
||
---
|
||
|
||
## Фактический diff Build 050
|
||
|
||
Перед подготовкой документации состояние изменений:
|
||
|
||
app/src/trading/market_analysis/service.py | 41 +++++++++++++++++++++++++++++++----------
|
||
1 file changed, 31 insertions(+), 10 deletions(-)
|
||
|
||
Дополнительно добавлен новый тестовый файл:
|
||
|
||
tests/unit/trading/market_analysis/test_market_analysis_service_candles.py
|
||
|
||
Документация Build добавляется как:
|
||
|
||
docs/migrations/build_050.md
|
||
|
||
---
|
||
|
||
## Архитектурный результат
|
||
|
||
После Build 050 основной Market Analysis orchestration имеет следующий путь:
|
||
|
||
MarketAnalysisService.analyze()
|
||
↓
|
||
ExchangeService.get_candles()
|
||
↓
|
||
tuple[Candle, ...]
|
||
↓
|
||
canonical Candle consumers
|
||
↓
|
||
explicit Decimal → float boundary for closes
|
||
↓
|
||
Market Analysis calculations
|
||
|
||
Переходный HTF-путь пока остаётся:
|
||
|
||
htf.py
|
||
↓
|
||
ExchangeService.get_klines()
|
||
↓
|
||
KlineBatch
|
||
|
||
Такое разделение позволяет продолжить миграцию поэтапно без удаления рабочего compatibility-кода.
|
||
|
||
---
|
||
|
||
## Что намеренно не выполнено
|
||
|
||
Build 050 намеренно не включает:
|
||
|
||
- переключение `htf.py` на `get_candles()`;
|
||
- удаление `ExchangeService.get_klines()`;
|
||
- удаление `Kline`;
|
||
- удаление `KlineBatch`;
|
||
- удаление `_kline_from_candle()`;
|
||
- изменение Market Data Acquisition;
|
||
- изменение алгоритмов анализа;
|
||
- изменение торговой логики;
|
||
- изменение scoring;
|
||
- изменение порогов;
|
||
- изменение HTF-логики;
|
||
- изменение структуры каталогов;
|
||
- удаление рабочего legacy-кода.
|
||
|
||
---
|
||
|
||
## Критерии завершения
|
||
|
||
Build 050 считается завершённым, поскольку:
|
||
|
||
- `MarketAnalysisService` переключён на `ExchangeService.get_candles()`;
|
||
- основной orchestration получает `tuple[Candle, ...]`;
|
||
- зависимость от `KlineBatch` в `service.py` устранена;
|
||
- обращения `batch.candles` устранены;
|
||
- обращения `batch.symbol` устранены;
|
||
- canonical symbol берётся из первой свечи;
|
||
- для пустого набора сохраняется исходный symbol;
|
||
- `Decimal` close prices явно преобразуются в `float`;
|
||
- невалидные и не конечные close prices безопасно отклоняются;
|
||
- новый контракт покрыт специализированными тестами;
|
||
- targeted tests проходят;
|
||
- regression-набор Market Analysis и стратегий проходит;
|
||
- полный suite проходит;
|
||
- legacy-вызовы остаются только в `htf.py`;
|
||
- `git diff --check` чистый.
|
||
|
||
---
|
||
|
||
## Итог
|
||
|
||
**Build 050 завершён успешно.**
|
||
|
||
Текущее состояние:
|
||
|
||
MarketAnalysisService → get_candles() → tuple[Candle, ...]
|
||
HTF → get_klines() → KlineBatch
|
||
|
||
Результаты проверок:
|
||
|
||
Targeted tests: 5 passed
|
||
Market Analysis + strategies: 27 passed
|
||
Full test suite: 794 passed
|
||
git diff --check: clean
|
||
|
||
Следующий безопасный этап — отдельная миграция двух оставшихся HTF-вызовов с:
|
||
|
||
ExchangeService.get_klines()
|
||
|
||
на:
|
||
|
||
ExchangeService.get_candles()
|
||
|
||
без одновременного удаления legacy compatibility-контракта. |