797 lines
16 KiB
Markdown
797 lines
16 KiB
Markdown
# Build 059.6 — WebSocket OHLC Mapper
|
||
|
||
**Проект:** Dzentra
|
||
**Подсистема:** Market Data Acquisition
|
||
**Этап:** 059.6
|
||
**Статус:** Completed
|
||
|
||
---
|
||
|
||
# Цель
|
||
|
||
Реализовать преобразование проверенного WebSocket OHLC-события Dzengi во внутреннюю модель Dzentra:
|
||
|
||
```text
|
||
CandleCloseEvent
|
||
```
|
||
|
||
Mapper должен стать последней стадией обработки WebSocket-сообщения перед передачей события во внутренний runtime.
|
||
|
||
---
|
||
|
||
# Причина изменения
|
||
|
||
После завершения Build 059.5 система уже содержит:
|
||
|
||
```text
|
||
Raw WebSocket JSON
|
||
↓
|
||
Schema Validation
|
||
↓
|
||
ValidatedWebSocketOhlcDocument
|
||
↓
|
||
Parser
|
||
↓
|
||
DzengiWebSocketOhlcEvent
|
||
↓
|
||
Value Validation
|
||
```
|
||
|
||
Однако transport-модель адаптера:
|
||
|
||
```text
|
||
DzengiWebSocketOhlcEvent
|
||
```
|
||
|
||
не должна использоваться за пределами слоя интеграции с Dzengi.
|
||
|
||
Для внутренних компонентов требуется полностью source-independent модель:
|
||
|
||
```text
|
||
CandleCloseEvent
|
||
```
|
||
|
||
Build 059.6 добавляет именно этот переход.
|
||
|
||
---
|
||
|
||
# Реализовано
|
||
|
||
Изменены файлы:
|
||
|
||
```text
|
||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||
src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
|
||
```
|
||
|
||
---
|
||
|
||
# Новый mapper
|
||
|
||
Добавлена функция:
|
||
|
||
```text
|
||
map_dzengi_websocket_ohlc_to_candle_close_event()
|
||
```
|
||
|
||
Назначение функции:
|
||
|
||
```text
|
||
DzengiWebSocketOhlcEvent
|
||
↓
|
||
CandleCloseEvent
|
||
```
|
||
|
||
Mapper предполагает, что ранее уже были выполнены:
|
||
|
||
- schema validation;
|
||
- parsing;
|
||
- value validation.
|
||
|
||
Повторная проверка предметной корректности данных не выполняется.
|
||
|
||
---
|
||
|
||
# Выполняемые преобразования
|
||
|
||
Mapper выполняет только преобразования типов.
|
||
|
||
## Timestamp
|
||
|
||
Поле:
|
||
|
||
```text
|
||
t
|
||
```
|
||
|
||
преобразуется из:
|
||
|
||
```text
|
||
int (milliseconds)
|
||
```
|
||
|
||
в:
|
||
|
||
```text
|
||
datetime (UTC)
|
||
```
|
||
|
||
---
|
||
|
||
## OHLC
|
||
|
||
Поля:
|
||
|
||
```text
|
||
o
|
||
h
|
||
l
|
||
c
|
||
```
|
||
|
||
преобразуются из:
|
||
|
||
```text
|
||
str | int | float
|
||
```
|
||
|
||
в:
|
||
|
||
```text
|
||
Decimal
|
||
```
|
||
|
||
Все вычисления внутри runtime далее выполняются только с использованием `Decimal`.
|
||
|
||
---
|
||
|
||
## received_at
|
||
|
||
Mapper принимает дополнительный аргумент:
|
||
|
||
```text
|
||
received_at
|
||
```
|
||
|
||
Тип:
|
||
|
||
```text
|
||
datetime
|
||
```
|
||
|
||
Обязательное требование:
|
||
|
||
```text
|
||
timezone-aware datetime
|
||
```
|
||
|
||
Naive datetime считается ошибкой mapper.
|
||
|
||
---
|
||
|
||
## source
|
||
|
||
Mapper автоматически устанавливает источник:
|
||
|
||
```text
|
||
dzengi_websocket_ohlc
|
||
```
|
||
|
||
Тем самым внутренние модели больше не зависят от транспортного контракта адаптера.
|
||
|
||
---
|
||
|
||
# Новый тип ошибки
|
||
|
||
Добавлено исключение:
|
||
|
||
```text
|
||
CandleWebSocketMappingError
|
||
```
|
||
|
||
Исключение используется исключительно на этапе mapping.
|
||
|
||
Это позволяет разделить ошибки различных стадий обработки.
|
||
|
||
Схема обработки стала следующей:
|
||
|
||
```text
|
||
Transport
|
||
↓
|
||
CandleTransportError
|
||
|
||
Schema
|
||
↓
|
||
CandleWebSocketSchemaError
|
||
|
||
Parser
|
||
↓
|
||
CandleWebSocketParseError
|
||
|
||
Value Validation
|
||
↓
|
||
CandleWebSocketValueError
|
||
|
||
Mapper
|
||
↓
|
||
CandleWebSocketMappingError
|
||
```
|
||
|
||
Таким образом каждая стадия имеет собственный независимый контракт ошибок.
|
||
|
||
---
|
||
|
||
# Почему не используется CandleMappingError
|
||
|
||
В проекте уже существует:
|
||
|
||
```text
|
||
CandleMappingError
|
||
```
|
||
|
||
Однако он относится исключительно к REST Candles Feed.
|
||
|
||
REST mapper строит:
|
||
|
||
```text
|
||
Canonical Candle
|
||
```
|
||
|
||
WebSocket mapper строит:
|
||
|
||
```text
|
||
CandleCloseEvent
|
||
```
|
||
|
||
Это разные сущности.
|
||
|
||
Использование общего исключения привело бы к смешиванию двух различных контрактов.
|
||
|
||
Поэтому был введён отдельный тип ошибки.
|
||
|
||
---
|
||
|
||
# Использование существующих helper-функций
|
||
|
||
В mapper уже присутствуют helper-функции для REST-свечей.
|
||
|
||
Например:
|
||
|
||
```text
|
||
_required_candle_decimal()
|
||
```
|
||
|
||
Они намеренно не переиспользуются.
|
||
|
||
Причина:
|
||
|
||
они выбрасывают:
|
||
|
||
```text
|
||
CandleMappingError
|
||
```
|
||
|
||
WebSocket mapper обязан выбрасывать:
|
||
|
||
```text
|
||
CandleWebSocketMappingError
|
||
```
|
||
|
||
Поэтому были реализованы отдельные helper-функции.
|
||
|
||
---
|
||
|
||
# Новые helper-функции
|
||
|
||
Добавлены:
|
||
|
||
```text
|
||
_required_websocket_ohlc_decimal()
|
||
|
||
_websocket_ohlc_timestamp_ms_to_utc_datetime()
|
||
|
||
_require_websocket_ohlc_aware_datetime()
|
||
```
|
||
|
||
Они полностью изолированы от REST mapper.
|
||
|
||
Это исключает смешивание различных источников данных и их контрактов.
|
||
|
||
---
|
||
|
||
# Преобразование времени
|
||
|
||
Поле:
|
||
|
||
```text
|
||
t
|
||
```
|
||
|
||
содержит Unix timestamp в миллисекундах.
|
||
|
||
Mapper преобразует его через:
|
||
|
||
```python
|
||
datetime.fromtimestamp(
|
||
value / 1000,
|
||
tz=timezone.utc,
|
||
)
|
||
```
|
||
|
||
В результате:
|
||
|
||
```text
|
||
open_time
|
||
```
|
||
|
||
становится объектом:
|
||
|
||
```text
|
||
datetime UTC
|
||
```
|
||
|
||
Внутренние слои больше не работают с миллисекундными timestamp.
|
||
|
||
---
|
||
|
||
# Преобразование Decimal
|
||
|
||
Каждое поле:
|
||
|
||
```text
|
||
open
|
||
high
|
||
low
|
||
close
|
||
```
|
||
|
||
проходит преобразование:
|
||
|
||
```text
|
||
str | int | float
|
||
↓
|
||
Decimal
|
||
```
|
||
|
||
Дополнительно проверяется:
|
||
|
||
- корректность числа;
|
||
- конечность значения.
|
||
|
||
---
|
||
|
||
# Контракт mapper
|
||
|
||
Mapper не выполняет:
|
||
|
||
- schema validation;
|
||
- parsing;
|
||
- value validation;
|
||
- REST reconciliation;
|
||
- получение объёма;
|
||
- создание канонической `Candle`.
|
||
|
||
Единственная задача mapper:
|
||
|
||
```text
|
||
построить внутреннюю модель
|
||
CandleCloseEvent
|
||
из уже проверенного
|
||
DzengiWebSocketOhlcEvent
|
||
```
|
||
|
||
---
|
||
|
||
# Что mapper НЕ делает
|
||
|
||
Mapper намеренно не выполняет следующие действия.
|
||
|
||
## Не создаёт volume
|
||
|
||
Фактический runtime-контракт Dzengi содержит:
|
||
|
||
```text
|
||
symbol
|
||
interval
|
||
type
|
||
t
|
||
o
|
||
h
|
||
l
|
||
c
|
||
```
|
||
|
||
Поля:
|
||
|
||
```text
|
||
volume
|
||
```
|
||
|
||
не существует.
|
||
|
||
Поэтому mapper не должен:
|
||
|
||
- подставлять `0`;
|
||
- использовать `None`;
|
||
- вычислять объём самостоятельно.
|
||
|
||
Полный объём будет получен позднее через REST reconciliation.
|
||
|
||
---
|
||
|
||
## Не создаёт Candle
|
||
|
||
Mapper не строит:
|
||
|
||
```text
|
||
Candle
|
||
```
|
||
|
||
Причина проста.
|
||
|
||
Каноническая модель требует:
|
||
|
||
```text
|
||
volume
|
||
```
|
||
|
||
которого WebSocket не содержит.
|
||
|
||
Следовательно единственным корректным результатом mapper является:
|
||
|
||
```text
|
||
CandleCloseEvent
|
||
```
|
||
|
||
---
|
||
|
||
## Не выполняет REST reconciliation
|
||
|
||
Build 059.6 заканчивается на построении внутреннего события.
|
||
|
||
Дальнейший pipeline выглядит следующим образом:
|
||
|
||
```text
|
||
CandleCloseEvent
|
||
↓
|
||
REST reconciliation
|
||
↓
|
||
Canonical Candle
|
||
```
|
||
|
||
Сам reconciliation будет реализован отдельным этапом.
|
||
|
||
---
|
||
|
||
# Поддерживаемые типы свечей
|
||
|
||
Mapper сохраняет значение:
|
||
|
||
```text
|
||
candle_type
|
||
```
|
||
|
||
без изменения.
|
||
|
||
Поддерживаемые runtime-значения:
|
||
|
||
```text
|
||
classic
|
||
heikin-ashi
|
||
```
|
||
|
||
Mapper не интерпретирует содержимое свечи.
|
||
|
||
Он лишь переносит значение в внутреннюю модель.
|
||
|
||
---
|
||
|
||
# Обработка received_at
|
||
|
||
Поле:
|
||
|
||
```text
|
||
received_at
|
||
```
|
||
|
||
не вычисляется автоматически.
|
||
|
||
Оно передаётся извне.
|
||
|
||
Это решение позволяет:
|
||
|
||
- использовать единый момент получения сообщения;
|
||
- исключить влияние задержек внутри mapper;
|
||
- обеспечить единый источник времени для всей системы.
|
||
|
||
Mapper только проверяет:
|
||
|
||
```text
|
||
timezone-aware datetime
|
||
```
|
||
|
||
Naive datetime приводит к ошибке:
|
||
|
||
```text
|
||
CandleWebSocketMappingError
|
||
```
|
||
|
||
---
|
||
|
||
# Конвейер после Build 059.6
|
||
|
||
После завершения этапа полный pipeline выглядит следующим образом:
|
||
|
||
```text
|
||
Raw WebSocket message
|
||
↓
|
||
Schema Validation
|
||
↓
|
||
ValidatedWebSocketOhlcDocument
|
||
↓
|
||
Parser
|
||
↓
|
||
DzengiWebSocketOhlcEvent
|
||
↓
|
||
Value Validation
|
||
↓
|
||
Mapper
|
||
↓
|
||
CandleCloseEvent
|
||
```
|
||
|
||
На этом Build 059.6 завершается.
|
||
|
||
Следующий этап добавит использование новой модели внутри runtime.
|
||
|
||
---
|
||
|
||
# Unit Tests
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
|
||
```
|
||
|
||
Проверяются:
|
||
|
||
- построение `CandleCloseEvent`;
|
||
- преобразование всех полей;
|
||
- преобразование timestamp;
|
||
- преобразование числовых типов в `Decimal`;
|
||
- сохранение `received_at`;
|
||
- поддержка `heikin-ashi`;
|
||
- обрезка пробелов у symbol;
|
||
- проверка timezone-aware datetime;
|
||
- ошибка при невозможности преобразовать timestamp;
|
||
- ошибка при некорректном Decimal;
|
||
- ошибка при бесконечных значениях (`NaN`, `Infinity`);
|
||
- отсутствие создания `volume`.
|
||
|
||
Всего выполнено:
|
||
|
||
```text
|
||
16 tests
|
||
```
|
||
|
||
---
|
||
|
||
# Проверка синтаксиса
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m compileall \
|
||
src/market_data/acquisition/exceptions.py \
|
||
src/market_data/acquisition/adapters/dzengi/mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
успешно
|
||
```
|
||
|
||
---
|
||
|
||
# Целевые тесты
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
16 passed
|
||
```
|
||
|
||
---
|
||
|
||
# Регрессия mapper
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_candle_mapper.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
52 passed
|
||
```
|
||
|
||
---
|
||
|
||
# Дополнительная регрессия
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q \
|
||
tests/unit/market_data/acquisition/models \
|
||
tests/unit/market_data/acquisition/validation/test_websocket_ohlc_values.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
126 passed
|
||
```
|
||
|
||
---
|
||
|
||
# Проверка форматирования
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
git diff --check
|
||
```
|
||
|
||
Ошибок форматирования не обнаружено.
|
||
|
||
---
|
||
|
||
# Изменённые файлы
|
||
|
||
```text
|
||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||
|
||
src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_mapper.py
|
||
```
|
||
|
||
Документация:
|
||
|
||
```text
|
||
docs/migrations/build_059_6.md
|
||
```
|
||
|
||
---
|
||
|
||
# Совместимость
|
||
|
||
Build 059.6 полностью обратно совместим.
|
||
|
||
Не изменены:
|
||
|
||
- REST Candles Feed;
|
||
- REST Candle mapper;
|
||
- Quote mapper;
|
||
- Instrument mapper;
|
||
- ExchangeService;
|
||
- runtime;
|
||
- Trading;
|
||
- Market Analysis;
|
||
- каноническая модель `Candle`.
|
||
|
||
Новый mapper пока не используется существующим runtime и не влияет на работу действующего торгового бота.
|
||
|
||
---
|
||
|
||
# Архитектурное значение
|
||
|
||
Build 059.6 завершает формирование полного внутреннего конвейера преобразования WebSocket OHLC.
|
||
|
||
После этого этапа система имеет чёткое разделение уровней ответственности:
|
||
|
||
```text
|
||
Transport Layer
|
||
│
|
||
▼
|
||
DzengiWebSocketOhlcEvent
|
||
│
|
||
▼
|
||
Validation Layer
|
||
│
|
||
▼
|
||
Mapper Layer
|
||
│
|
||
▼
|
||
CandleCloseEvent
|
||
```
|
||
|
||
Каждый уровень отвечает только за собственную задачу:
|
||
|
||
| Уровень | Ответственность |
|
||
|---------|-----------------|
|
||
| Schema Validation | Проверка структуры входящего сообщения |
|
||
| Parser | Построение transport-модели |
|
||
| Value Validation | Проверка допустимости значений |
|
||
| Mapper | Преобразование transport-модели во внутреннюю модель |
|
||
| Runtime | Использование внутренней модели |
|
||
|
||
Такое разделение соответствует общей архитектуре Dzentra и исключает смешивание обязанностей между слоями.
|
||
|
||
---
|
||
|
||
# Ограничения этапа
|
||
|
||
Build 059.6 намеренно **не реализует**:
|
||
|
||
- подписку на WebSocket;
|
||
- публикацию события в runtime;
|
||
- REST reconciliation;
|
||
- получение объёма свечи;
|
||
- создание канонической `Candle`;
|
||
- проверку соответствия REST и WebSocket OHLC;
|
||
- восстановление после разрыва соединения;
|
||
- повторную синхронизацию истории свечей.
|
||
|
||
Все перечисленные задачи относятся к последующим этапам Build 059.
|
||
|
||
---
|
||
|
||
# Итог
|
||
|
||
Build 059.6 добавляет полноценный mapper:
|
||
|
||
```text
|
||
DzengiWebSocketOhlcEvent
|
||
↓
|
||
CandleCloseEvent
|
||
```
|
||
|
||
В результате система получила:
|
||
|
||
- источник-независимую внутреннюю модель события закрытия свечи;
|
||
- корректное преобразование timestamp в `datetime (UTC)`;
|
||
- преобразование цен в `Decimal`;
|
||
- собственный контракт ошибок (`CandleWebSocketMappingError`);
|
||
- полную изоляцию WebSocket-контракта Dzengi от внутренних компонентов Dzentra;
|
||
- набор unit-тестов, подтверждающих корректность преобразования и обработки ошибок.
|
||
|
||
После Build 059.6 цепочка обработки WebSocket OHLC стала полностью завершённой вплоть до внутреннего события Dzentra.
|
||
|
||
---
|
||
|
||
# Следующий этап
|
||
|
||
```text
|
||
Build 059.7 — WebSocket Adapter
|
||
```
|
||
|
||
На этом этапе новый mapper будет встроен в адаптер получения рыночных данных, чтобы при поступлении подтверждённого WebSocket OHLC-сообщения формировался `CandleCloseEvent`, который затем сможет быть передан в механизм REST reconciliation. |