feat: add market data architecture and complete migration through build 039

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit a996f2f797
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,591 @@
# Build 028 — Dzengi REST Quote Models, Parser и Validation
## Статус
**Завершён**
---
## 1. Цель Build 028
Цель Build 028 — реализовать специализированный слой приёма, разбора и первичной проверки REST-ответа Dzengi для текущей рыночной котировки инструмента, не изменяя существующее поведение работающего бота и не подключая новую реализацию к production runtime до следующих этапов миграции.
Build является частью поэтапной миграции подсистемы:
**Quotes Feed**
в новую архитектуру:
```text
src/market_data/acquisition/
```
На данном этапе реализованы:
- модель сырого REST-ответа Dzengi;
- parser REST-ответа `/api/v1/ticker/24hr`;
- проверка структуры входящего payload;
- проверка допустимости значений котировки;
- специализированные ошибки обработки quote payload.
Подключение mapper, handler, feed, service, store и перевод runtime-потребителей в данный Build не входят.
---
## 2. Место Build 028 в плане миграции Quotes Feed
Утверждённая последовательность:
```text
Build 026 — Аудит текущего контура Quotes Feed
Build 027 — Каноническая модель Quote и специализированные контракты
Build 028 — Dzengi REST quote models, parser и validation
Build 029 — Dzengi mapper и Quotes Handler
Build 030 — Quotes Feed и регистрация в Acquisition Service
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
Build 032 — Канонический Quote Store
Build 033 — Перенос MarketPriceCache на Quote Store
Build 034 — Dzengi WebSocket quote parsing и адаптер
Build 035 — Перевод market runtime на Quotes Feed
Build 036 — Перевод read-only и UI-потребителей
Build 037 — Перевод execution-потребителей
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
Build 039 — Удаление legacy quote parsing и MarketPriceCache
Build 040 — Финальная архитектурная проверка Quotes Feed
```
Build 028 продолжает фундамент, созданный в Build 027.
Целевая цепочка после завершения следующих этапов:
```text
Dzengi REST /api/v1/ticker/24hr
adapters/dzengi/rest.py
adapters/dzengi/parser.py
adapters/dzengi/models.py
validation/schema.py
validation/values.py
adapters/dzengi/mapper.py
handlers/quotes_handler.py
feeds/quotes_feed.py
acquisition/service.py
Quote Store
потребители платформы
```
---
## 3. Исходные данные
Для проектирования реализации использован реальный успешный ответ Dzengi:
```json
{
"askPrice": "64159.55",
"bidPrice": "64159.45",
"closeTime": 1783887270312,
"highPrice": "64261.45",
"lastPrice": "64159.45",
"lastQty": "5.0",
"lowPrice": "63590.7",
"openPrice": "63785.75",
"openTime": 1783814400000,
"prevClosePrice": "63785.75",
"priceChange": "368.85",
"priceChangePercent": "0.57822",
"quoteVolume": "616402.92146",
"symbol": "BTC/USD_LEVERAGE",
"volume": "9.6002",
"weightedAvgPrice": "64159.50"
}
```
Для базовой модели текущей котировки используются поля:
```text
symbol
lastPrice
bidPrice
askPrice
closeTime
```
Остальные поля ответа `/api/v1/ticker/24hr` относятся к расширенной 24-часовой статистике рынка и не включаются в базовую модель `Quote`.
Это сохраняет правильное разделение ответственностей между:
- текущей котировкой;
- рыночной статистикой;
- OHLCV;
- trades;
- order book;
- другими специализированными типами рыночных данных.
---
## 4. Реализованные компоненты
В рамках Build 028 изменены следующие файлы:
```text
src/market_data/acquisition/adapters/dzengi/models.py
src/market_data/acquisition/adapters/dzengi/parser.py
src/market_data/acquisition/validation/schema.py
src/market_data/acquisition/validation/values.py
src/market_data/acquisition/exceptions.py
```
Добавлены специализированные тесты:
```text
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py
tests/unit/market_data/acquisition/validation/test_quote_schema.py
tests/unit/market_data/acquisition/validation/test_quote_values.py
```
---
## 5. Dzengi REST Quote Model
В файле:
```text
src/market_data/acquisition/adapters/dzengi/models.py
```
реализована модель сырой котировки Dzengi.
Её ответственность:
- представить уже разобранные поля ответа Dzengi;
- сохранить биржевую семантику полей;
- не зависеть от legacy-моделей `TickerPrice` и `MarketPriceSnapshot`;
- не выполнять бизнес-интерпретацию;
- не выполнять преобразование во внутреннюю каноническую модель `Quote`.
Архитектурная граница:
```text
Dzengi API payload
Dzengi REST quote model
mapper
canonical Quote
```
Модель адаптера является специфичной для Dzengi и не должна использоваться напрямую верхними слоями платформы.
---
## 6. REST Quote Parser
В файле:
```text
src/market_data/acquisition/adapters/dzengi/parser.py
```
реализован специализированный parser REST-котировки.
Его ответственность:
1. принять необработанный ответ API;
2. определить фактический quote payload;
3. поддержать прямую структуру ответа;
4. поддержать wrapped payload;
5. проверить структуру через schema validation;
6. извлечь необходимые поля;
7. проверить значения через value validation;
8. вернуть специализированную Dzengi quote model.
Поддерживаемые формы payload:
```text
Прямой payload
```
```json
{
"symbol": "BTC/USD_LEVERAGE",
"lastPrice": "64159.45",
"bidPrice": "64159.45",
"askPrice": "64159.55",
"closeTime": 1783887270312
}
```
и wrapped payload:
```json
{
"payload": {
"symbol": "BTC/USD_LEVERAGE",
"lastPrice": "64159.45",
"bidPrice": "64159.45",
"askPrice": "64159.55",
"closeTime": 1783887270312
}
}
```
Parser не создаёт канонический `Quote`. Это ответственность mapper, реализуемого в Build 029.
---
## 7. Schema Validation
В файле:
```text
src/market_data/acquisition/validation/schema.py
```
реализована проверка структуры quote payload.
Проверяются обязательные поля:
```text
symbol
lastPrice
bidPrice
askPrice
closeTime
```
Schema validation отвечает только на вопрос:
> Имеет ли входящее сообщение необходимую структуру для дальнейшей обработки?
Она не должна:
- преобразовывать значения;
- вычислять midpoint;
- определять freshness;
- создавать `Quote`;
- обращаться к сети;
- обращаться к store;
- зависеть от `ExchangeService`.
---
## 8. Value Validation
В файле:
```text
src/market_data/acquisition/validation/values.py
```
реализована проверка допустимости значений REST-котировки.
Контролируются следующие инварианты:
```text
symbol != empty
last_price > 0
bid_price > 0
ask_price > 0
close_time >= 0
bid_price <= ask_price
```
Проверка:
```text
bid_price <= ask_price
```
является важным базовым инвариантом котировки.
Payload, в котором:
```text
bid_price > ask_price
```
не должен бесконтрольно попадать в канонический слой платформы.
---
## 9. Исключения
В файле:
```text
src/market_data/acquisition/exceptions.py
```
используются специализированные исключения Acquisition layer для ошибок обработки рыночных данных.
Ошибки quote parsing и validation не должны зависеть от:
```text
src/integrations/exchange/exceptions.py
```
Это необходимо для соблюдения направления зависимостей:
```text
market_data/acquisition
X
integrations/exchange legacy layer
```
Новая подсистема Acquisition не должна архитектурно зависеть от legacy `ExchangeService`.
---
## 10. Архитектурные решения Build 028
### 10.1. Каноническая модель не зависит от формата Dzengi
Поля API:
```text
lastPrice
bidPrice
askPrice
closeTime
```
существуют только внутри Dzengi adapter layer.
Во внутренних слоях платформы используются канонические имена:
```text
last_price
bid_price
ask_price
source_timestamp_ms
```
Преобразование между ними является ответственностью mapper.
---
### 10.2. Parser не выполняет mapping
Разделение сохраняется строго:
```text
parser
разбирает внешний payload
validation
проверяет структуру и значения
mapper
преобразует adapter model в canonical model
```
Это предотвращает смешивание:
- API-specific parsing;
- validation;
- domain mapping.
---
### 10.3. REST quote не зависит от legacy TickerPrice
Новая цепочка не использует:
```text
src.integrations.exchange.models.TickerPrice
```
`TickerPrice` остаётся временной legacy-моделью и будет удалён только после перевода всех потребителей согласно плану миграции.
---
### 10.4. REST quote не зависит от MarketPriceCache
Build 028 не изменяет:
```text
src/integrations/exchange/market_cache.py
```
и не записывает данные в:
```text
MarketPriceCache
```
Миграция хранения выполняется отдельно:
```text
Build 032 — Канонический Quote Store
Build 033 — Перенос MarketPriceCache на Quote Store
```
---
### 10.5. Runtime-поведение бота не изменено
На этапе Build 028:
- новый parser не подключён к production runtime;
- `ExchangeService` продолжает работать по прежнему интерфейсу;
- `MarketPriceCache` не изменён;
- `MarketDataRunner` не изменён;
- execution-потребители не изменены;
- UI-потребители не изменены.
Таким образом, Build 028 является безопасным additive-этапом миграции.
---
## 11. Что намеренно не реализовано
В Build 028 не входят:
```text
Dzengi mapper
Quotes Handler
Quotes Feed
регистрация Quotes Feed
подключение к Acquisition Service
подключение к ExchangeService facade
Quote Store
перенос MarketPriceCache
WebSocket quote parser
перевод MarketDataRunner
перевод UI-потребителей
перевод execution-потребителей
удаление TickerPrice
удаление market snapshot dict layer
удаление MarketPriceCache
```
Эти изменения выполняются только в соответствующих последующих Build.
---
## 12. Проверки
Выполнена синтаксическая проверка:
```bash
python -m py_compile \
src/market_data/acquisition/adapters/dzengi/models.py \
src/market_data/acquisition/adapters/dzengi/parser.py \
src/market_data/acquisition/validation/schema.py \
src/market_data/acquisition/validation/values.py \
src/market_data/acquisition/exceptions.py \
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \
tests/unit/market_data/acquisition/validation/test_quote_schema.py \
tests/unit/market_data/acquisition/validation/test_quote_values.py
```
Результат:
```text
Успешно.
```
Выполнены специализированные тесты Build 028:
```bash
python -m pytest \
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \
tests/unit/market_data/acquisition/validation/test_quote_schema.py \
tests/unit/market_data/acquisition/validation/test_quote_values.py \
-q
```
Результат:
```text
22 passed in 0.02s
```
Выполнена полная регрессия проекта:
```bash
python -m pytest -q
```
Результат:
```text
445 passed in 0.27s
```
Регрессий не обнаружено.
---
## 13. Критерии завершения Build 028
Build 028 считается завершённым, поскольку выполнены все необходимые условия:
- [x] получен реальный успешный ответ `/api/v1/ticker/24hr`;
- [x] определён минимальный набор полей текущей котировки;
- [x] реализована специализированная Dzengi REST quote model;
- [x] реализован REST quote parser;
- [x] поддержан прямой payload;
- [x] поддержан wrapped payload;
- [x] реализована schema validation;
- [x] реализована value validation;
- [x] проверяются положительные цены;
- [x] проверяется временная метка;
- [x] проверяется инвариант `bid_price <= ask_price`;
- [x] новая реализация не зависит от legacy `TickerPrice`;
- [x] новая реализация не зависит от `MarketPriceCache`;
- [x] production runtime не изменён;
- [x] специализированные тесты проходят;
- [x] полная регрессия проходит.
---
## 14. Итог
В результате Build 028 создан специализированный входной контур для REST-котировок Dzengi:
```text
Dzengi /api/v1/ticker/24hr
raw payload
schema validation
Dzengi quote parser
value validation
Dzengi REST quote model
```
При этом сохранены ключевые архитектурные свойства миграции:
- новая реализация добавлена параллельно legacy-контуру;
- работающий бот не сломан;
- публичное поведение `ExchangeService` не изменено;
- отсутствует зависимость новой Acquisition subsystem от legacy quote models;
- parsing, validation и будущий mapping разделены по ответственности;
- сохранена возможность безопасного поэтапного переключения потребителей.
**Build 028 завершён.**
Следующий этап:
```text
Build 029 — Dzengi mapper и Quotes Handler
```