591 lines
16 KiB
Markdown
591 lines
16 KiB
Markdown
# 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
|
||
``` |