# 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-контракта.