# Build 059.3 — WebSocket OHLC Parser

**Проект:** Dzentra
**Подсистема:** Market Data Acquisition
**Этап:** 059.3
**Статус:** Completed

---

# Цель

Добавить parser для структурно проверенного события Dzengi WebSocket OHLC.

Parser должен преобразовывать:

```text
ValidatedWebSocketOhlcDocument
```

в:

```text
DzengiWebSocketOhlcEvent
```

При этом parser не должен выполнять:

- предметную проверку значений;
- проверку поддерживаемых интервалов;
- проверку допустимого типа свечи;
- преобразование timestamp в `datetime`;
- преобразование цен в `Decimal`;
- проверку OHLC-инвариантов;
- mapping во внутреннюю модель Dzentra.

---

# Причина изменения

После Build 059.2 проект умеет проверять структуру сообщения Dzengi WebSocket:

```json
{
  "status": "OK",
  "destination": "ohlc.event",
  "payload": {
    "symbol": "BTC/USD_LEVERAGE",
    "interval": "1m",
    "type": "classic",
    "t": 1784224740000,
    "o": 63992.0,
    "h": 64032.55,
    "l": 63984.0,
    "c": 64032.55
  }
}
```

Однако структурно проверенный документ ещё не преобразовывался в transport-модель адаптера Dzengi.

Build 059.3 добавляет отдельный parsing-слой между schema validation и value validation.

---

# Реализованные изменения

## Parser

В файл:

```text
src/market_data/acquisition/adapters/dzengi/parser.py
```

добавлена функция:

```text
parse_dzengi_websocket_ohlc()
```

Она принимает:

```text
ValidatedWebSocketOhlcDocument
```

и возвращает:

```text
DzengiWebSocketOhlcEvent
```

---

## Исключение parser-слоя

В файл:

```text
src/market_data/acquisition/exceptions.py
```

добавлено исключение:

```text
CandleWebSocketParseError
```

Оно используется только при невозможности преобразовать структурно проверенный WebSocket OHLC-документ в transport-модель адаптера.

---

## Unit-тесты

Создан файл:

```text
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
```

Он покрывает успешное преобразование и ошибки транспортных типов.

---

# Соответствие полей

Parser выполняет следующее преобразование:

| WebSocket payload | Transport model |
|---|---|
| `symbol` | `symbol` |
| `interval` | `interval` |
| `type` | `candle_type` |
| `t` | `open_time` |
| `o` | `open_price` |
| `h` | `high_price` |
| `l` | `low_price` |
| `c` | `close_price` |

Parser не переименовывает и не интерпретирует значения предметно. Он только переносит их в явную transport-модель адаптера.

---

# Проверка базовых транспортных типов

## Строковые поля

Следующие поля обязаны быть строками:

```text
symbol
interval
type
```

Если одно из них не является строкой, выбрасывается:

```text
CandleWebSocketParseError
```

Пример ошибки:

```text
$.payload.symbol должен быть строкой
```

---

## Timestamp

Поле:

```text
t
```

должно быть целым числом Python:

```text
int
```

Не принимаются:

```text
None
bool
float
str
list
```

`bool` отклоняется отдельно, несмотря на то что в Python он является подклассом `int`.

Parser не проверяет, является ли timestamp положительным или реалистичным. Это относится к Value Validation.

---

## Поля OHLC

Поля:

```text
o
h
l
c
```

могут быть представлены как:

```text
str
int
float
```

Это соответствует существующему транспортному типу:

```text
DzengiRawNumeric
```

Такой контракт необходим, поскольку внешнее API может передавать числовые значения как JSON-числа или строки.

Не принимаются:

```text
None
bool
list
dict
object
```

---

# Сохранение исходных числовых типов

Parser не преобразует цены.

Например, документ:

```json
{
  "o": "63992.00",
  "h": 64033,
  "l": 63984.0,
  "c": "64032.55"
}
```

преобразуется в transport-модель с теми же исходными типами:

```text
open_price  = str
high_price  = int
low_price   = float
close_price = str
```

Преобразование в `Decimal` будет выполняться только на mapping-этапе.

---

# Поддержка типов свечей

Parser сохраняет значение поля:

```text
type
```

в поле transport-модели:

```text
candle_type
```

Поддерживаются на уровне базового транспортного типа любые строки.

Например:

```text
classic
heikin-ashi
unknown
```

Parser не определяет допустимость значения.

Проверка, что значение входит в подтверждённый runtime-набор:

```text
classic
heikin-ashi
```

будет реализована в Build 059.4 — Value Validation.

---

# Отсутствие предметной валидации

Parser намеренно пропускает значения, которые имеют допустимый транспортный тип, но являются предметно некорректными.

Пример:

```text
symbol      = ""
interval    = "unsupported"
candle_type = "unknown"
open_time   = -1
open_price  = "NaN"
high_price  = -10
low_price   = 100
close_price = 0
```

Такой документ успешно преобразуется в `DzengiWebSocketOhlcEvent`.

Это ожидаемое поведение.

Следующий слой должен проверить:

- непустой символ;
- допустимый интервал;
- допустимый тип свечи;
- положительный timestamp;
- конечность чисел;
- положительность цен;
- OHLC-инварианты.

---

# Игнорирование metadata envelope

Parser использует только:

```text
document.payload
```

Поля внешнего envelope:

```text
status
destination
correlation_id
```

не участвуют в создании `DzengiWebSocketOhlcEvent`.

Это обеспечивает разделение ответственности:

```text
Schema Validation
    ↓
проверяет структуру envelope и payload

Parser
    ↓
преобразует payload в transport-модель

Value Validation
    ↓
проверяет предметную допустимость значений
```

---

# Новый конвейер

После Build 059.3 подготовленная часть конвейера выглядит так:

```text
Raw WebSocket message
        ↓
validate_dzengi_websocket_ohlc_schema()
        ↓
ValidatedWebSocketOhlcDocument
        ↓
parse_dzengi_websocket_ohlc()
        ↓
DzengiWebSocketOhlcEvent
        ↓
Value Validation
        ↓
Internal Close Event
        ↓
REST reconciliation
        ↓
Canonical Candle
```

На текущем этапе реализована часть до `DzengiWebSocketOhlcEvent` включительно.

---

# Unit Tests

Добавлен файл:

```text
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
```

Проверяются следующие сценарии:

- успешное создание transport-модели;
- корректное соответствие всех полей;
- сохранение типа `classic`;
- сохранение типа `heikin-ashi`;
- сохранение исходных числовых типов;
- отклонение нестрокового `symbol`;
- отклонение нестрокового `interval`;
- отклонение нестрокового `type`;
- отклонение `None` в timestamp;
- отклонение `bool` в timestamp;
- отклонение `float` в timestamp;
- отклонение строкового timestamp;
- отклонение коллекции вместо timestamp;
- отклонение некорректных типов для `o`;
- отклонение некорректных типов для `h`;
- отклонение некорректных типов для `l`;
- отклонение некорректных типов для `c`;
- отсутствие предметной валидации;
- игнорирование metadata внешнего envelope.

Всего выполнено:

```text
36 tests
```

---

# Проверка синтаксиса

Выполнена команда:

```bash
python -m compileall \
  src/market_data/acquisition/exceptions.py \
  src/market_data/acquisition/adapters/dzengi/parser.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
```

Результат:

```text
успешно
```

---

# Целевые тесты

Выполнена команда:

```bash
python -m pytest -q \
  tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
```

Результат:

```text
36 passed in 0.03s
```

---

# Регрессия parser-слоя

Выполнена команда:

```bash
python -m pytest -q \
  tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_candle_parser.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
```

Результат:

```text
80 passed in 0.05s
```

---

# Проверка форматирования

Выполнена команда:

```bash
git diff --check
```

Ошибок форматирования не обнаружено.

---

# Изменённые файлы

```text
src/market_data/acquisition/adapters/dzengi/parser.py
src/market_data/acquisition/exceptions.py
```

Создан файл:

```text
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
```

Документация этапа:

```text
docs/migrations/build_059_3.md
```

---

# Совместимость

Изменение полностью обратно совместимо.

Не изменены:

- REST Candles Feed;
- REST parser свечей;
- Quotes Feed;
- WebSocket Quote parser;
- WebSocket Quote Adapter;
- ExchangeService;
- Market Analysis;
- Trading;
- runtime.

Новый parser пока не подключён к рабочему WebSocket runtime и не влияет на действующий бот.

---

# Ограничения этапа

Build 059.3 не выполняет:

- проверку значения `status`;
- проверку значения `destination`;
- проверку допустимых интервалов;
- проверку допустимого типа свечи;
- проверку timestamp;
- проверку конечности OHLC;
- проверку положительности OHLC;
- проверку OHLC-инвариантов;
- преобразование в `Decimal`;
- преобразование времени в UTC `datetime`;
- создание внутреннего close event;
- REST reconciliation;
- выпуск канонической `Candle`.

Эти обязанности остаются за последующими подпунктами Build 059.

---

# Итог

Build 059.3 завершает parsing-слой Dzengi WebSocket OHLC.

Проект теперь умеет преобразовывать структурно проверенный документ:

```text
ValidatedWebSocketOhlcDocument
```

в transport-модель:

```text
DzengiWebSocketOhlcEvent
```

без смешивания parsing, value validation и domain mapping.

Следующий этап:

```text
Build 059.4 — Value Validation
```