feat: add market data architecture and complete migration through build 039
This commit is contained in:
325
docs/migrations/build_001.md
Normal file
325
docs/migrations/build_001.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Build 001 — Внутренняя модель Instrument Reference Data
|
||||
|
||||
**Проект:** Dzentra
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Миграция:** Instrument Reference Data
|
||||
**Статус:** ✅ Завершён
|
||||
**Дата:** 2026-07-10
|
||||
|
||||
---
|
||||
|
||||
# Цель Build
|
||||
|
||||
Создать независимую внутреннюю модель Instrument Reference Data.
|
||||
|
||||
На данном этапе запрещается:
|
||||
|
||||
- изменять ExchangeService;
|
||||
- изменять существующий runtime;
|
||||
- менять Telegram UI;
|
||||
- менять Symbol Validation;
|
||||
- подключать новую модель к production-коду.
|
||||
|
||||
Build создаёт исключительно новую внутреннюю модель, которая станет целевой моделью для последующего mapper.
|
||||
|
||||
---
|
||||
|
||||
# Причина выполнения Build
|
||||
|
||||
В утверждённой архитектуре Dzentra модель предметной области должна существовать отдельно от:
|
||||
|
||||
- REST API Dzengi;
|
||||
- parser;
|
||||
- mapper;
|
||||
- ExchangeService;
|
||||
- Telegram UI;
|
||||
- runtime.
|
||||
|
||||
Поэтому сначала создаётся независимая модель Instrument, а только затем будут строиться parser и mapper.
|
||||
|
||||
---
|
||||
|
||||
# Проанализированные файлы
|
||||
|
||||
В ходе Build были полностью проанализированы:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/integrations/exchange/models.py
|
||||
app/src/integrations/exchange/symbol_utils.py
|
||||
app/src/integrations/exchange/rest_client.py
|
||||
app/src/integrations/exchange/status.py
|
||||
|
||||
app/src/telegram/ui/currency_ui.py
|
||||
|
||||
docs/stages/stage-03_3-exchange_info.md
|
||||
docs/decisions/0007-symbol-validation.md
|
||||
```
|
||||
|
||||
Также были проанализированы:
|
||||
|
||||
- сохранённый runtime-ответ `/exchangeInfo`;
|
||||
- OpenAPI Dzengi;
|
||||
- все текущие потребители ExchangeSymbol;
|
||||
- результаты grep по всему проекту.
|
||||
|
||||
---
|
||||
|
||||
# Основные выводы анализа
|
||||
|
||||
Установлено:
|
||||
|
||||
- ExchangeSymbol создаётся только в одном месте;
|
||||
- parser и mapper сейчас объединены внутри ExchangeService;
|
||||
- SymbolValidationResult используется только ExchangeService;
|
||||
- normalize_symbol() и symbol_candidates() централизованы;
|
||||
- отдельного parser ещё не существует;
|
||||
- отдельного mapper ещё не существует;
|
||||
- кэш справочника представляет собой неуправляемый singleton без TTL.
|
||||
|
||||
Также подтверждено:
|
||||
|
||||
- новый Build не должен менять существующий ExchangeService;
|
||||
- новая модель не должна зависеть от Telegram UI;
|
||||
- runtime-статусы не являются частью Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
# Анализ реального ответа exchangeInfo
|
||||
|
||||
Подтверждено наличие следующих полей:
|
||||
|
||||
Обязательные:
|
||||
|
||||
- symbol
|
||||
- name
|
||||
- status
|
||||
- baseAsset
|
||||
- quoteAsset
|
||||
- marketModes
|
||||
- marketType
|
||||
- tickSize
|
||||
|
||||
Через filters:
|
||||
|
||||
- stepSize
|
||||
- minQty
|
||||
- maxQty
|
||||
- minNotional
|
||||
|
||||
Также обнаружены дополнительные справочные поля:
|
||||
|
||||
- assetType
|
||||
- orderTypes
|
||||
- baseAssetPrecision
|
||||
- quotePrecision
|
||||
- tickValue
|
||||
- country
|
||||
- sector
|
||||
- industry
|
||||
- tradingHours
|
||||
|
||||
---
|
||||
|
||||
# Принятые архитектурные решения
|
||||
|
||||
## Новая модель не копирует ExchangeSymbol
|
||||
|
||||
Новая модель является самостоятельной внутренней моделью предметной области.
|
||||
|
||||
---
|
||||
|
||||
## Runtime не входит в Instrument
|
||||
|
||||
Из модели исключены:
|
||||
|
||||
- title
|
||||
- message
|
||||
- ui_line
|
||||
- reason
|
||||
- is_open
|
||||
- is_available
|
||||
- is_auth_ok
|
||||
|
||||
Эти поля относятся к Runtime Status.
|
||||
|
||||
---
|
||||
|
||||
## Использование Decimal
|
||||
|
||||
Для следующих значений принято использовать Decimal:
|
||||
|
||||
- tick_size
|
||||
- tick_value
|
||||
- step_size
|
||||
- min_qty
|
||||
- max_qty
|
||||
- min_notional
|
||||
|
||||
Причина:
|
||||
|
||||
данные используются при нормализации цен и количества и не должны терять точность.
|
||||
|
||||
Поскольку модель пока нигде не используется, изменение не влияет на работающего бота.
|
||||
|
||||
Классификация:
|
||||
|
||||
**Улучшение надёжности.**
|
||||
|
||||
---
|
||||
|
||||
## Не включены в Instrument
|
||||
|
||||
Сознательно исключены:
|
||||
|
||||
- trading_fee
|
||||
- long_rate
|
||||
- short_rate
|
||||
- swap_charge_interval
|
||||
- min_sl_gap
|
||||
- max_sl_gap
|
||||
- min_tp_gap
|
||||
- max_tp_gap
|
||||
|
||||
Причина:
|
||||
|
||||
данные относятся к другим предметным подсистемам.
|
||||
|
||||
---
|
||||
|
||||
# Созданные файлы
|
||||
|
||||
Создан:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/models/instrument.py
|
||||
```
|
||||
|
||||
Создан:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/models/test_instrument.py
|
||||
```
|
||||
|
||||
Другие production-файлы не изменялись.
|
||||
|
||||
---
|
||||
|
||||
# Проверки
|
||||
|
||||
Выполнены проверки.
|
||||
|
||||
## Unit Test новой модели
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
```
|
||||
4 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Импорт модели
|
||||
|
||||
Проверено:
|
||||
|
||||
- импорт проходит;
|
||||
- dataclass создаётся;
|
||||
- Decimal работает;
|
||||
- tuple работают.
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
## Компиляция
|
||||
|
||||
Проверено:
|
||||
|
||||
```
|
||||
python -m py_compile
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
## Legacy Compatibility
|
||||
|
||||
Проверено:
|
||||
|
||||
- ExchangeSymbol
|
||||
- SymbolValidationResult
|
||||
- ExchangeService
|
||||
- normalize_symbol()
|
||||
- symbol_candidates()
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Полностью совместимо.
|
||||
|
||||
---
|
||||
|
||||
## Проверка отсутствия подключения новой модели
|
||||
|
||||
Подтверждено:
|
||||
|
||||
новая модель используется исключительно в unit-тестах.
|
||||
|
||||
Production-код её не импортирует.
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
## Полный набор тестов
|
||||
|
||||
Выполнено:
|
||||
|
||||
```
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
5 passed
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
# Итог Build
|
||||
|
||||
Build 001 завершён успешно.
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- создана независимая модель Instrument;
|
||||
- модель не зависит от legacy-кода;
|
||||
- обратная совместимость полностью сохранена;
|
||||
- работающий бот не изменён;
|
||||
- новая архитектура готова к реализации Build 002.
|
||||
|
||||
---
|
||||
|
||||
# Следующий Build
|
||||
|
||||
Build 002
|
||||
|
||||
**Dzengi Raw Models**
|
||||
|
||||
Следующий этап создаст модели сырого ответа REST API Dzengi.
|
||||
|
||||
На этом этапе ExchangeService по-прежнему изменяться не будет.
|
||||
421
docs/migrations/build_002.md
Normal file
421
docs/migrations/build_002.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Build 002 — Raw Models Dzengi exchangeInfo
|
||||
|
||||
**Проект:** Dzentra
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Миграция:** Instrument Reference Data
|
||||
**Статус:** ✅ Завершён
|
||||
**Дата:** 2026-07-10
|
||||
|
||||
---
|
||||
|
||||
# Цель Build
|
||||
|
||||
Создать транспортные модели (Raw Models), полностью описывающие ответ REST API Dzengi `exchangeInfo`.
|
||||
|
||||
На данном этапе запрещалось:
|
||||
|
||||
- изменять ExchangeService;
|
||||
- изменять ExchangeSymbol;
|
||||
- изменять SymbolValidationResult;
|
||||
- изменять runtime;
|
||||
- изменять Telegram UI;
|
||||
- выполнять parser JSON;
|
||||
- выполнять mapper в предметную модель Instrument.
|
||||
|
||||
Build создаёт исключительно транспортный контракт между REST API и будущим parser.
|
||||
|
||||
---
|
||||
|
||||
# Причина выполнения Build
|
||||
|
||||
После Build 001 уже существует независимая предметная модель:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
```
|
||||
|
||||
Следующим архитектурным слоем является транспортная модель адаптера.
|
||||
|
||||
До начала Build 002 существующая реализация выглядела следующим образом:
|
||||
|
||||
```text
|
||||
REST
|
||||
↓
|
||||
dict
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Parser и mapper были объединены внутри `ExchangeService`.
|
||||
|
||||
Это нарушало принцип разделения ответственности.
|
||||
|
||||
После Build 002 появилась отдельная транспортная модель:
|
||||
|
||||
```text
|
||||
REST
|
||||
↓
|
||||
Raw Models
|
||||
```
|
||||
|
||||
которая станет входом для parser в следующем Build.
|
||||
|
||||
---
|
||||
|
||||
# Проанализированные материалы
|
||||
|
||||
Перед реализацией были полностью проанализированы:
|
||||
|
||||
```text
|
||||
app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json
|
||||
docs/market_intelligence/information/dzengi_openapi.json
|
||||
```
|
||||
|
||||
Также были повторно использованы результаты анализа:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
и результаты grep по использованию ExchangeSymbol.
|
||||
|
||||
---
|
||||
|
||||
# Основные выводы анализа
|
||||
|
||||
Подтверждено:
|
||||
|
||||
реальный ответ API значительно богаче текущей модели ExchangeSymbol.
|
||||
|
||||
В ответе присутствуют:
|
||||
|
||||
- symbol
|
||||
- name
|
||||
- status
|
||||
- assetType
|
||||
- baseAsset
|
||||
- baseAssetPrecision
|
||||
- quoteAsset
|
||||
- quoteAssetId
|
||||
- quotePrecision
|
||||
- orderTypes
|
||||
- marketModes
|
||||
- marketType
|
||||
- country
|
||||
- sector
|
||||
- industry
|
||||
- tradingHours
|
||||
- tickSize
|
||||
- tickValue
|
||||
- tradingFee
|
||||
- exchangeFee
|
||||
- longRate
|
||||
- shortRate
|
||||
- swapChargeInterval
|
||||
- minSLGap
|
||||
- maxSLGap
|
||||
- minTPGap
|
||||
- maxTPGap
|
||||
- filters
|
||||
- rateLimits
|
||||
- exchangeFilters
|
||||
|
||||
Часть этих данных ранее полностью терялась.
|
||||
|
||||
---
|
||||
|
||||
# Принятые архитектурные решения
|
||||
|
||||
## 1. Raw Models полностью отделены от предметной модели
|
||||
|
||||
Созданы транспортные модели.
|
||||
|
||||
Они:
|
||||
|
||||
- ничего не вычисляют;
|
||||
- ничего не нормализуют;
|
||||
- ничего не валидируют.
|
||||
|
||||
Они только описывают транспортный контракт.
|
||||
|
||||
---
|
||||
|
||||
## 2. Поддержка двух форматов ответа API
|
||||
|
||||
Поддерживаются оба варианта:
|
||||
|
||||
### Wrapped
|
||||
|
||||
```text
|
||||
status
|
||||
correlationId
|
||||
payload
|
||||
```
|
||||
|
||||
и
|
||||
|
||||
### Unwrapped
|
||||
|
||||
```text
|
||||
timezone
|
||||
serverTime
|
||||
symbols
|
||||
```
|
||||
|
||||
Parser следующего Build сможет привести оба формата к единому контракту.
|
||||
|
||||
Классификация:
|
||||
|
||||
**Улучшение надёжности.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Decimal сознательно не используется
|
||||
|
||||
Raw Models сохраняют транспортный тип.
|
||||
|
||||
Например:
|
||||
|
||||
```json
|
||||
"stepSize": "0.001"
|
||||
```
|
||||
|
||||
остаётся строкой.
|
||||
|
||||
Преобразование в Decimal является обязанностью mapper.
|
||||
|
||||
---
|
||||
|
||||
## 4. Filters представлены отдельной иерархией
|
||||
|
||||
Созданы:
|
||||
|
||||
```text
|
||||
DzengiLotSizeFilter
|
||||
DzengiMinNotionalFilter
|
||||
DzengiUnknownFilter
|
||||
```
|
||||
|
||||
Неизвестные фильтры не приводят к ошибке.
|
||||
|
||||
Они сохраняются для дальнейшего анализа.
|
||||
|
||||
Классификация:
|
||||
|
||||
**Улучшение надёжности.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Все коллекции являются immutable
|
||||
|
||||
Используются:
|
||||
|
||||
```python
|
||||
tuple
|
||||
```
|
||||
|
||||
вместо
|
||||
|
||||
```python
|
||||
list
|
||||
```
|
||||
|
||||
Причина:
|
||||
|
||||
Raw Models являются снимком транспортного ответа.
|
||||
|
||||
Их нельзя изменять после создания.
|
||||
|
||||
---
|
||||
|
||||
## 6. Все Raw Models являются immutable
|
||||
|
||||
Все dataclass объявлены как:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
```
|
||||
|
||||
Это гарантирует неизменяемость транспортного контракта.
|
||||
|
||||
---
|
||||
|
||||
# Созданные файлы
|
||||
|
||||
Создан:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/adapters/dzengi/models.py
|
||||
```
|
||||
|
||||
Создан:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
|
||||
```
|
||||
|
||||
Другие production-файлы не изменялись.
|
||||
|
||||
---
|
||||
|
||||
# Проверки
|
||||
|
||||
Выполнены все проверки Build.
|
||||
|
||||
---
|
||||
|
||||
## Unit Test
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
```
|
||||
6 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Импорт полной транспортной модели
|
||||
|
||||
Проверено:
|
||||
|
||||
- импортируются все классы;
|
||||
- создаётся полная иерархия объектов;
|
||||
- вложенные dataclass работают корректно.
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
## Компиляция
|
||||
|
||||
Проверено:
|
||||
|
||||
```text
|
||||
python -m py_compile
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
## Проверка отсутствия использования в production
|
||||
|
||||
Подтверждено:
|
||||
|
||||
Raw Models используются только:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/models.py
|
||||
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
|
||||
```
|
||||
|
||||
ExchangeService их не использует.
|
||||
|
||||
Parser их ещё не использует.
|
||||
|
||||
Mapper их ещё не использует.
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
## Полный набор тестов
|
||||
|
||||
Выполнено:
|
||||
|
||||
```text
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
11 passed
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
✅ Passed
|
||||
|
||||
---
|
||||
|
||||
# Архитектурный результат
|
||||
|
||||
После Build 002 структура подсистемы стала выглядеть следующим образом:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
│
|
||||
▼
|
||||
Raw Models
|
||||
```
|
||||
|
||||
Следующие слои пока отсутствуют:
|
||||
|
||||
```text
|
||||
Parser
|
||||
Mapper
|
||||
Instrument
|
||||
Handler
|
||||
Feed
|
||||
Service
|
||||
```
|
||||
|
||||
Именно это соответствует утверждённому плану миграции.
|
||||
|
||||
---
|
||||
|
||||
# Обратная совместимость
|
||||
|
||||
Полностью сохранена.
|
||||
|
||||
Не изменены:
|
||||
|
||||
- ExchangeService
|
||||
- ExchangeSymbol
|
||||
- SymbolValidationResult
|
||||
- validate_symbol()
|
||||
- normalize_symbol()
|
||||
- symbol_candidates()
|
||||
- Runtime
|
||||
- Telegram UI
|
||||
|
||||
Новые Raw Models пока не подключены к работающему боту.
|
||||
|
||||
---
|
||||
|
||||
# Итог Build
|
||||
|
||||
Build 002 завершён успешно.
|
||||
|
||||
Получен полноценный транспортный контракт REST API Dzengi.
|
||||
|
||||
Создан фундамент для следующих этапов миграции.
|
||||
|
||||
Работающий бот не изменил своего поведения.
|
||||
|
||||
---
|
||||
|
||||
# Следующий Build
|
||||
|
||||
## Build 003 — Структурная валидация exchangeInfo
|
||||
|
||||
Следующий этап реализует parser, который будет преобразовывать сырой JSON REST API в созданные транспортные модели.
|
||||
|
||||
На Build 003 по-прежнему:
|
||||
|
||||
- ExchangeService изменяться не будет;
|
||||
- предметная модель Instrument использоваться не будет;
|
||||
- parser останется полностью независимым от бизнес-логики.
|
||||
850
docs/migrations/build_003.md
Normal file
850
docs/migrations/build_003.md
Normal file
@@ -0,0 +1,850 @@
|
||||
# Dzentra — Instrument Reference Data Migration
|
||||
|
||||
## Build 003 — структурная валидация `exchangeInfo`
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Область:** Instrument Reference Data
|
||||
**Проект:** Dzentra
|
||||
**Язык реализации:** Python 3.12
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 003
|
||||
|
||||
Цель Build 003 — создать отдельный слой структурной валидации сырого JSON-документа `exchangeInfo` до его преобразования в raw-модели Dzengi.
|
||||
|
||||
После завершения Build 003 формируется следующий архитектурный поток:
|
||||
|
||||
```text
|
||||
JSON response
|
||||
↓
|
||||
Schema validation
|
||||
↓
|
||||
Parser
|
||||
↓
|
||||
Dzengi Raw Models
|
||||
```
|
||||
|
||||
В рамках этого Build реализована только проверка формы и структуры входных данных.
|
||||
|
||||
Schema validation не выполняет:
|
||||
|
||||
- преобразование JSON в raw-модели Dzengi;
|
||||
- преобразование camelCase в snake_case;
|
||||
- преобразование строковых чисел в `Decimal`, `float` или `int`;
|
||||
- нормализацию символов;
|
||||
- проверку допустимости числовых значений;
|
||||
- проверку торговой семантики статусов;
|
||||
- создание внутренней модели `Instrument`;
|
||||
- фильтрацию или отбрасывание инструментов.
|
||||
|
||||
---
|
||||
|
||||
## 2. Почему Build 003 выполняется перед parser
|
||||
|
||||
После Build 002 уже существуют типизированные raw-модели ответа Dzengi.
|
||||
|
||||
Следующий этап — parser, однако parser не должен одновременно:
|
||||
|
||||
- определять wrapped или unwrapped формат ответа;
|
||||
- проверять тип корневого объекта;
|
||||
- проверять наличие `symbols`;
|
||||
- проверять тип `symbols`;
|
||||
- проверять структуру элементов `symbols`;
|
||||
- проверять структуру вложенных коллекций;
|
||||
- создавать raw-модели.
|
||||
|
||||
Поэтому перед parser был выделен отдельный слой:
|
||||
|
||||
```text
|
||||
validation/schema.py
|
||||
```
|
||||
|
||||
Разделение ответственности теперь выглядит следующим образом:
|
||||
|
||||
```text
|
||||
schema.py
|
||||
Проверяет форму и структуру данных.
|
||||
|
||||
parser.py
|
||||
Преобразует структурно корректные данные в raw-модели Dzengi.
|
||||
|
||||
values.py
|
||||
Проверяет допустимость предметных значений.
|
||||
|
||||
mapper.py
|
||||
Преобразует raw-модели Dzengi во внутреннюю модель Instrument.
|
||||
```
|
||||
|
||||
Это является обязательным архитектурным изменением для утверждённой структуры Dzentra.
|
||||
|
||||
---
|
||||
|
||||
## 3. Изменённые и созданные файлы
|
||||
|
||||
В рамках Build 003 были изменены или созданы только следующие файлы:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
app/src/market_data/acquisition/validation/schema.py
|
||||
app/tests/unit/market_data/acquisition/validation/test_schema.py
|
||||
```
|
||||
|
||||
Другие файлы проекта не изменялись.
|
||||
|
||||
---
|
||||
|
||||
## 4. Реализованные исключения
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
```
|
||||
|
||||
добавлены:
|
||||
|
||||
```python
|
||||
class MarketDataAcquisitionError(Exception):
|
||||
pass
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
class InstrumentReferenceSchemaError(MarketDataAcquisitionError):
|
||||
pass
|
||||
```
|
||||
|
||||
Иерархия ошибок:
|
||||
|
||||
```text
|
||||
Exception
|
||||
↓
|
||||
MarketDataAcquisitionError
|
||||
↓
|
||||
InstrumentReferenceSchemaError
|
||||
```
|
||||
|
||||
`InstrumentReferenceSchemaError` используется для явного обозначения ошибки структуры документа Instrument Reference Data.
|
||||
|
||||
Это позволяет в последующих Build отличать структурную ошибку от:
|
||||
|
||||
- сетевой ошибки;
|
||||
- ошибки HTTP;
|
||||
- ошибки JSON-декодирования;
|
||||
- ошибки значений;
|
||||
- ошибки parser;
|
||||
- ошибки mapper.
|
||||
|
||||
Исключение создано не «на будущее»: оно непосредственно используется schema validation в Build 003.
|
||||
|
||||
---
|
||||
|
||||
## 5. Реализованный контракт schema validation
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/validation/schema.py
|
||||
```
|
||||
|
||||
создана модель:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ValidatedExchangeInfoDocument:
|
||||
payload: Mapping[str, object]
|
||||
is_wrapped: bool
|
||||
status: object | None
|
||||
correlation_id: object | None
|
||||
```
|
||||
|
||||
Она представляет структурно проверенный документ `exchangeInfo`.
|
||||
|
||||
Модель содержит:
|
||||
|
||||
| Поле | Назначение |
|
||||
|---|---|
|
||||
| `payload` | Проверенный payload с данными `exchangeInfo` |
|
||||
| `is_wrapped` | Признак wrapped/unwrapped формата |
|
||||
| `status` | Верхнеуровневый статус wrapped-ответа |
|
||||
| `correlation_id` | Верхнеуровневый `correlationId` wrapped-ответа |
|
||||
|
||||
Основная функция:
|
||||
|
||||
```python
|
||||
validate_exchange_info_schema(document: object) -> ValidatedExchangeInfoDocument
|
||||
```
|
||||
|
||||
принимает произвольный объект и либо:
|
||||
|
||||
- возвращает `ValidatedExchangeInfoDocument`;
|
||||
- либо выбрасывает `InstrumentReferenceSchemaError`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Поддерживаемые форматы ответа
|
||||
|
||||
### 6.1. Unwrapped-формат
|
||||
|
||||
Поддерживается непосредственный payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"timezone": "UTC",
|
||||
"serverTime": 1783537921471,
|
||||
"symbols": []
|
||||
}
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
is_wrapped = False
|
||||
status = None
|
||||
correlation_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.2. Wrapped-формат
|
||||
|
||||
Поддерживается ответ с вложенным `payload`:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"correlationId": "2",
|
||||
"payload": {
|
||||
"timezone": "UTC",
|
||||
"serverTime": 1783537921471,
|
||||
"symbols": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
is_wrapped = True
|
||||
status = "OK"
|
||||
correlation_id = "2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Что проверяет schema validation
|
||||
|
||||
Build 003 проверяет следующие структурные свойства документа.
|
||||
|
||||
### 7.1. Корень документа
|
||||
|
||||
Корень должен быть JSON-объектом.
|
||||
|
||||
Отклоняются:
|
||||
|
||||
```json
|
||||
[]
|
||||
```
|
||||
|
||||
```json
|
||||
null
|
||||
```
|
||||
|
||||
```json
|
||||
"invalid"
|
||||
```
|
||||
|
||||
```json
|
||||
123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.2. Wrapped payload
|
||||
|
||||
Если присутствует ключ:
|
||||
|
||||
```text
|
||||
payload
|
||||
```
|
||||
|
||||
его значение должно быть JSON-объектом.
|
||||
|
||||
Например, отклоняется:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"payload": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.3. Наличие `symbols`
|
||||
|
||||
Payload должен содержать:
|
||||
|
||||
```text
|
||||
symbols
|
||||
```
|
||||
|
||||
Отсутствие `symbols` считается ошибкой структуры.
|
||||
|
||||
---
|
||||
|
||||
### 7.4. Тип `symbols`
|
||||
|
||||
`symbols` должен быть JSON-массивом.
|
||||
|
||||
Отклоняется:
|
||||
|
||||
```json
|
||||
{
|
||||
"symbols": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.5. Элементы `symbols`
|
||||
|
||||
Каждый элемент `symbols` должен быть JSON-объектом.
|
||||
|
||||
Отклоняется:
|
||||
|
||||
```json
|
||||
{
|
||||
"symbols": [
|
||||
"BTC/USD"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.6. `filters`
|
||||
|
||||
Если у инструмента присутствует:
|
||||
|
||||
```text
|
||||
filters
|
||||
```
|
||||
|
||||
то:
|
||||
|
||||
- `filters` должен быть JSON-массивом;
|
||||
- каждый элемент `filters` должен быть JSON-объектом.
|
||||
|
||||
Отклоняется:
|
||||
|
||||
```json
|
||||
{
|
||||
"symbols": [
|
||||
{
|
||||
"symbol": "BTC/USD",
|
||||
"filters": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Также отклоняется:
|
||||
|
||||
```json
|
||||
{
|
||||
"symbols": [
|
||||
{
|
||||
"symbol": "BTC/USD",
|
||||
"filters": [
|
||||
"LOT_SIZE"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.7. `marketModes`
|
||||
|
||||
Если присутствует:
|
||||
|
||||
```text
|
||||
marketModes
|
||||
```
|
||||
|
||||
то:
|
||||
|
||||
- значение должно быть JSON-массивом;
|
||||
- каждый элемент должен быть строкой.
|
||||
|
||||
---
|
||||
|
||||
### 7.8. `orderTypes`
|
||||
|
||||
Если присутствует:
|
||||
|
||||
```text
|
||||
orderTypes
|
||||
```
|
||||
|
||||
то:
|
||||
|
||||
- значение должно быть JSON-массивом;
|
||||
- каждый элемент должен быть строкой.
|
||||
|
||||
---
|
||||
|
||||
### 7.9. `rateLimits`
|
||||
|
||||
Если присутствует:
|
||||
|
||||
```text
|
||||
rateLimits
|
||||
```
|
||||
|
||||
то:
|
||||
|
||||
- значение должно быть JSON-массивом;
|
||||
- каждый элемент должен быть JSON-объектом.
|
||||
|
||||
---
|
||||
|
||||
### 7.10. `exchangeFilters`
|
||||
|
||||
Если присутствует:
|
||||
|
||||
```text
|
||||
exchangeFilters
|
||||
```
|
||||
|
||||
то:
|
||||
|
||||
- значение должно быть JSON-массивом;
|
||||
- каждый элемент должен быть JSON-объектом.
|
||||
|
||||
---
|
||||
|
||||
## 8. Что намеренно не проверяется в Build 003
|
||||
|
||||
Build 003 не проверяет допустимость значений.
|
||||
|
||||
Следующие примеры не относятся к schema validation:
|
||||
|
||||
```json
|
||||
{
|
||||
"tickSize": -1
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"baseAssetPrecision": -5
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": ""
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"minQty": "not-a-number"
|
||||
}
|
||||
```
|
||||
|
||||
Такие проверки относятся к:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/validation/values.py
|
||||
```
|
||||
|
||||
и должны реализовываться отдельно, без смешения структурной и предметной валидации.
|
||||
|
||||
---
|
||||
|
||||
## 9. Защита проверенного payload
|
||||
|
||||
Поле:
|
||||
|
||||
```python
|
||||
payload: Mapping[str, object]
|
||||
```
|
||||
|
||||
возвращается через:
|
||||
|
||||
```python
|
||||
MappingProxyType
|
||||
```
|
||||
|
||||
Это предотвращает случайное изменение корневого payload последующим parser.
|
||||
|
||||
Таким образом, schema validation передаёт следующему слою проверенное read-only представление данных.
|
||||
|
||||
---
|
||||
|
||||
## 10. Диагностические пути ошибок
|
||||
|
||||
Schema validation формирует ошибки с указанием пути до проблемного значения.
|
||||
|
||||
Примеры:
|
||||
|
||||
```text
|
||||
$ должен быть JSON-объектом, получен list.
|
||||
```
|
||||
|
||||
```text
|
||||
$.payload.symbols должен быть JSON-массивом, получен dict.
|
||||
```
|
||||
|
||||
```text
|
||||
$.payload.symbols[0] должен быть JSON-объектом, получен str.
|
||||
```
|
||||
|
||||
```text
|
||||
$.payload.symbols[0].filters должен быть JSON-массивом, получен dict.
|
||||
```
|
||||
|
||||
На текущем этапе для `symbols` используется унифицированный диагностический путь:
|
||||
|
||||
```text
|
||||
$.payload.symbols
|
||||
```
|
||||
|
||||
как для wrapped-, так и для unwrapped-формата.
|
||||
|
||||
Это сознательное упрощение текущего контракта и не влияет на production-поведение.
|
||||
|
||||
---
|
||||
|
||||
## 11. Реализованные тесты
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/validation/test_schema.py
|
||||
```
|
||||
|
||||
Он проверяет:
|
||||
|
||||
- корректный unwrapped-документ;
|
||||
- корректный wrapped-документ;
|
||||
- отклонение не-объекта в корне;
|
||||
- отклонение некорректного wrapped payload;
|
||||
- отсутствие `symbols`;
|
||||
- некорректный тип `symbols`;
|
||||
- некорректный элемент `symbols`;
|
||||
- некорректный тип `filters`;
|
||||
- некорректный элемент `filters`;
|
||||
- некорректный тип `marketModes`;
|
||||
- некорректный тип `orderTypes`;
|
||||
- нестроковый элемент `marketModes`;
|
||||
- нестроковый элемент `orderTypes`;
|
||||
- некорректный тип `rateLimits`;
|
||||
- некорректный элемент `rateLimits`;
|
||||
- некорректный тип `exchangeFilters`;
|
||||
- некорректный элемент `exchangeFilters`.
|
||||
|
||||
Итог:
|
||||
|
||||
```text
|
||||
20 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Выполненные проверки
|
||||
|
||||
### Проверка 1 — unit-тесты Build 003
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/validation/test_schema.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
.................... [100%]
|
||||
20 passed in 0.01s
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 2 — компиляция файлов Build 003
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/exceptions.py \
|
||||
src/market_data/acquisition/validation/schema.py \
|
||||
tests/unit/market_data/acquisition/validation/test_schema.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Ошибок нет.
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 3 — ручная проверка wrapped/unwrapped контрактов
|
||||
|
||||
Получен результат:
|
||||
|
||||
```text
|
||||
Unwrapped:
|
||||
is_wrapped: False
|
||||
status: None
|
||||
correlation_id: None
|
||||
symbols: []
|
||||
|
||||
Wrapped:
|
||||
is_wrapped: True
|
||||
status: OK
|
||||
correlation_id: 2
|
||||
symbols: []
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- unwrapped-формат определяется корректно;
|
||||
- wrapped-формат определяется корректно;
|
||||
- `status` сохраняется;
|
||||
- `correlationId` сохраняется как `correlation_id`;
|
||||
- `symbols` доступны через проверенный payload.
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 4 — типизированные ошибки
|
||||
|
||||
Проверены четыре некорректных документа.
|
||||
|
||||
Получен результат:
|
||||
|
||||
```text
|
||||
1: InstrumentReferenceSchemaError: $ должен быть JSON-объектом, получен list.
|
||||
2: InstrumentReferenceSchemaError: $.payload.symbols должен быть JSON-массивом, получен dict.
|
||||
3: InstrumentReferenceSchemaError: $.payload.symbols[0] должен быть JSON-объектом, получен str.
|
||||
4: InstrumentReferenceSchemaError: $.payload.symbols[0].filters должен быть JSON-массивом, получен dict.
|
||||
```
|
||||
|
||||
Ни один ошибочный документ не был принят.
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 5 — полный набор тестов проекта
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
............................... [100%]
|
||||
31 passed in 0.04s
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 6 — отсутствие подключения к production-коду
|
||||
|
||||
Выполнен поиск:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "validate_exchange_info_schema|ValidatedExchangeInfoDocument|InstrumentReferenceSchemaError" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Подтверждено, что новые сущности Build 003 используются только в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/exceptions.py
|
||||
src/market_data/acquisition/validation/schema.py
|
||||
tests/unit/market_data/acquisition/validation/test_schema.py
|
||||
```
|
||||
|
||||
Они пока не подключены к:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
legacy exchangeInfo
|
||||
Telegram UI
|
||||
runtime
|
||||
автоторговле
|
||||
market stream
|
||||
production parser
|
||||
production mapper
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Обратная совместимость
|
||||
|
||||
Build 003 не изменяет:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
ExchangeService.validate_symbol()
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
ExchangeService.get_symbol_market_status()
|
||||
```
|
||||
|
||||
Не изменены:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
SymbolValidationResult
|
||||
normalize_symbol()
|
||||
symbol_candidates()
|
||||
```
|
||||
|
||||
Не изменено поведение:
|
||||
|
||||
- Telegram UI;
|
||||
- автоторговли;
|
||||
- runtime-проверок символа;
|
||||
- legacy-кэша инструментов;
|
||||
- получения цен;
|
||||
- market stream;
|
||||
- существующего `exchangeInfo`.
|
||||
|
||||
Новый schema validation пока изолирован от production-кода.
|
||||
|
||||
---
|
||||
|
||||
## 14. Точки обратной совместимости
|
||||
|
||||
После Build 003 продолжают действовать прежние точки совместимости:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
→ list[ExchangeSymbol]
|
||||
|
||||
ExchangeService.validate_symbol()
|
||||
→ SymbolValidationResult
|
||||
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
→ ExchangeRuntimeStatus
|
||||
|
||||
ExchangeService.get_symbol_market_status()
|
||||
→ dict[str, object]
|
||||
```
|
||||
|
||||
Ни одна из этих сигнатур не изменена.
|
||||
|
||||
---
|
||||
|
||||
## 15. Классификация изменений
|
||||
|
||||
| Изменение | Классификация |
|
||||
|---|---|
|
||||
| Создание schema validation | Обязательное архитектурное изменение |
|
||||
| Создание `MarketDataAcquisitionError` | Обязательное архитектурное изменение |
|
||||
| Создание `InstrumentReferenceSchemaError` | Обязательное архитектурное изменение |
|
||||
| Поддержка wrapped/unwrapped форматов | Улучшение надёжности |
|
||||
| Явные структурные ошибки | Улучшение надёжности |
|
||||
| Read-only представление проверенного payload | Улучшение надёжности |
|
||||
| Изменение production-поведения | Отсутствует |
|
||||
| Изменение публичных legacy-интерфейсов | Отсутствует |
|
||||
|
||||
---
|
||||
|
||||
## 16. Условие завершения Build 003
|
||||
|
||||
Build 003 считается завершённым, потому что выполнены все условия:
|
||||
|
||||
- создан отдельный schema validation layer;
|
||||
- поддержан wrapped-формат;
|
||||
- поддержан unwrapped-формат;
|
||||
- проверяется обязательная структура `symbols`;
|
||||
- проверяются вложенные коллекции;
|
||||
- ошибки типизированы;
|
||||
- создан read-only контракт для передачи данных parser;
|
||||
- написаны unit-тесты;
|
||||
- все тесты Build проходят;
|
||||
- полный набор тестов проекта проходит;
|
||||
- production-код не изменён;
|
||||
- обратная совместимость сохранена.
|
||||
|
||||
---
|
||||
|
||||
## 17. Итоговый статус
|
||||
|
||||
```text
|
||||
Build 003 — COMPLETED
|
||||
```
|
||||
|
||||
Итоговый набор тестов проекта:
|
||||
|
||||
```text
|
||||
31 passed in 0.04s
|
||||
```
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 004 — Parser exchangeInfo
|
||||
```
|
||||
|
||||
На Build 004 новый parser должен:
|
||||
|
||||
1. принимать структурно проверенный `ValidatedExchangeInfoDocument`;
|
||||
2. преобразовывать payload в raw-модели Dzengi, созданные в Build 002;
|
||||
3. не выполнять повторную schema validation;
|
||||
4. не создавать внутреннюю модель `Instrument`;
|
||||
5. не подключаться к `ExchangeService`;
|
||||
6. не менять production-поведение работающего бота.
|
||||
775
docs/migrations/build_004.md
Normal file
775
docs/migrations/build_004.md
Normal file
@@ -0,0 +1,775 @@
|
||||
# Dzentra — Instrument Reference Data Migration
|
||||
|
||||
## Build 004 — Dzengi exchangeInfo Parser
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Компонент:** Dzengi Adapter / exchangeInfo Parser
|
||||
**Проект:** Dzentra
|
||||
**Язык:** Русский
|
||||
**Python:** 3.12
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 004
|
||||
|
||||
Цель Build 004 — реализовать parser для ответа Dzengi `exchangeInfo`, который преобразует уже структурно проверенный документ из Build 003 в типизированные транспортные модели Dzengi, созданные в Build 002.
|
||||
|
||||
Целевая цепочка после завершения Build 004:
|
||||
|
||||
```text
|
||||
Raw JSON document
|
||||
↓
|
||||
validate_exchange_info_schema()
|
||||
↓
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
parse_exchange_info()
|
||||
↓
|
||||
DzengiExchangeInfoResponse
|
||||
```
|
||||
|
||||
Build 004 не подключает новую реализацию к существующему `ExchangeService` и не изменяет поведение работающего бота.
|
||||
|
||||
---
|
||||
|
||||
## 2. Почему Build 004 выполняется именно сейчас
|
||||
|
||||
До начала Build 004 уже были завершены необходимые предыдущие этапы.
|
||||
|
||||
### Build 001 — внутренняя модель Instrument
|
||||
|
||||
Создана внутренняя типизированная модель:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
```
|
||||
|
||||
Она представляет инструмент внутри новой архитектуры Dzentra и не зависит от формата конкретной биржи.
|
||||
|
||||
### Build 002 — транспортные модели Dzengi
|
||||
|
||||
Созданы типизированные модели сырого ответа `exchangeInfo`:
|
||||
|
||||
```text
|
||||
DzengiExchangeInfoResponse
|
||||
DzengiExchangeInfoPayload
|
||||
DzengiExchangeInfoSymbol
|
||||
DzengiRateLimit
|
||||
DzengiInstrumentFilter
|
||||
DzengiLotSizeFilter
|
||||
DzengiMinNotionalFilter
|
||||
DzengiUnknownFilter
|
||||
```
|
||||
|
||||
### Build 003 — структурная валидация exchangeInfo
|
||||
|
||||
Создан слой проверки структуры сырого JSON-документа:
|
||||
|
||||
```text
|
||||
validate_exchange_info_schema()
|
||||
```
|
||||
|
||||
Его результат:
|
||||
|
||||
```text
|
||||
ValidatedExchangeInfoDocument
|
||||
```
|
||||
|
||||
Таким образом, только после Build 001–003 стало безопасно реализовать parser, не смешивая:
|
||||
|
||||
- проверку структуры JSON;
|
||||
- разбор транспортного ответа;
|
||||
- предметное преобразование в `Instrument`;
|
||||
- сетевой REST-доступ;
|
||||
- кэширование;
|
||||
- legacy-совместимость.
|
||||
|
||||
---
|
||||
|
||||
## 3. Реализованная архитектурная цепочка
|
||||
|
||||
На текущем этапе действует следующая архитектура:
|
||||
|
||||
```text
|
||||
Raw Dzengi JSON
|
||||
↓
|
||||
Schema Validation
|
||||
↓
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
Dzengi Parser
|
||||
↓
|
||||
DzengiExchangeInfoResponse
|
||||
```
|
||||
|
||||
Полная целевая цепочка миграции пока ещё не завершена:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
↓
|
||||
Dzengi REST Adapter
|
||||
↓
|
||||
Schema Validation
|
||||
↓
|
||||
Parser
|
||||
↓
|
||||
Dzengi Raw Models
|
||||
↓
|
||||
Mapper
|
||||
↓
|
||||
Instrument
|
||||
↓
|
||||
Instrument Handler
|
||||
↓
|
||||
Instrument Feed
|
||||
↓
|
||||
Market Data Acquisition Service
|
||||
↓
|
||||
Compatibility Layer
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
Existing Consumers
|
||||
```
|
||||
|
||||
Build 004 реализует только участок:
|
||||
|
||||
```text
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
Parser
|
||||
↓
|
||||
Dzengi Raw Models
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Файлы Build 004
|
||||
|
||||
### Реализован
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/adapters/dzengi/parser.py
|
||||
```
|
||||
|
||||
### Использованы существующие файлы
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/adapters/dzengi/models.py
|
||||
app/src/market_data/acquisition/validation/schema.py
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
```
|
||||
|
||||
### Добавлены тесты
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Публичный контракт parser
|
||||
|
||||
Основной публичный вход Build 004:
|
||||
|
||||
```python
|
||||
def parse_exchange_info(
|
||||
document: ValidatedExchangeInfoDocument,
|
||||
) -> DzengiExchangeInfoResponse:
|
||||
...
|
||||
```
|
||||
|
||||
Parser намеренно не принимает произвольный сырой `dict`.
|
||||
|
||||
Корректная последовательность вызовов:
|
||||
|
||||
```python
|
||||
validated = validate_exchange_info_schema(raw_document)
|
||||
response = parse_exchange_info(validated)
|
||||
```
|
||||
|
||||
Это обеспечивает явное разделение ответственности между Build 003 и Build 004.
|
||||
|
||||
---
|
||||
|
||||
## 6. Разделение ответственности
|
||||
|
||||
### Build 003 — Schema Validation
|
||||
|
||||
Отвечает за структурную корректность документа:
|
||||
|
||||
- корневой объект должен быть JSON-объектом;
|
||||
- `payload`, если используется wrapped-формат, должен быть объектом;
|
||||
- `symbols` должен быть массивом;
|
||||
- каждый элемент `symbols` должен быть объектом;
|
||||
- `filters` должен быть массивом;
|
||||
- другие структурные ограничения проверяются до parser.
|
||||
|
||||
Build 003 не создаёт транспортные модели Dzengi.
|
||||
|
||||
### Build 004 — Parser
|
||||
|
||||
Отвечает за преобразование структурно проверенного документа в:
|
||||
|
||||
```text
|
||||
DzengiExchangeInfoResponse
|
||||
```
|
||||
|
||||
и вложенные типизированные транспортные модели.
|
||||
|
||||
Parser не отвечает за:
|
||||
|
||||
- HTTP-запросы;
|
||||
- кэширование;
|
||||
- предметную модель `Instrument`;
|
||||
- нормализацию символа для бизнес-логики;
|
||||
- runtime-статус инструмента;
|
||||
- UI;
|
||||
- legacy-совместимость.
|
||||
|
||||
---
|
||||
|
||||
## 7. Поддерживаемые форматы exchangeInfo
|
||||
|
||||
Архитектура поддерживает два формата ответа.
|
||||
|
||||
### Unwrapped
|
||||
|
||||
```json
|
||||
{
|
||||
"timezone": "UTC",
|
||||
"serverTime": 1783537921471,
|
||||
"rateLimits": [],
|
||||
"exchangeFilters": [],
|
||||
"symbols": []
|
||||
}
|
||||
```
|
||||
|
||||
После Build 003:
|
||||
|
||||
```text
|
||||
is_wrapped: False
|
||||
status: None
|
||||
correlation_id: None
|
||||
```
|
||||
|
||||
После Build 004 документ преобразуется в:
|
||||
|
||||
```text
|
||||
DzengiExchangeInfoResponse
|
||||
└── payload: DzengiExchangeInfoPayload
|
||||
```
|
||||
|
||||
### Wrapped
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"correlationId": "2",
|
||||
"payload": {
|
||||
"timezone": "UTC",
|
||||
"serverTime": 1783537921471,
|
||||
"rateLimits": [],
|
||||
"exchangeFilters": [],
|
||||
"symbols": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
После Build 003:
|
||||
|
||||
```text
|
||||
is_wrapped: True
|
||||
status: OK
|
||||
correlation_id: 2
|
||||
```
|
||||
|
||||
После Build 004 метаданные оболочки сохраняются в:
|
||||
|
||||
```text
|
||||
DzengiExchangeInfoResponse.status
|
||||
DzengiExchangeInfoResponse.correlation_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Разбор инструмента
|
||||
|
||||
Каждый элемент массива `symbols` преобразуется в:
|
||||
|
||||
```text
|
||||
DzengiExchangeInfoSymbol
|
||||
```
|
||||
|
||||
Поддерживаются следующие поля:
|
||||
|
||||
```text
|
||||
symbol
|
||||
name
|
||||
status
|
||||
|
||||
asset_type
|
||||
|
||||
base_asset
|
||||
base_asset_precision
|
||||
|
||||
quote_asset
|
||||
quote_asset_id
|
||||
quote_precision
|
||||
|
||||
order_types
|
||||
filters
|
||||
|
||||
market_modes
|
||||
market_type
|
||||
|
||||
country
|
||||
sector
|
||||
industry
|
||||
trading_hours
|
||||
|
||||
tick_size
|
||||
tick_value
|
||||
|
||||
trading_fee
|
||||
exchange_fee
|
||||
|
||||
long_rate
|
||||
short_rate
|
||||
swap_charge_interval
|
||||
|
||||
min_sl_gap
|
||||
max_sl_gap
|
||||
min_tp_gap
|
||||
max_tp_gap
|
||||
```
|
||||
|
||||
На этом этапе поля сохраняют транспортную семантику Dzengi и ещё не преобразуются в предметную модель `Instrument`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Разбор filters
|
||||
|
||||
Parser преобразует известные типы фильтров в отдельные типизированные модели.
|
||||
|
||||
### LOT_SIZE
|
||||
|
||||
Преобразуется в:
|
||||
|
||||
```text
|
||||
DzengiLotSizeFilter
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
```text
|
||||
filter_type
|
||||
min_qty
|
||||
max_qty
|
||||
step_size
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
DzengiLotSizeFilter(
|
||||
filter_type='LOT_SIZE',
|
||||
min_qty='0.0001',
|
||||
max_qty='1000',
|
||||
step_size='0.0001',
|
||||
)
|
||||
```
|
||||
|
||||
### MIN_NOTIONAL
|
||||
|
||||
Преобразуется в:
|
||||
|
||||
```text
|
||||
DzengiMinNotionalFilter
|
||||
```
|
||||
|
||||
Поле:
|
||||
|
||||
```text
|
||||
min_notional
|
||||
```
|
||||
|
||||
### Неизвестные фильтры
|
||||
|
||||
Неизвестный `filterType` не отбрасывается автоматически.
|
||||
|
||||
Он преобразуется в:
|
||||
|
||||
```text
|
||||
DzengiUnknownFilter
|
||||
```
|
||||
|
||||
Это позволяет сохранить неизвестные скалярные поля транспортного ответа без добавления неподтверждённой предметной семантики.
|
||||
|
||||
---
|
||||
|
||||
## 10. Числовые значения
|
||||
|
||||
Build 004 сохраняет важное разделение между транспортным и предметным слоями.
|
||||
|
||||
Например, значения фильтра:
|
||||
|
||||
```json
|
||||
{
|
||||
"minQty": "0.0001",
|
||||
"maxQty": "1000",
|
||||
"stepSize": "0.0001"
|
||||
}
|
||||
```
|
||||
|
||||
в транспортной модели остаются:
|
||||
|
||||
```text
|
||||
min_qty='0.0001'
|
||||
max_qty='1000'
|
||||
step_size='0.0001'
|
||||
```
|
||||
|
||||
Parser не выполняет преждевременное преобразование этих значений в `float`.
|
||||
|
||||
Преобразование в точный предметный числовой тип должно выполняться на следующем архитектурном этапе при построении `Instrument`.
|
||||
|
||||
Это позволяет избежать потери точности и сохраняет исходную семантику ответа Dzengi.
|
||||
|
||||
---
|
||||
|
||||
## 11. Обработка ошибок
|
||||
|
||||
Для ошибок parser используется отдельное исключение:
|
||||
|
||||
```text
|
||||
InstrumentReferenceParseError
|
||||
```
|
||||
|
||||
Оно объявлено в:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
```
|
||||
|
||||
Иерархия:
|
||||
|
||||
```text
|
||||
MarketDataAcquisitionError
|
||||
└── InstrumentReferenceParseError
|
||||
```
|
||||
|
||||
`InstrumentReferenceParseError` используется только:
|
||||
|
||||
- в `parser.py`;
|
||||
- в unit-тестах parser.
|
||||
|
||||
На момент завершения Build 004 это исключение не используется:
|
||||
|
||||
- `ExchangeService`;
|
||||
- Telegram UI;
|
||||
- runtime-кодом;
|
||||
- автоторговлей;
|
||||
- существующими production-потребителями.
|
||||
|
||||
---
|
||||
|
||||
## 12. Классификация изменений
|
||||
|
||||
| Изменение | Классификация | Влияние на поведение |
|
||||
|---|---|---|
|
||||
| Реализация `exchangeInfo` parser | Обязательное архитектурное изменение | Нет |
|
||||
| Типизированное преобразование symbols | Обязательное архитектурное изменение | Нет |
|
||||
| Типизированный разбор известных filters | Обязательное архитектурное изменение | Нет |
|
||||
| Сохранение неизвестных filters | Улучшение надёжности | Нет |
|
||||
| Отдельное `InstrumentReferenceParseError` | Улучшение надёжности | Нет |
|
||||
| Сохранение числовых строк без преобразования в `float` | Улучшение надёжности | Нет |
|
||||
| Поддержка wrapped и unwrapped форматов | Улучшение надёжности | Нет |
|
||||
|
||||
Изменений поведения работающего бота в Build 004 нет.
|
||||
|
||||
---
|
||||
|
||||
## 13. Обратная совместимость
|
||||
|
||||
Build 004 не изменяет существующие публичные интерфейсы.
|
||||
|
||||
Без изменений продолжают работать:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
ExchangeService.validate_symbol()
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
ExchangeService.get_symbol_market_status()
|
||||
```
|
||||
|
||||
Также без изменений остаются:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
SymbolValidationResult
|
||||
normalize_symbol()
|
||||
symbol_candidates()
|
||||
```
|
||||
|
||||
Новая реализация parser пока не подключена к существующему `ExchangeService`.
|
||||
|
||||
Следовательно:
|
||||
|
||||
- Telegram UI не изменён;
|
||||
- автоторговля не изменена;
|
||||
- runtime-проверки символа не изменены;
|
||||
- существующий кэш инструментов не изменён;
|
||||
- старые импорты продолжают работать;
|
||||
- production-поведение бота сохранено.
|
||||
|
||||
---
|
||||
|
||||
## 14. Выполненные проверки
|
||||
|
||||
### Проверка 1 — unit-тесты parser
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
19 passed in 0.02s
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 2 — синтаксическая компиляция
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/exceptions.py \
|
||||
src/market_data/acquisition/adapters/dzengi/parser.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Команда завершена без ошибок.
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 3 — ручная цепочка schema → parser
|
||||
|
||||
Была проверена цепочка:
|
||||
|
||||
```text
|
||||
raw dict
|
||||
↓
|
||||
validate_exchange_info_schema()
|
||||
↓
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
parse_exchange_info()
|
||||
↓
|
||||
DzengiExchangeInfoResponse
|
||||
```
|
||||
|
||||
Полученный результат:
|
||||
|
||||
```text
|
||||
Symbol: BTC/USD_LEVERAGE
|
||||
Status: TRADING
|
||||
Asset type: CRYPTOCURRENCY
|
||||
Base asset: BTC
|
||||
Quote asset: USD
|
||||
Tick size: 0.05
|
||||
Filters: (DzengiLotSizeFilter(filter_type='LOT_SIZE', min_qty='0.0001', max_qty='1000', step_size='0.0001'),)
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 4 — полный набор тестов проекта
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
50 passed in 0.05s
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 5 — контроль зависимостей parser
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "parse_exchange_info|_parse_exchange_info|DzengiExchangeInfoParseError" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- `parse_exchange_info()` определён в новом `parser.py`;
|
||||
- используется только unit-тестами нового parser;
|
||||
- не используется существующим `ExchangeService`;
|
||||
- не используется UI;
|
||||
- не используется runtime;
|
||||
- не используется автоторговлей.
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 6 — контроль использования parse-ошибки
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "InstrumentReferenceParseError" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- исключение объявлено в `exceptions.py`;
|
||||
- используется parser;
|
||||
- используется unit-тестами parser;
|
||||
- не подключено к production-потребителям.
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Итог Build 004
|
||||
|
||||
Build 004 успешно завершён.
|
||||
|
||||
Реализован типизированный parser:
|
||||
|
||||
```text
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
parse_exchange_info()
|
||||
↓
|
||||
DzengiExchangeInfoResponse
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
```text
|
||||
Build 001 — Instrument domain model PASS
|
||||
Build 002 — Dzengi raw transport models PASS
|
||||
Build 003 — exchangeInfo schema validation PASS
|
||||
Build 004 — Dzengi exchangeInfo parser PASS
|
||||
```
|
||||
|
||||
Общий результат тестов после завершения Build 004:
|
||||
|
||||
```text
|
||||
50 passed in 0.05s
|
||||
```
|
||||
|
||||
Работающий бот не затронут.
|
||||
|
||||
---
|
||||
|
||||
## 16. Условие завершения Build 004
|
||||
|
||||
Build 004 считается завершённым, поскольку выполнены все условия:
|
||||
|
||||
- parser реализован;
|
||||
- parser принимает только `ValidatedExchangeInfoDocument`;
|
||||
- parser возвращает `DzengiExchangeInfoResponse`;
|
||||
- основные поля инструмента сохраняются;
|
||||
- известные filters типизированы;
|
||||
- неизвестные filters сохраняются;
|
||||
- ошибки parser имеют отдельный тип;
|
||||
- unit-тесты проходят;
|
||||
- полный набор тестов проходит;
|
||||
- production-потребители не изменены;
|
||||
- обратная совместимость сохранена.
|
||||
|
||||
---
|
||||
|
||||
## 17. Следующий этап
|
||||
|
||||
Следующий этап миграции:
|
||||
|
||||
```text
|
||||
Build 005 — Проверка значений Instrument Reference Data
|
||||
```
|
||||
|
||||
Его задача — преобразовать:
|
||||
|
||||
```text
|
||||
DzengiExchangeInfoSymbol
|
||||
↓
|
||||
Mapper
|
||||
↓
|
||||
Instrument
|
||||
```
|
||||
|
||||
При этом Build 005 должен:
|
||||
|
||||
- использовать модель `Instrument`, созданную в Build 001;
|
||||
- использовать транспортную модель `DzengiExchangeInfoSymbol` из Build 002;
|
||||
- не выполнять HTTP-запросы;
|
||||
- не заниматься кэшированием;
|
||||
- не подключаться к `ExchangeService`;
|
||||
- не менять production-поведение бота;
|
||||
- явно определить правила преобразования числовых значений в `Decimal`;
|
||||
- явно определить соответствие полей Dzengi полям внутренней модели `Instrument`;
|
||||
- отдельно классифицировать любые потенциальные изменения поведения.
|
||||
|
||||
До написания кода Build 005 необходимо сначала проанализировать существующие модели и контракты, относящиеся к преобразованию `DzengiExchangeInfoSymbol` в `Instrument`.
|
||||
1067
docs/migrations/build_005.md
Normal file
1067
docs/migrations/build_005.md
Normal file
File diff suppressed because it is too large
Load Diff
1071
docs/migrations/build_006.md
Normal file
1071
docs/migrations/build_006.md
Normal file
File diff suppressed because it is too large
Load Diff
999
docs/migrations/build_007.md
Normal file
999
docs/migrations/build_007.md
Normal file
@@ -0,0 +1,999 @@
|
||||
# Build 007 — Protocol и Exceptions
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** `market_data/acquisition`
|
||||
**Область:** Instrument Reference Data
|
||||
**Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime
|
||||
**Результат полного набора тестов:** `113 passed`
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 007
|
||||
|
||||
Цель Build 007 — зафиксировать минимальные публичные контракты взаимодействия между следующими слоями новой подсистемы Instrument Reference Data:
|
||||
|
||||
```text
|
||||
Build 008 — Dzengi REST Adapter
|
||||
Build 009 — Instrument Handler
|
||||
Build 010 — Instrument Feed
|
||||
Build 011 — Registry
|
||||
Build 012 — Acquisition Service
|
||||
```
|
||||
|
||||
Также добавлена специализированная ошибка транспортного уровня для будущего REST adapter.
|
||||
|
||||
После завершения Build 007 архитектурная последовательность выглядит следующим образом:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
↓
|
||||
InstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentFeedProtocol
|
||||
↓
|
||||
Registry
|
||||
↓
|
||||
Acquisition Service
|
||||
```
|
||||
|
||||
Build 007 не реализует получение или обработку данных и не подключает новую подсистему к существующему `ExchangeService`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Почему Build 007 выполняется именно сейчас
|
||||
|
||||
До начала Build 007 были завершены предыдущие этапы:
|
||||
|
||||
```text
|
||||
Build 001 — внутренняя модель Instrument Reference Data
|
||||
Build 002 — raw-модели ответа Dzengi
|
||||
Build 003 — структурная валидация exchangeInfo
|
||||
Build 004 — parser exchangeInfo
|
||||
Build 005 — value validation
|
||||
Build 006 — mapper Dzengi → Instrument
|
||||
```
|
||||
|
||||
К началу Build 007 уже существовал полный pipeline преобразования заранее полученного JSON-документа:
|
||||
|
||||
```text
|
||||
raw JSON document
|
||||
↓
|
||||
Schema Validation
|
||||
↓
|
||||
Parser
|
||||
↓
|
||||
Dzengi Raw Models
|
||||
↓
|
||||
Value Validation
|
||||
↓
|
||||
Mapper
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Следующие Build должны добавить транспортный источник, handler, feed, registry и acquisition service.
|
||||
|
||||
Перед их реализацией необходимо было определить минимальные интерфейсы взаимодействия между этими слоями и добавить специализированную ошибку транспортного уровня.
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектурная граница Build 007
|
||||
|
||||
Build 007 отвечает только за:
|
||||
|
||||
```text
|
||||
определение контракта источника сырого документа;
|
||||
определение контракта обработчика сырого документа;
|
||||
определение контракта источника готовых Instrument;
|
||||
добавление ошибки транспортного уровня.
|
||||
```
|
||||
|
||||
Build 007 не выполняет:
|
||||
|
||||
```text
|
||||
HTTP-запросы;
|
||||
получение exchangeInfo;
|
||||
schema validation;
|
||||
parsing;
|
||||
value validation;
|
||||
mapping;
|
||||
создание конкретного Handler;
|
||||
создание конкретного Feed;
|
||||
создание Registry;
|
||||
создание Acquisition Service;
|
||||
кэширование;
|
||||
изменение ExchangeService;
|
||||
изменение runtime;
|
||||
изменение Telegram UI;
|
||||
изменение автоторговли.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменённые файлы
|
||||
|
||||
В рамках Build 007 изменены:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/protocol.py
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
```
|
||||
|
||||
Создан тестовый файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/test_protocol.py
|
||||
```
|
||||
|
||||
Не изменялись:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
app/src/market_data/acquisition/handlers/instrument_handler.py
|
||||
app/src/market_data/acquisition/feeds/instrument_feed.py
|
||||
app/src/market_data/acquisition/registry.py
|
||||
app/src/market_data/acquisition/service.py
|
||||
|
||||
app/src/market_data/acquisition/adapters/dzengi/models.py
|
||||
app/src/market_data/acquisition/adapters/dzengi/parser.py
|
||||
app/src/market_data/acquisition/adapters/dzengi/mapper.py
|
||||
|
||||
app/src/market_data/acquisition/validation/schema.py
|
||||
app/src/market_data/acquisition/validation/values.py
|
||||
|
||||
app/src/integrations/exchange/*
|
||||
app/src/telegram/*
|
||||
app/src/trading/*
|
||||
```
|
||||
|
||||
Работающий legacy-код бота не изменён.
|
||||
|
||||
---
|
||||
|
||||
## 5. Созданные Protocol
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/protocol.py
|
||||
```
|
||||
|
||||
созданы три минимальных протокола:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
InstrumentDocumentHandler
|
||||
InstrumentFeedProtocol
|
||||
```
|
||||
|
||||
Все протоколы основаны на структурной типизации Python:
|
||||
|
||||
```python
|
||||
typing.Protocol
|
||||
```
|
||||
|
||||
и объявлены как:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
```
|
||||
|
||||
Это позволяет использовать их как для статической типизации, так и для ограниченной runtime-проверки через `isinstance()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. InstrumentDocumentSource
|
||||
|
||||
Контракт:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class InstrumentDocumentSource(Protocol):
|
||||
def fetch_instrument_document(self) -> object:
|
||||
...
|
||||
```
|
||||
|
||||
Назначение:
|
||||
|
||||
```text
|
||||
получить декодированный транспортный документ Instrument Reference Data.
|
||||
```
|
||||
|
||||
Предполагаемый конкретный потребитель этого контракта появится в:
|
||||
|
||||
```text
|
||||
Build 008 — Dzengi REST Adapter
|
||||
```
|
||||
|
||||
Будущий REST adapter должен реализовать метод:
|
||||
|
||||
```python
|
||||
fetch_instrument_document()
|
||||
```
|
||||
|
||||
и вернуть сырой декодированный документ.
|
||||
|
||||
---
|
||||
|
||||
## 7. Почему InstrumentDocumentSource возвращает object
|
||||
|
||||
Возвращаемый тип:
|
||||
|
||||
```python
|
||||
object
|
||||
```
|
||||
|
||||
выбран сознательно.
|
||||
|
||||
Транспортный источник не должен выполнять:
|
||||
|
||||
```text
|
||||
schema validation;
|
||||
parsing;
|
||||
value validation;
|
||||
mapping.
|
||||
```
|
||||
|
||||
На транспортной границе REST adapter может получить произвольное декодированное JSON-значение:
|
||||
|
||||
```text
|
||||
dict
|
||||
list
|
||||
str
|
||||
int
|
||||
float
|
||||
bool
|
||||
None
|
||||
```
|
||||
|
||||
Проверка структуры является обязанностью:
|
||||
|
||||
```text
|
||||
Schema Validation
|
||||
```
|
||||
|
||||
Поэтому транспортный слой не должен преждевременно утверждать, что полученный документ является корректным JSON-объектом нужной структуры.
|
||||
|
||||
Архитектурная граница остаётся следующей:
|
||||
|
||||
```text
|
||||
REST Adapter
|
||||
↓
|
||||
object
|
||||
↓
|
||||
Schema Validation
|
||||
↓
|
||||
ValidatedExchangeInfoDocument
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. InstrumentDocumentHandler
|
||||
|
||||
Контракт:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class InstrumentDocumentHandler(Protocol):
|
||||
def handle_instrument_document(
|
||||
self,
|
||||
document: object,
|
||||
) -> tuple[Instrument, ...]:
|
||||
...
|
||||
```
|
||||
|
||||
Назначение:
|
||||
|
||||
```text
|
||||
преобразовать сырой документ в проверенные внутренние модели Instrument.
|
||||
```
|
||||
|
||||
Конкретная реализация появится в:
|
||||
|
||||
```text
|
||||
Build 009 — Instrument Handler
|
||||
```
|
||||
|
||||
Handler должен объединить уже существующий pipeline:
|
||||
|
||||
```text
|
||||
object
|
||||
↓
|
||||
validate_exchange_info_schema()
|
||||
↓
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
parse_exchange_info()
|
||||
↓
|
||||
DzengiExchangeInfoResponse
|
||||
↓
|
||||
validate_exchange_info_values()
|
||||
↓
|
||||
map_dzengi_exchange_info_to_instruments()
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
При этом Protocol не знает:
|
||||
|
||||
```text
|
||||
какая биржа является источником;
|
||||
какой parser используется;
|
||||
какой mapper используется;
|
||||
какие source-specific raw-модели существуют.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. InstrumentFeedProtocol
|
||||
|
||||
Контракт:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class InstrumentFeedProtocol(Protocol):
|
||||
def load_instruments(self) -> tuple[Instrument, ...]:
|
||||
...
|
||||
```
|
||||
|
||||
Назначение:
|
||||
|
||||
```text
|
||||
получить полный immutable-набор внутренних моделей Instrument.
|
||||
```
|
||||
|
||||
Конкретный Feed появится в:
|
||||
|
||||
```text
|
||||
Build 010 — Instrument Feed
|
||||
```
|
||||
|
||||
Предполагаемая композиция:
|
||||
|
||||
```text
|
||||
InstrumentFeed
|
||||
├── InstrumentDocumentSource
|
||||
└── InstrumentDocumentHandler
|
||||
```
|
||||
|
||||
Рабочая последовательность:
|
||||
|
||||
```text
|
||||
InstrumentFeed.load_instruments()
|
||||
↓
|
||||
InstrumentDocumentSource.fetch_instrument_document()
|
||||
↓
|
||||
object
|
||||
↓
|
||||
InstrumentDocumentHandler.handle_instrument_document()
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Почему протокол называется InstrumentFeedProtocol
|
||||
|
||||
Будущий файл:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/feeds/instrument_feed.py
|
||||
```
|
||||
|
||||
предназначен для конкретной реализации Feed.
|
||||
|
||||
Чтобы избежать конфликта между интерфейсом и конкретным классом, протокол получил имя:
|
||||
|
||||
```text
|
||||
InstrumentFeedProtocol
|
||||
```
|
||||
|
||||
Конкретная реализация в Build 010 сможет называться:
|
||||
|
||||
```text
|
||||
InstrumentFeed
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
```text
|
||||
InstrumentFeedProtocol
|
||||
контракт
|
||||
|
||||
InstrumentFeed
|
||||
конкретная реализация
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Structural Typing
|
||||
|
||||
Реализации не обязаны наследоваться от Protocol напрямую.
|
||||
|
||||
Например:
|
||||
|
||||
```python
|
||||
class StubInstrumentDocumentSource:
|
||||
def fetch_instrument_document(self) -> object:
|
||||
return {
|
||||
"symbols": [],
|
||||
}
|
||||
```
|
||||
|
||||
Такой объект удовлетворяет контракту:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
```
|
||||
|
||||
без явного наследования:
|
||||
|
||||
```python
|
||||
class StubInstrumentDocumentSource(InstrumentDocumentSource):
|
||||
...
|
||||
```
|
||||
|
||||
Это уменьшает связанность между конкретными реализациями и интерфейсами.
|
||||
|
||||
---
|
||||
|
||||
## 12. Runtime Checkable
|
||||
|
||||
Все три Protocol объявлены с:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
```
|
||||
|
||||
Благодаря этому допустима проверка:
|
||||
|
||||
```python
|
||||
isinstance(source, InstrumentDocumentSource)
|
||||
```
|
||||
|
||||
Тестами подтверждено:
|
||||
|
||||
```text
|
||||
объект с требуемым методом
|
||||
→ соответствует Protocol
|
||||
|
||||
объект без требуемого метода
|
||||
→ не соответствует Protocol
|
||||
```
|
||||
|
||||
Важно: runtime-проверка Protocol подтверждает структурное наличие требуемых атрибутов и методов, но не выполняет полную глубокую проверку всех аннотаций типов и фактических возвращаемых значений.
|
||||
|
||||
---
|
||||
|
||||
## 13. Новая транспортная ошибка
|
||||
|
||||
В файл:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
```
|
||||
|
||||
добавлена:
|
||||
|
||||
```python
|
||||
class InstrumentReferenceTransportError(
|
||||
MarketDataAcquisitionError
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
Она предназначена для будущего:
|
||||
|
||||
```text
|
||||
Build 008 — Dzengi REST Adapter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Назначение InstrumentReferenceTransportError
|
||||
|
||||
Ошибка предназначена для проблем получения Instrument Reference Data от внешнего источника.
|
||||
|
||||
Потенциальные случаи:
|
||||
|
||||
```text
|
||||
ошибка соединения;
|
||||
timeout;
|
||||
HTTP error;
|
||||
ошибка JSON decoding;
|
||||
непредвиденная ошибка REST-клиента.
|
||||
```
|
||||
|
||||
Она не используется для:
|
||||
|
||||
```text
|
||||
неверной структуры документа;
|
||||
ошибки parsing;
|
||||
недопустимых значений;
|
||||
ошибки mapping.
|
||||
```
|
||||
|
||||
Эти случаи уже имеют специализированные типы исключений.
|
||||
|
||||
---
|
||||
|
||||
## 15. Итоговая иерархия ошибок
|
||||
|
||||
После Build 007 иерархия выглядит следующим образом:
|
||||
|
||||
```text
|
||||
MarketDataAcquisitionError
|
||||
├── InstrumentReferenceTransportError
|
||||
├── InstrumentReferenceSchemaError
|
||||
├── InstrumentReferenceParseError
|
||||
├── InstrumentReferenceValueError
|
||||
└── InstrumentReferenceMappingError
|
||||
```
|
||||
|
||||
Назначение ошибок:
|
||||
|
||||
| Ошибка | Ответственность |
|
||||
|---|---|
|
||||
| `InstrumentReferenceTransportError` | Получение данных от внешнего источника |
|
||||
| `InstrumentReferenceSchemaError` | Структура исходного документа |
|
||||
| `InstrumentReferenceParseError` | Преобразование проверенного документа в raw-модели |
|
||||
| `InstrumentReferenceValueError` | Допустимость значений |
|
||||
| `InstrumentReferenceMappingError` | Преобразование raw-модели во внутреннюю модель `Instrument` |
|
||||
|
||||
---
|
||||
|
||||
## 16. Какие дополнительные Protocol не создавались
|
||||
|
||||
В Build 007 сознательно не создавались отдельные Protocol для:
|
||||
|
||||
```text
|
||||
Schema Validator
|
||||
Parser
|
||||
Value Validator
|
||||
Mapper
|
||||
Registry
|
||||
Acquisition Service
|
||||
```
|
||||
|
||||
Причины:
|
||||
|
||||
```text
|
||||
validators, parser и mapper уже реализованы как чистые функции;
|
||||
|
||||
Registry и Acquisition Service пока не имеют нескольких реализаций;
|
||||
|
||||
дополнительные интерфейсы сейчас не используются;
|
||||
|
||||
создание таких контрактов было бы преждевременной абстракцией.
|
||||
```
|
||||
|
||||
Это соответствует принципу:
|
||||
|
||||
```text
|
||||
не создавать абстракции «на будущее» без конкретного потребителя.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. Какие дополнительные Exceptions не создавались
|
||||
|
||||
В Build 007 сознательно не добавлялись:
|
||||
|
||||
```text
|
||||
InstrumentReferenceHandlerError
|
||||
InstrumentReferenceFeedError
|
||||
InstrumentReferenceRegistryError
|
||||
InstrumentReferenceServiceError
|
||||
```
|
||||
|
||||
Причины:
|
||||
|
||||
```text
|
||||
Handler может передавать точные ошибки Schema, Parser, Value и Mapping;
|
||||
|
||||
Feed может передавать точные ошибки Transport и Processing;
|
||||
|
||||
Registry ещё не реализован;
|
||||
|
||||
Acquisition Service ещё не реализован;
|
||||
|
||||
новые типы ошибок следует вводить только там, где появляется реальная новая категория отказа.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Реализованные тестовые сценарии
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/test_protocol.py
|
||||
```
|
||||
|
||||
Реализовано 7 тестов.
|
||||
|
||||
Проверены:
|
||||
|
||||
1. соответствие корректного source объекта `InstrumentDocumentSource`;
|
||||
2. соответствие корректного handler объекта `InstrumentDocumentHandler`;
|
||||
3. соответствие корректного feed объекта `InstrumentFeedProtocol`;
|
||||
4. отклонение объектов без обязательных методов;
|
||||
5. structural typing без явного наследования;
|
||||
6. наследование `InstrumentReferenceTransportError` от `MarketDataAcquisitionError`;
|
||||
7. наличие общего базового типа у всех ошибок Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
## 19. Выполненные проверки
|
||||
|
||||
### Проверка 1 — unit-тесты Protocol и Exceptions
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/test_protocol.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
....... [100%]
|
||||
7 passed in 0.01s
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 2 — Python compilation
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/protocol.py \
|
||||
src/market_data/acquisition/exceptions.py \
|
||||
tests/unit/market_data/acquisition/test_protocol.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Команда завершилась без ошибок и без вывода.
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 3 — полный набор тестов проекта
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
................................................................................................................. [100%]
|
||||
113 passed in 0.06s
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка 4 — отсутствие преждевременной production-интеграции
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "InstrumentDocumentSource|InstrumentDocumentHandler|InstrumentFeedProtocol|InstrumentReferenceTransportError" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
используется только в protocol.py и unit-тестах
|
||||
|
||||
InstrumentDocumentHandler
|
||||
используется только в protocol.py и unit-тестах
|
||||
|
||||
InstrumentFeedProtocol
|
||||
используется только в protocol.py и unit-тестах
|
||||
|
||||
InstrumentReferenceTransportError
|
||||
объявлена в exceptions.py и используется только в unit-тестах
|
||||
```
|
||||
|
||||
Не обнаружено подключения к:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/*
|
||||
src/telegram/*
|
||||
src/trading/*
|
||||
```
|
||||
|
||||
Статус:
|
||||
|
||||
```text
|
||||
PASSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Архитектура после Build 007
|
||||
|
||||
После завершения Build 007 новая часть подсистемы имеет следующую структуру:
|
||||
|
||||
```text
|
||||
market_data/
|
||||
└── acquisition/
|
||||
├── exceptions.py
|
||||
│ ├── MarketDataAcquisitionError
|
||||
│ ├── InstrumentReferenceTransportError
|
||||
│ ├── InstrumentReferenceSchemaError
|
||||
│ ├── InstrumentReferenceParseError
|
||||
│ ├── InstrumentReferenceValueError
|
||||
│ └── InstrumentReferenceMappingError
|
||||
│
|
||||
├── protocol.py
|
||||
│ ├── InstrumentDocumentSource
|
||||
│ ├── InstrumentDocumentHandler
|
||||
│ └── InstrumentFeedProtocol
|
||||
│
|
||||
├── models/
|
||||
│ └── instrument.py
|
||||
│ └── Instrument
|
||||
│
|
||||
├── validation/
|
||||
│ ├── schema.py
|
||||
│ └── values.py
|
||||
│
|
||||
└── adapters/
|
||||
└── dzengi/
|
||||
├── models.py
|
||||
├── parser.py
|
||||
├── mapper.py
|
||||
└── rest.py
|
||||
```
|
||||
|
||||
На текущем этапе:
|
||||
|
||||
```text
|
||||
rest.py
|
||||
ещё не реализован
|
||||
|
||||
instrument_handler.py
|
||||
ещё не реализован
|
||||
|
||||
instrument_feed.py
|
||||
ещё не реализован
|
||||
|
||||
registry.py
|
||||
ещё не реализован
|
||||
|
||||
service.py
|
||||
ещё не реализован
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. Полная архитектурная цепочка после Build 007
|
||||
|
||||
Уже реализовано:
|
||||
|
||||
```text
|
||||
raw JSON document
|
||||
↓
|
||||
validate_exchange_info_schema()
|
||||
↓
|
||||
ValidatedExchangeInfoDocument
|
||||
↓
|
||||
parse_exchange_info()
|
||||
↓
|
||||
DzengiExchangeInfoResponse
|
||||
↓
|
||||
validate_exchange_info_values()
|
||||
↓
|
||||
map_dzengi_exchange_info_to_instruments()
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Зафиксированы контракты для будущей orchestration-цепочки:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
↓
|
||||
InstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentFeedProtocol
|
||||
↓
|
||||
Registry
|
||||
↓
|
||||
Acquisition Service
|
||||
```
|
||||
|
||||
После реализации Build 008–012 предполагаемая полная цепочка будет выглядеть так:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
↓
|
||||
Dzengi REST Adapter
|
||||
implements InstrumentDocumentSource
|
||||
↓
|
||||
object
|
||||
↓
|
||||
Instrument Handler
|
||||
implements InstrumentDocumentHandler
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
Instrument Feed
|
||||
implements InstrumentFeedProtocol
|
||||
↓
|
||||
Registry
|
||||
↓
|
||||
Acquisition Service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22. Влияние на legacy-систему
|
||||
|
||||
Build 007 не подключён к существующим компонентам:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
ExchangeSymbol
|
||||
SymbolValidationResult
|
||||
Telegram UI
|
||||
AutoTrade
|
||||
Market Stream
|
||||
Market Data Runner
|
||||
Execution Quality
|
||||
```
|
||||
|
||||
Не изменены:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
ExchangeService.validate_symbol()
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
normalize_symbol()
|
||||
symbol_candidates()
|
||||
```
|
||||
|
||||
Старый production-путь продолжает работать без изменений.
|
||||
|
||||
Новая подсистема развивается параллельно.
|
||||
|
||||
---
|
||||
|
||||
## 23. Обратная совместимость
|
||||
|
||||
Подтверждено сохранение:
|
||||
|
||||
```text
|
||||
сигнатур существующих методов;
|
||||
старых импортов;
|
||||
существующего формата ошибок;
|
||||
Telegram UI;
|
||||
автоторговли;
|
||||
runtime-поведения;
|
||||
legacy ExchangeSymbol;
|
||||
legacy-кэша.
|
||||
```
|
||||
|
||||
Build 007 имеет полную обратную совместимость.
|
||||
|
||||
---
|
||||
|
||||
## 24. Классификация изменений
|
||||
|
||||
| Изменение | Классификация |
|
||||
|---|---|
|
||||
| `InstrumentDocumentSource` | Обязательное архитектурное изменение |
|
||||
| `InstrumentDocumentHandler` | Обязательное архитектурное изменение |
|
||||
| `InstrumentFeedProtocol` | Обязательное архитектурное изменение |
|
||||
| `InstrumentReferenceTransportError` | Обязательное архитектурное изменение |
|
||||
| Structural typing | Улучшение архитектурной независимости |
|
||||
| `runtime_checkable` | Улучшение тестируемости и проверяемости контрактов |
|
||||
| Дополнительные преждевременные Protocol | Не создавались |
|
||||
| Дополнительные преждевременные Exceptions | Не создавались |
|
||||
| Изменение production-поведения | Отсутствует |
|
||||
|
||||
---
|
||||
|
||||
## 25. Итог Build 007
|
||||
|
||||
Build 007 завершён успешно.
|
||||
|
||||
Реализовано:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
InstrumentDocumentHandler
|
||||
InstrumentFeedProtocol
|
||||
InstrumentReferenceTransportError
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
```text
|
||||
7 protocol tests passed
|
||||
113 total project tests passed
|
||||
Python compilation passed
|
||||
No production integration detected
|
||||
Legacy bot behavior unchanged
|
||||
```
|
||||
|
||||
Итоговый статус:
|
||||
|
||||
```text
|
||||
BUILD 007 — COMPLETE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 26. Следующий этап
|
||||
|
||||
Следующий этап утверждённого плана:
|
||||
|
||||
```text
|
||||
Build 008 — Dzengi REST Adapter
|
||||
```
|
||||
|
||||
Его задача — реализовать конкретный транспортный источник, соответствующий контракту:
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
```
|
||||
|
||||
Будущая граница Build 008:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
↓
|
||||
Dzengi REST Adapter
|
||||
↓
|
||||
object
|
||||
```
|
||||
|
||||
Build 008 не должен выполнять:
|
||||
|
||||
```text
|
||||
schema validation;
|
||||
parsing;
|
||||
value validation;
|
||||
mapping;
|
||||
создание Instrument;
|
||||
создание Handler;
|
||||
создание Feed;
|
||||
создание Registry;
|
||||
создание Acquisition Service;
|
||||
изменение ExchangeService;
|
||||
production-подключение.
|
||||
```
|
||||
1092
docs/migrations/build_008.md
Normal file
1092
docs/migrations/build_008.md
Normal file
File diff suppressed because it is too large
Load Diff
1275
docs/migrations/build_009.md
Normal file
1275
docs/migrations/build_009.md
Normal file
File diff suppressed because it is too large
Load Diff
1411
docs/migrations/build_010.md
Normal file
1411
docs/migrations/build_010.md
Normal file
File diff suppressed because it is too large
Load Diff
1602
docs/migrations/build_011.md
Normal file
1602
docs/migrations/build_011.md
Normal file
File diff suppressed because it is too large
Load Diff
1556
docs/migrations/build_012.md
Normal file
1556
docs/migrations/build_012.md
Normal file
File diff suppressed because it is too large
Load Diff
812
docs/migrations/build_013.md
Normal file
812
docs/migrations/build_013.md
Normal file
@@ -0,0 +1,812 @@
|
||||
# Dzentra — Instrument Reference Data Migration — Build 013
|
||||
|
||||
> Статус: Завершён
|
||||
|
||||
## Название
|
||||
|
||||
**Проверка эквивалентности старой и новой реализации**
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Доказать эквивалентность существующей legacy-реализации обработки `exchangeInfo` и новой подсистемы Instrument Reference Data до начала переключения production-потребителей.
|
||||
|
||||
Проверка должна подтвердить, что один и тот же исходный документ `exchangeInfo`, обработанный двумя независимыми путями, приводит к эквивалентным результатам в части полей, существующих одновременно в legacy-модели `ExchangeSymbol` и новой модели `Instrument`.
|
||||
|
||||
Build 013 не изменяет production-код и не переключает существующий runtime на новую реализацию.
|
||||
|
||||
---
|
||||
|
||||
## Исходная архитектура проверки
|
||||
|
||||
Один и тот же сохранённый документ используется обеими реализациями:
|
||||
|
||||
```text
|
||||
один exchangeInfo document
|
||||
│
|
||||
├──→ legacy implementation
|
||||
│ │
|
||||
│ ├──→ _extract_exchange_symbols_raw()
|
||||
│ │
|
||||
│ └──→ _parse_exchange_symbol()
|
||||
│ │
|
||||
│ ↓
|
||||
│ list[ExchangeSymbol]
|
||||
│
|
||||
└──→ new implementation
|
||||
│
|
||||
└──→ DzengiInstrumentDocumentHandler
|
||||
│
|
||||
├──→ schema validation
|
||||
├──→ parser
|
||||
├──→ value validation
|
||||
└──→ mapper
|
||||
│
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
|
||||
│
|
||||
↓
|
||||
equivalence comparator
|
||||
│
|
||||
↓
|
||||
structured comparison report
|
||||
```
|
||||
|
||||
Сетевые запросы в проверке не выполняются.
|
||||
|
||||
Обе реализации получают один и тот же сохранённый документ, что исключает влияние изменений данных биржи между двумя отдельными REST-запросами.
|
||||
|
||||
---
|
||||
|
||||
## Реализованные файлы
|
||||
|
||||
Созданы:
|
||||
|
||||
```text
|
||||
app/tests/support/instrument_reference_equivalence.py
|
||||
|
||||
app/tests/unit/market_data/acquisition/
|
||||
└── test_equivalence_comparator.py
|
||||
|
||||
app/tests/integration/market_data/acquisition/
|
||||
└── test_instrument_reference_equivalence.py
|
||||
```
|
||||
|
||||
Production-код не изменялся.
|
||||
|
||||
Не создавался production-модуль:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/equivalence.py
|
||||
```
|
||||
|
||||
Это принципиальное архитектурное решение: механизм проверки эквивалентности является временным миграционным инструментом и не должен создавать зависимость новой production-подсистемы от legacy-модели `ExchangeSymbol`.
|
||||
|
||||
---
|
||||
|
||||
## Проверяемые реализации
|
||||
|
||||
### Legacy implementation
|
||||
|
||||
Проверяется существующая логика:
|
||||
|
||||
```text
|
||||
ExchangeService._extract_exchange_symbols_raw()
|
||||
ExchangeService._parse_exchange_symbol()
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Для запуска legacy parsing logic используется:
|
||||
|
||||
```python
|
||||
service = object.__new__(ExchangeService)
|
||||
```
|
||||
|
||||
Это позволяет проверить существующие parsing helpers без запуска:
|
||||
|
||||
```text
|
||||
ExchangeService.__init__()
|
||||
load_settings()
|
||||
JournalService()
|
||||
REST request
|
||||
exchange symbols cache
|
||||
```
|
||||
|
||||
На корректном документе используемые parsing helpers не требуют `settings` или `journal`.
|
||||
|
||||
Legacy-логика не копируется и не переписывается внутри теста.
|
||||
|
||||
---
|
||||
|
||||
### New implementation
|
||||
|
||||
Проверяется существующий Handler:
|
||||
|
||||
```text
|
||||
DzengiInstrumentDocumentHandler
|
||||
```
|
||||
|
||||
Вызов:
|
||||
|
||||
```python
|
||||
DzengiInstrumentDocumentHandler().handle_instrument_document(document)
|
||||
```
|
||||
|
||||
Через него запускается новая pipeline:
|
||||
|
||||
```text
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Таким образом Build 013 проверяет реальную реализацию, созданную в предыдущих Build, а не её тестовую копию.
|
||||
|
||||
---
|
||||
|
||||
## Источник тестовых данных
|
||||
|
||||
Для integration-проверки используется сохранённый реальный sample:
|
||||
|
||||
```text
|
||||
app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json
|
||||
```
|
||||
|
||||
Один и тот же JSON document передаётся обеим реализациям.
|
||||
|
||||
Это гарантирует корректность сравнения:
|
||||
|
||||
```text
|
||||
same input
|
||||
↓
|
||||
legacy implementation
|
||||
↓
|
||||
new implementation
|
||||
↓
|
||||
equivalence comparison
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Общие поля моделей
|
||||
|
||||
Legacy-модель:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Новая модель:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
```
|
||||
|
||||
Сравниваются только поля, существующие одновременно в обеих моделях:
|
||||
|
||||
```text
|
||||
symbol
|
||||
name
|
||||
status
|
||||
base_asset
|
||||
quote_asset
|
||||
market_modes
|
||||
market_type
|
||||
tick_size
|
||||
step_size
|
||||
min_qty
|
||||
min_notional
|
||||
```
|
||||
|
||||
Всего:
|
||||
|
||||
```text
|
||||
11 общих полей
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Поля новой модели, не участвующие в проверке эквивалентности
|
||||
|
||||
Новая модель `Instrument` содержит дополнительные поля:
|
||||
|
||||
```text
|
||||
asset_type
|
||||
order_types
|
||||
base_asset_precision
|
||||
quote_asset_precision
|
||||
tick_value
|
||||
max_qty
|
||||
country
|
||||
sector
|
||||
industry
|
||||
trading_hours
|
||||
```
|
||||
|
||||
Их отсутствие в legacy-модели `ExchangeSymbol` не является нарушением эквивалентности.
|
||||
|
||||
Эти поля являются расширением новой внутренней модели Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
## Каноническое сравнение числовых значений
|
||||
|
||||
Legacy implementation хранит числовые ограничения как:
|
||||
|
||||
```text
|
||||
float | None
|
||||
```
|
||||
|
||||
Новая модель хранит их как:
|
||||
|
||||
```text
|
||||
Decimal | None
|
||||
```
|
||||
|
||||
Для корректного сравнения обе стороны приводятся к общей канонической форме:
|
||||
|
||||
```python
|
||||
Decimal(str(value))
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
legacy:
|
||||
0.0001
|
||||
|
||||
new:
|
||||
Decimal("0.0001")
|
||||
|
||||
canonical comparison:
|
||||
Decimal("0.0001") == Decimal("0.0001")
|
||||
```
|
||||
|
||||
Таким образом различие представления:
|
||||
|
||||
```text
|
||||
float
|
||||
Decimal
|
||||
```
|
||||
|
||||
не считается различием значения.
|
||||
|
||||
Для сравнения не используется:
|
||||
|
||||
```text
|
||||
math.isclose()
|
||||
```
|
||||
|
||||
Поскольку биржевые ограничения являются точными справочными десятичными значениями, а не измерениями с допустимой погрешностью.
|
||||
|
||||
---
|
||||
|
||||
## Сравнение market_modes
|
||||
|
||||
Legacy-модель использует:
|
||||
|
||||
```python
|
||||
list[str]
|
||||
```
|
||||
|
||||
Новая модель использует:
|
||||
|
||||
```python
|
||||
tuple[str, ...]
|
||||
```
|
||||
|
||||
Обе стороны приводятся к:
|
||||
|
||||
```python
|
||||
tuple(value)
|
||||
```
|
||||
|
||||
Поэтому:
|
||||
|
||||
```text
|
||||
["REGULAR"]
|
||||
```
|
||||
|
||||
эквивалентно:
|
||||
|
||||
```text
|
||||
("REGULAR",)
|
||||
```
|
||||
|
||||
Тип контейнера не считается расхождением.
|
||||
|
||||
Порядок значений сохраняется и участвует в сравнении.
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
("REGULAR", "CLOSE_ONLY")
|
||||
```
|
||||
|
||||
не эквивалентно:
|
||||
|
||||
```text
|
||||
("CLOSE_ONLY", "REGULAR")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сравнение строковых значений
|
||||
|
||||
Следующие поля сравниваются точно:
|
||||
|
||||
```text
|
||||
symbol
|
||||
name
|
||||
status
|
||||
base_asset
|
||||
quote_asset
|
||||
market_type
|
||||
```
|
||||
|
||||
Не выполняется дополнительная нормализация:
|
||||
|
||||
```text
|
||||
lower()
|
||||
upper()
|
||||
casefold()
|
||||
alias mapping
|
||||
```
|
||||
|
||||
Причина: Build 013 должен обнаруживать реальные различия интерпретации между двумя реализациями, а не скрывать их дополнительной логикой comparator.
|
||||
|
||||
---
|
||||
|
||||
## Проверка состава инструментов
|
||||
|
||||
До сравнения полей проверяются:
|
||||
|
||||
```text
|
||||
дубликаты symbol в legacy implementation;
|
||||
дубликаты symbol в new implementation;
|
||||
symbol существует только в legacy;
|
||||
symbol существует только в new.
|
||||
```
|
||||
|
||||
После проверки дубликатов строятся индексы:
|
||||
|
||||
```text
|
||||
symbol → ExchangeSymbol
|
||||
symbol → Instrument
|
||||
```
|
||||
|
||||
Для построения индексов используется сохранение первого встретившегося объекта.
|
||||
|
||||
Дубликаты при этом уже отдельно фиксируются как mismatch и не могут быть незаметно скрыты перезаписью значения в словаре.
|
||||
|
||||
---
|
||||
|
||||
## Модель расхождения
|
||||
|
||||
Каждое найденное расхождение представлено структурой:
|
||||
|
||||
```text
|
||||
InstrumentReferenceMismatch
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
```text
|
||||
kind
|
||||
symbol
|
||||
field
|
||||
legacy_value
|
||||
new_value
|
||||
message
|
||||
```
|
||||
|
||||
Поддерживаемые категории:
|
||||
|
||||
```text
|
||||
duplicate_legacy
|
||||
duplicate_new
|
||||
missing_in_legacy
|
||||
missing_in_new
|
||||
field_mismatch
|
||||
```
|
||||
|
||||
Пример расхождения поля:
|
||||
|
||||
```text
|
||||
kind: field_mismatch
|
||||
symbol: ETH/EUR_LEVERAGE
|
||||
field: min_notional
|
||||
legacy: None
|
||||
new: Decimal("2")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Модель итогового отчёта
|
||||
|
||||
Результат сравнения представлен структурой:
|
||||
|
||||
```text
|
||||
InstrumentReferenceEquivalenceReport
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
```text
|
||||
legacy_count
|
||||
new_count
|
||||
compared_count
|
||||
mismatches
|
||||
```
|
||||
|
||||
Также предоставляется свойство:
|
||||
|
||||
```text
|
||||
is_equivalent
|
||||
```
|
||||
|
||||
Логика:
|
||||
|
||||
```text
|
||||
mismatches == ()
|
||||
↓
|
||||
is_equivalent == True
|
||||
```
|
||||
|
||||
При наличии хотя бы одного mismatch:
|
||||
|
||||
```text
|
||||
is_equivalent == False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Диагностический отчёт
|
||||
|
||||
Метод:
|
||||
|
||||
```text
|
||||
InstrumentReferenceEquivalenceReport.format()
|
||||
```
|
||||
|
||||
формирует человекочитаемый диагностический отчёт.
|
||||
|
||||
Успешный результат имеет вид:
|
||||
|
||||
```text
|
||||
Instrument Reference Data equivalence report
|
||||
legacy_count: 51
|
||||
new_count: 51
|
||||
compared_count: 51
|
||||
mismatches: 0
|
||||
is_equivalent: True
|
||||
```
|
||||
|
||||
При обнаружении различий отчёт содержит для каждого mismatch:
|
||||
|
||||
```text
|
||||
kind
|
||||
symbol
|
||||
field
|
||||
legacy value
|
||||
new value
|
||||
message
|
||||
```
|
||||
|
||||
Это позволяет анализировать все обнаруженные расхождения, а не только первое.
|
||||
|
||||
---
|
||||
|
||||
## Unit-тесты comparator
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/test_equivalence_comparator.py
|
||||
```
|
||||
|
||||
Реализовано 11 тестов.
|
||||
|
||||
Проверяются:
|
||||
|
||||
1. полностью эквивалентные инструменты;
|
||||
2. эквивалентность `float` и `Decimal`;
|
||||
3. эквивалентность `list` и `tuple` для `market_modes`;
|
||||
4. несовпадение строкового поля;
|
||||
5. несовпадение числового поля;
|
||||
6. символ отсутствует в новой реализации;
|
||||
7. символ отсутствует в legacy-реализации;
|
||||
8. duplicate symbol в legacy;
|
||||
9. duplicate symbol в новой реализации;
|
||||
10. несколько расхождений одновременно;
|
||||
11. корректное формирование диагностического отчёта.
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
11 passed in 0.02s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration-проверка реального exchangeInfo sample
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
app/tests/integration/market_data/acquisition/
|
||||
└── test_instrument_reference_equivalence.py
|
||||
```
|
||||
|
||||
Проверяется полный путь:
|
||||
|
||||
```text
|
||||
all.json
|
||||
│
|
||||
├──→ legacy parser
|
||||
│ ↓
|
||||
│ list[ExchangeSymbol]
|
||||
│
|
||||
└──→ DzengiInstrumentDocumentHandler
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
│
|
||||
↓
|
||||
compare_instrument_reference_data()
|
||||
│
|
||||
↓
|
||||
InstrumentReferenceEquivalenceReport
|
||||
```
|
||||
|
||||
Основное утверждение:
|
||||
|
||||
```python
|
||||
assert report.is_equivalent, report.format()
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
1 passed in 0.10s
|
||||
```
|
||||
|
||||
Проверка подтвердила эквивалентность legacy- и новой реализации на одном и том же реальном `exchangeInfo` sample.
|
||||
|
||||
---
|
||||
|
||||
## Проверка синтаксиса
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
tests/support/instrument_reference_equivalence.py \
|
||||
tests/unit/market_data/acquisition/test_equivalence_comparator.py \
|
||||
tests/integration/market_data/acquisition/test_instrument_reference_equivalence.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
Ошибок синтаксиса не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## Полный регрессионный прогон
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
192 passed in 0.15s
|
||||
```
|
||||
|
||||
Регрессий существующего проекта не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## Проверка отсутствия production-зависимостей
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "compare_instrument_reference_data|InstrumentReferenceMismatch|InstrumentReferenceEquivalenceReport" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Результат подтвердил, что все сущности механизма проверки эквивалентности находятся только в:
|
||||
|
||||
```text
|
||||
tests/support/
|
||||
tests/unit/
|
||||
tests/integration/
|
||||
```
|
||||
|
||||
В каталоге:
|
||||
|
||||
```text
|
||||
src/
|
||||
```
|
||||
|
||||
упоминаний нет.
|
||||
|
||||
Следовательно:
|
||||
|
||||
```text
|
||||
production-код не зависит от comparator;
|
||||
новая acquisition-подсистема не зависит от legacy-модели ради runtime;
|
||||
ExchangeService не изменён;
|
||||
Telegram UI не изменён;
|
||||
AutoTrade не изменён;
|
||||
trading runtime не изменён.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Изменения production-кода
|
||||
|
||||
В рамках Build 013 production-код не изменялся.
|
||||
|
||||
Не изменены:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/integrations/exchange/models.py
|
||||
|
||||
app/src/market_data/acquisition/
|
||||
app/src/telegram/
|
||||
app/src/trading/
|
||||
```
|
||||
|
||||
Не создавался:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/equivalence.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обратная совместимость
|
||||
|
||||
Полностью сохранены:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
ExchangeService.validate_symbol()
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
```
|
||||
|
||||
Также не изменены:
|
||||
|
||||
```text
|
||||
сигнатуры существующих production-методов;
|
||||
legacy imports;
|
||||
формат runtime-ошибок;
|
||||
Telegram UI;
|
||||
автоторговля;
|
||||
существующее runtime-поведение;
|
||||
exchange symbols cache.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что Build 013 намеренно не делает
|
||||
|
||||
Build 013 не выполняет:
|
||||
|
||||
```text
|
||||
изменение legacy parser;
|
||||
изменение нового parser;
|
||||
изменение mapper;
|
||||
переключение get_exchange_symbols();
|
||||
создание compatibility mapper Instrument → ExchangeSymbol;
|
||||
перенос кэша;
|
||||
изменение validate_symbol();
|
||||
изменение get_symbol_runtime_status();
|
||||
подключение InstrumentAcquisitionService к production runtime;
|
||||
изменение Telegram UI;
|
||||
изменение AutoTrade.
|
||||
```
|
||||
|
||||
Эти изменения относятся к последующим Build утверждённого Migration Plan.
|
||||
|
||||
---
|
||||
|
||||
## Классификация изменений
|
||||
|
||||
| Изменение | Классификация |
|
||||
|---|---|
|
||||
| Comparator legacy/new | Миграционная проверка |
|
||||
| Структурированный отчёт | Улучшение диагностируемости |
|
||||
| Unit-тесты comparator | Обязательная проверка надёжности |
|
||||
| Integration-тест на одном sample | Обязательная проверка эквивалентности |
|
||||
| Новый production-модуль | Не создавался |
|
||||
| Изменение legacy-кода | Отсутствует |
|
||||
| Изменение новой acquisition pipeline | Отсутствует |
|
||||
| Изменение runtime-поведения | Отсутствует |
|
||||
|
||||
---
|
||||
|
||||
## Итоговые проверки
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Unit-тесты comparator | `11 passed in 0.02s` |
|
||||
| Integration-тест реального sample | `1 passed in 0.10s` |
|
||||
| `py_compile` | Успешно |
|
||||
| Полный `pytest` | `192 passed in 0.15s` |
|
||||
| Отсутствие production-интеграции comparator | Подтверждено |
|
||||
|
||||
---
|
||||
|
||||
## Условие завершения Build 013
|
||||
|
||||
Все условия выполнены:
|
||||
|
||||
```text
|
||||
[✓] Comparator обнаруживает категории различий.
|
||||
|
||||
[✓] Legacy и new implementations получают один и тот же документ.
|
||||
|
||||
[✓] Legacy parser запускается без REST-запроса и кэша.
|
||||
|
||||
[✓] New pipeline запускается через реальный DzengiInstrumentDocumentHandler.
|
||||
|
||||
[✓] Дубликаты проверяются до сравнения полей.
|
||||
|
||||
[✓] Проверяются все 11 общих полей.
|
||||
|
||||
[✓] Реальный exchangeInfo sample проходит без mismatches.
|
||||
|
||||
[✓] Unit-тесты comparator проходят.
|
||||
|
||||
[✓] Integration-тест проходит.
|
||||
|
||||
[✓] Синтаксическая проверка проходит.
|
||||
|
||||
[✓] Полный набор тестов проекта проходит.
|
||||
|
||||
[✓] Production-код не изменён.
|
||||
|
||||
[✓] Обратная совместимость полностью сохранена.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Результат
|
||||
|
||||
```text
|
||||
BUILD 013 — COMPLETE
|
||||
```
|
||||
|
||||
Проверка подтвердила эквивалентность существующей legacy-реализации и новой Instrument Reference Data pipeline на одном и том же реальном документе `exchangeInfo`.
|
||||
|
||||
Можно переходить к следующему этапу утверждённого Migration Plan:
|
||||
|
||||
```text
|
||||
Build 014 — Compatibility mapper Instrument → ExchangeSymbol
|
||||
```
|
||||
890
docs/migrations/build_014.md
Normal file
890
docs/migrations/build_014.md
Normal file
@@ -0,0 +1,890 @@
|
||||
# Dzentra — Instrument Reference Data Migration — Build 014
|
||||
|
||||
> Статус: Завершён
|
||||
|
||||
## Название
|
||||
|
||||
**Compatibility mapper Instrument → ExchangeSymbol**
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Создать временный compatibility-слой, преобразующий новую внутреннюю модель:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
```
|
||||
|
||||
в существующую legacy-модель:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Целевая цепочка:
|
||||
|
||||
```text
|
||||
new acquisition pipeline
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
compatibility mapper
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Compatibility mapper необходим для последующего переключения:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
на новую Instrument Reference Data pipeline без изменения существующего публичного контракта:
|
||||
|
||||
```python
|
||||
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
|
||||
...
|
||||
```
|
||||
|
||||
Build 014 создаёт только compatibility-границу.
|
||||
|
||||
Переключение `ExchangeService.get_exchange_symbols()` в рамках этого Build не выполняется.
|
||||
|
||||
---
|
||||
|
||||
## Причина создания compatibility-слоя
|
||||
|
||||
К началу Build 014 завершены:
|
||||
|
||||
```text
|
||||
Build 001–012
|
||||
↓
|
||||
создана новая независимая Instrument Reference Data acquisition pipeline
|
||||
|
||||
Build 013
|
||||
↓
|
||||
доказана эквивалентность legacy- и новой реализации
|
||||
на одном и том же реальном exchangeInfo sample
|
||||
```
|
||||
|
||||
Новая pipeline возвращает:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Существующий legacy-контракт возвращает:
|
||||
|
||||
```python
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Существующие production-потребители продолжают ожидать:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Поэтому прямое переключение невозможно без временного преобразования:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Реализованные файлы
|
||||
|
||||
Создан production-файл:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/compatibility.py
|
||||
```
|
||||
|
||||
Создан unit-тест:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/test_compatibility.py
|
||||
```
|
||||
|
||||
Другие файлы в рамках Build 014 не изменялись.
|
||||
|
||||
---
|
||||
|
||||
## Размещение compatibility mapper
|
||||
|
||||
Compatibility mapper размещён в:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/compatibility.py
|
||||
```
|
||||
|
||||
Он намеренно не размещён в:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/adapters/dzengi/
|
||||
```
|
||||
|
||||
поскольку преобразование:
|
||||
|
||||
```text
|
||||
Instrument → ExchangeSymbol
|
||||
```
|
||||
|
||||
не зависит от формата Dzengi.
|
||||
|
||||
Он также не размещён в:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/
|
||||
```
|
||||
|
||||
поскольку новый миграционный код не должен расширять legacy-подсистему.
|
||||
|
||||
Архитектурная граница имеет следующий вид:
|
||||
|
||||
```text
|
||||
new acquisition model
|
||||
↓
|
||||
compatibility.py
|
||||
↓
|
||||
legacy integration model
|
||||
```
|
||||
|
||||
Compatibility mapper является временным слоем и должен быть удалён после полного перевода production-потребителей:
|
||||
|
||||
```text
|
||||
Build 024 — Удаление compatibility-слоя
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Реализованные функции
|
||||
|
||||
Созданы две функции:
|
||||
|
||||
```python
|
||||
def map_instrument_to_exchange_symbol(
|
||||
instrument: Instrument,
|
||||
) -> ExchangeSymbol:
|
||||
...
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
def map_instruments_to_exchange_symbols(
|
||||
instruments: tuple[Instrument, ...],
|
||||
) -> list[ExchangeSymbol]:
|
||||
...
|
||||
```
|
||||
|
||||
Первая функция преобразует один объект:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Вторая преобразует полный immutable-набор:
|
||||
|
||||
```text
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные зависимости
|
||||
|
||||
Compatibility mapper сознательно зависит от обеих моделей:
|
||||
|
||||
```python
|
||||
from src.integrations.exchange.models import ExchangeSymbol
|
||||
from src.market_data.acquisition.models.instrument import Instrument
|
||||
```
|
||||
|
||||
Это допустимая временная зависимость:
|
||||
|
||||
```text
|
||||
новая acquisition-подсистема
|
||||
↓
|
||||
compatibility boundary
|
||||
↓
|
||||
legacy contract
|
||||
```
|
||||
|
||||
Никакие другие компоненты новой acquisition pipeline не изменялись для добавления зависимости от `ExchangeSymbol`.
|
||||
|
||||
Re-export через:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/__init__.py
|
||||
```
|
||||
|
||||
не добавлялся.
|
||||
|
||||
---
|
||||
|
||||
## Соответствие полей
|
||||
|
||||
Compatibility mapper переносит все 11 полей, общих для `Instrument` и `ExchangeSymbol`:
|
||||
|
||||
```text
|
||||
Instrument.symbol
|
||||
→ ExchangeSymbol.symbol
|
||||
|
||||
Instrument.name
|
||||
→ ExchangeSymbol.name
|
||||
|
||||
Instrument.status
|
||||
→ ExchangeSymbol.status
|
||||
|
||||
Instrument.base_asset
|
||||
→ ExchangeSymbol.base_asset
|
||||
|
||||
Instrument.quote_asset
|
||||
→ ExchangeSymbol.quote_asset
|
||||
|
||||
Instrument.market_modes
|
||||
→ ExchangeSymbol.market_modes
|
||||
|
||||
Instrument.market_type
|
||||
→ ExchangeSymbol.market_type
|
||||
|
||||
Instrument.tick_size
|
||||
→ ExchangeSymbol.tick_size
|
||||
|
||||
Instrument.step_size
|
||||
→ ExchangeSymbol.step_size
|
||||
|
||||
Instrument.min_qty
|
||||
→ ExchangeSymbol.min_qty
|
||||
|
||||
Instrument.min_notional
|
||||
→ ExchangeSymbol.min_notional
|
||||
```
|
||||
|
||||
Никакая повторная предметная интерпретация данных в compatibility mapper не выполняется.
|
||||
|
||||
---
|
||||
|
||||
## Поля новой модели, не представленные в legacy-модели
|
||||
|
||||
Модель `Instrument` содержит дополнительные поля:
|
||||
|
||||
```text
|
||||
asset_type
|
||||
order_types
|
||||
base_asset_precision
|
||||
quote_asset_precision
|
||||
tick_value
|
||||
max_qty
|
||||
country
|
||||
sector
|
||||
industry
|
||||
trading_hours
|
||||
```
|
||||
|
||||
Эти поля отсутствуют в legacy-модели:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Поэтому они намеренно не переносятся через compatibility boundary.
|
||||
|
||||
Это ожидаемая потеря расширенной информации:
|
||||
|
||||
```text
|
||||
полная новая модель Instrument
|
||||
↓
|
||||
ограниченный legacy-контракт ExchangeSymbol
|
||||
```
|
||||
|
||||
Compatibility mapper не:
|
||||
|
||||
```text
|
||||
расширяет ExchangeSymbol;
|
||||
создаёт дополнительные атрибуты;
|
||||
переносит данные в несоответствующие поля;
|
||||
изменяет модель Instrument.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Преобразование Decimal → float
|
||||
|
||||
Новая модель `Instrument` использует:
|
||||
|
||||
```python
|
||||
Decimal | None
|
||||
```
|
||||
|
||||
Legacy-модель `ExchangeSymbol` использует:
|
||||
|
||||
```python
|
||||
float | None
|
||||
```
|
||||
|
||||
Преобразованию подлежат:
|
||||
|
||||
```text
|
||||
tick_size
|
||||
step_size
|
||||
min_qty
|
||||
min_notional
|
||||
```
|
||||
|
||||
Правило преобразования:
|
||||
|
||||
```text
|
||||
None
|
||||
↓
|
||||
None
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
Decimal("0.0001")
|
||||
↓
|
||||
0.0001
|
||||
```
|
||||
|
||||
Для этого реализован внутренний helper:
|
||||
|
||||
```python
|
||||
def _decimal_to_float(
|
||||
value: Decimal | None,
|
||||
) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return float(value)
|
||||
```
|
||||
|
||||
Переход к `float` выполняется только на compatibility-границе.
|
||||
|
||||
Внутри новой Instrument Reference Data pipeline точное десятичное представление через `Decimal` сохраняется.
|
||||
|
||||
---
|
||||
|
||||
## Преобразование market_modes
|
||||
|
||||
Новая модель использует:
|
||||
|
||||
```python
|
||||
tuple[str, ...]
|
||||
```
|
||||
|
||||
Legacy-модель использует:
|
||||
|
||||
```python
|
||||
list[str]
|
||||
```
|
||||
|
||||
Compatibility mapper выполняет:
|
||||
|
||||
```python
|
||||
list(instrument.market_modes)
|
||||
```
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
Instrument:
|
||||
("REGULAR", "CLOSE_ONLY")
|
||||
|
||||
↓
|
||||
|
||||
ExchangeSymbol:
|
||||
["REGULAR", "CLOSE_ONLY"]
|
||||
```
|
||||
|
||||
Порядок значений сохраняется.
|
||||
|
||||
Для каждого результата создаётся новый независимый список.
|
||||
|
||||
Изменение:
|
||||
|
||||
```python
|
||||
exchange_symbol.market_modes.append("ADDED_IN_LEGACY")
|
||||
```
|
||||
|
||||
не изменяет:
|
||||
|
||||
```python
|
||||
instrument.market_modes
|
||||
```
|
||||
|
||||
и не влияет на другие объекты `ExchangeSymbol`, созданные из того же `Instrument`.
|
||||
|
||||
---
|
||||
|
||||
## Преобразование полного набора инструментов
|
||||
|
||||
Функция:
|
||||
|
||||
```python
|
||||
map_instruments_to_exchange_symbols()
|
||||
```
|
||||
|
||||
принимает:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
и возвращает:
|
||||
|
||||
```python
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Порядок инструментов сохраняется.
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
(
|
||||
BTC/USD_LEVERAGE,
|
||||
ETH/USD_LEVERAGE,
|
||||
XRP/USD_LEVERAGE,
|
||||
)
|
||||
```
|
||||
|
||||
преобразуется в:
|
||||
|
||||
```text
|
||||
[
|
||||
BTC/USD_LEVERAGE,
|
||||
ETH/USD_LEVERAGE,
|
||||
XRP/USD_LEVERAGE,
|
||||
]
|
||||
```
|
||||
|
||||
Для пустого входного набора:
|
||||
|
||||
```python
|
||||
()
|
||||
```
|
||||
|
||||
возвращается:
|
||||
|
||||
```python
|
||||
[]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Новый тип исключения в рамках Build 014 не создавался.
|
||||
|
||||
Compatibility mapper получает уже построенную и проверенную модель:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
```
|
||||
|
||||
и выполняет только:
|
||||
|
||||
```text
|
||||
чтение полей;
|
||||
Decimal → float;
|
||||
tuple → list;
|
||||
создание ExchangeSymbol.
|
||||
```
|
||||
|
||||
Существующая ошибка:
|
||||
|
||||
```text
|
||||
InstrumentReferenceMappingError
|
||||
```
|
||||
|
||||
не переиспользуется, поскольку она относится к другому направлению преобразования:
|
||||
|
||||
```text
|
||||
Dzengi raw model
|
||||
↓
|
||||
Instrument
|
||||
```
|
||||
|
||||
Создание отдельной категории ошибки без конкретного реального сценария не требуется.
|
||||
|
||||
---
|
||||
|
||||
## Unit-тесты
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/market_data/acquisition/test_compatibility.py
|
||||
```
|
||||
|
||||
Реализовано 12 тестов.
|
||||
|
||||
Проверяются:
|
||||
|
||||
1. преобразование одного `Instrument` в `ExchangeSymbol`;
|
||||
2. перенос всех 11 общих legacy-полей;
|
||||
3. преобразование `Decimal → float`;
|
||||
4. сохранение `None` для отсутствующих числовых значений;
|
||||
5. преобразование `tuple[str, ...] → list[str]` для `market_modes`;
|
||||
6. сохранение порядка `market_modes`;
|
||||
7. создание независимого списка `market_modes`;
|
||||
8. преобразование нескольких инструментов;
|
||||
9. сохранение порядка инструментов;
|
||||
10. пустой `tuple` преобразуется в пустой `list`;
|
||||
11. исходный `Instrument` не изменяется;
|
||||
12. результат compatibility mapper эквивалентен исходным `Instrument` по comparator Build 013.
|
||||
|
||||
---
|
||||
|
||||
## Round-trip проверка через comparator Build 013
|
||||
|
||||
Для дополнительной проверки используется уже созданный в Build 013 comparator:
|
||||
|
||||
```text
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
map_instruments_to_exchange_symbols()
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
↓
|
||||
compare_instrument_reference_data()
|
||||
↓
|
||||
InstrumentReferenceEquivalenceReport
|
||||
```
|
||||
|
||||
Основная проверка:
|
||||
|
||||
```python
|
||||
legacy_symbols = map_instruments_to_exchange_symbols(
|
||||
instruments
|
||||
)
|
||||
|
||||
report = compare_instrument_reference_data(
|
||||
legacy_symbols,
|
||||
instruments,
|
||||
)
|
||||
|
||||
assert report.is_equivalent, report.format()
|
||||
```
|
||||
|
||||
Это подтверждает, что compatibility mapper воспроизводит все 11 общих полей legacy-контракта.
|
||||
|
||||
Comparator остаётся только в тестовом контуре.
|
||||
|
||||
Production-код от comparator не зависит.
|
||||
|
||||
---
|
||||
|
||||
## Результат unit-тестов
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/test_compatibility.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
12 passed in 0.02s
|
||||
```
|
||||
|
||||
Все тесты compatibility mapper успешно пройдены.
|
||||
|
||||
---
|
||||
|
||||
## Проверка синтаксиса
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/compatibility.py \
|
||||
tests/unit/market_data/acquisition/test_compatibility.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
Ошибок синтаксиса не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## Полный регрессионный прогон
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
204 passed in 0.19s
|
||||
```
|
||||
|
||||
До Build 014 полный набор содержал:
|
||||
|
||||
```text
|
||||
192 passed
|
||||
```
|
||||
|
||||
В Build 014 добавлено:
|
||||
|
||||
```text
|
||||
12 новых тестов
|
||||
```
|
||||
|
||||
Итого:
|
||||
|
||||
```text
|
||||
192 + 12 = 204
|
||||
```
|
||||
|
||||
Все предыдущие тесты продолжают проходить.
|
||||
|
||||
Регрессий существующего проекта не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## Проверка отсутствия преждевременного production-использования
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Результат подтвердил, что функции compatibility mapper используются только в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/compatibility.py
|
||||
tests/unit/market_data/acquisition/test_compatibility.py
|
||||
```
|
||||
|
||||
Других production-потребителей нет.
|
||||
|
||||
В частности:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
ещё не использует compatibility mapper.
|
||||
|
||||
Это подтверждает отсутствие преждевременного переключения production runtime.
|
||||
|
||||
---
|
||||
|
||||
## Изменения production-кода
|
||||
|
||||
В рамках Build 014 создан только один новый production-файл:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/compatibility.py
|
||||
```
|
||||
|
||||
Не изменялись:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/integrations/exchange/models.py
|
||||
|
||||
app/src/market_data/acquisition/service.py
|
||||
app/src/market_data/acquisition/registry.py
|
||||
app/src/market_data/acquisition/protocol.py
|
||||
app/src/market_data/acquisition/exceptions.py
|
||||
app/src/market_data/acquisition/models/instrument.py
|
||||
|
||||
app/src/market_data/acquisition/adapters/dzengi/
|
||||
app/src/market_data/acquisition/handlers/instrument_handler.py
|
||||
app/src/market_data/acquisition/feeds/instrument_feed.py
|
||||
|
||||
app/src/telegram/
|
||||
app/src/trading/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обратная совместимость
|
||||
|
||||
Полностью сохранены:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
ExchangeService.validate_symbol()
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
```
|
||||
|
||||
Также не изменены:
|
||||
|
||||
```text
|
||||
сигнатуры существующих production-методов;
|
||||
legacy imports;
|
||||
формат runtime-ошибок;
|
||||
Telegram UI;
|
||||
автоторговля;
|
||||
существующее runtime-поведение;
|
||||
exchange symbols cache.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что Build 014 намеренно не делает
|
||||
|
||||
Build 014 не выполняет:
|
||||
|
||||
```text
|
||||
переключение ExchangeService.get_exchange_symbols();
|
||||
подключение InstrumentAcquisitionService к ExchangeService;
|
||||
создание production composition;
|
||||
изменение exchange symbols cache;
|
||||
изменение validate_symbol();
|
||||
изменение get_symbol_runtime_status();
|
||||
изменение normalize_symbol();
|
||||
изменение symbol_candidates();
|
||||
изменение legacy parser;
|
||||
удаление legacy parser;
|
||||
изменение ExchangeSymbol;
|
||||
изменение Instrument;
|
||||
изменение Telegram UI;
|
||||
изменение AutoTrade.
|
||||
```
|
||||
|
||||
Переключение:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
относится строго к следующему этапу:
|
||||
|
||||
```text
|
||||
Build 015 — Переключение get_exchange_symbols()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Классификация изменений
|
||||
|
||||
| Изменение | Классификация |
|
||||
|---|---|
|
||||
| Compatibility mapper `Instrument → ExchangeSymbol` | Обязательное архитектурное изменение |
|
||||
| `Decimal → float` на legacy-границе | Обратная совместимость |
|
||||
| `tuple → list` для `market_modes` | Обратная совместимость |
|
||||
| Batch mapper | Обязательное архитектурное изменение |
|
||||
| Unit-тесты | Улучшение надёжности |
|
||||
| Round-trip проверка через Build 013 comparator | Миграционная проверка |
|
||||
| Новый exception | Не создавался |
|
||||
| Изменение legacy runtime | Отсутствует |
|
||||
| Изменение поведения | Отсутствует |
|
||||
|
||||
---
|
||||
|
||||
## Итоговые проверки
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Unit-тесты Compatibility Mapper | `12 passed in 0.02s` |
|
||||
| `py_compile` | Успешно |
|
||||
| Полный `pytest` | `204 passed in 0.19s` |
|
||||
| Round-trip эквивалентность | Подтверждена |
|
||||
| Независимость `market_modes` | Подтверждена |
|
||||
| Сохранение порядка инструментов | Подтверждено |
|
||||
| Отсутствие преждевременного production-использования | Подтверждено |
|
||||
|
||||
---
|
||||
|
||||
## Условие завершения Build 014
|
||||
|
||||
Все условия выполнены:
|
||||
|
||||
```text
|
||||
[✓] Создан compatibility mapper Instrument → ExchangeSymbol.
|
||||
|
||||
[✓] Переносятся все 11 общих legacy-полей.
|
||||
|
||||
[✓] Decimal корректно преобразуется в float только на legacy-границе.
|
||||
|
||||
[✓] None сохраняется.
|
||||
|
||||
[✓] market_modes преобразуется из tuple в независимый list.
|
||||
|
||||
[✓] Порядок market_modes сохраняется.
|
||||
|
||||
[✓] Batch mapper сохраняет порядок инструментов.
|
||||
|
||||
[✓] Пустой tuple преобразуется в пустой list.
|
||||
|
||||
[✓] Исходные Instrument не изменяются.
|
||||
|
||||
[✓] Round-trip проверка через comparator Build 013 проходит.
|
||||
|
||||
[✓] 12 unit-тестов проходят.
|
||||
|
||||
[✓] Синтаксическая проверка проходит.
|
||||
|
||||
[✓] Полный набор из 204 тестов проходит.
|
||||
|
||||
[✓] ExchangeService ещё не использует compatibility mapper.
|
||||
|
||||
[✓] Production runtime не переключён преждевременно.
|
||||
|
||||
[✓] Обратная совместимость полностью сохранена.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Результат
|
||||
|
||||
```text
|
||||
BUILD 014 — COMPLETE
|
||||
```
|
||||
|
||||
Создана временная compatibility-граница:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Она позволяет на следующем этапе переключить:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
на новую Instrument Reference Data acquisition pipeline без изменения существующего публичного контракта:
|
||||
|
||||
```python
|
||||
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
|
||||
...
|
||||
```
|
||||
|
||||
Следующий этап утверждённого Migration Plan:
|
||||
|
||||
```text
|
||||
Build 015 — Переключение get_exchange_symbols()
|
||||
```
|
||||
552
docs/migrations/build_015.md
Normal file
552
docs/migrations/build_015.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# Build 015 — Переключение `get_exchange_symbols()` на новый Acquisition Pipeline
|
||||
|
||||
## Статус
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Переключить существующий публичный метод:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
с прямого legacy-получения и обработки `exchangeInfo` на новый стандартизированный Instrument Reference Data acquisition pipeline, сохранив при этом существующий внешний контракт и работоспособность старого бота.
|
||||
|
||||
---
|
||||
|
||||
## Исходное состояние
|
||||
|
||||
До Build 015 метод:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
самостоятельно выполнял весь цикл обработки `exchangeInfo`:
|
||||
|
||||
1. создавал `ExchangeRestClient`;
|
||||
2. выполнял прямой REST-запрос:
|
||||
|
||||
```text
|
||||
/api/v1/exchangeInfo
|
||||
```
|
||||
|
||||
3. извлекал массив `symbols`;
|
||||
4. преобразовывал каждый элемент в legacy-модель `ExchangeSymbol`;
|
||||
5. сохранял результат в class-level cache:
|
||||
|
||||
```python
|
||||
_exchange_symbols_cache
|
||||
```
|
||||
|
||||
Таким образом, transport, validation, parsing, mapping и compatibility logic были сосредоточены внутри legacy `ExchangeService`.
|
||||
|
||||
---
|
||||
|
||||
## Реализованное изменение
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
переключён на новый Instrument Reference Data acquisition pipeline.
|
||||
|
||||
Теперь production-путь использует следующую цепочку:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
_load_exchange_symbols_via_acquisition()
|
||||
│
|
||||
▼
|
||||
DzengiInstrumentDocumentSource
|
||||
│
|
||||
▼
|
||||
DzengiInstrumentDocumentHandler
|
||||
│
|
||||
▼
|
||||
InstrumentFeed
|
||||
│
|
||||
▼
|
||||
InstrumentFeedRegistry
|
||||
│
|
||||
▼
|
||||
InstrumentAcquisitionService
|
||||
│
|
||||
▼
|
||||
Instrument
|
||||
│
|
||||
▼
|
||||
map_instruments_to_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Новый production-путь
|
||||
|
||||
В `ExchangeService` используется отдельный compatibility bridge:
|
||||
|
||||
```python
|
||||
def _load_exchange_symbols_via_acquisition(
|
||||
self,
|
||||
) -> list[ExchangeSymbol]:
|
||||
```
|
||||
|
||||
Его задача:
|
||||
|
||||
1. создать источник Instrument Reference Data для Dzengi;
|
||||
2. создать обработчик документа;
|
||||
3. собрать `InstrumentFeed`;
|
||||
4. зарегистрировать feed;
|
||||
5. выполнить acquisition через `InstrumentAcquisitionService`;
|
||||
6. получить канонические модели `Instrument`;
|
||||
7. преобразовать их в legacy-модели `ExchangeSymbol`.
|
||||
|
||||
Это позволяет старому боту продолжать использовать существующий контракт:
|
||||
|
||||
```python
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
при том, что фактическим источником данных уже является новая архитектура `market_data/acquisition`.
|
||||
|
||||
---
|
||||
|
||||
## Сохранённый публичный контракт
|
||||
|
||||
Сигнатура метода не изменилась:
|
||||
|
||||
```python
|
||||
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
|
||||
```
|
||||
|
||||
Это принципиально важно для безопасной поэтапной миграции.
|
||||
|
||||
Существующие потребители не требуют немедленного изменения и продолжают работать через прежний API.
|
||||
|
||||
В частности, существующий UI продолжает использовать:
|
||||
|
||||
```python
|
||||
exchange_service.get_exchange_symbols()
|
||||
```
|
||||
|
||||
без знания о внутреннем переходе на новый acquisition pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение cache semantics
|
||||
|
||||
Сохранён существующий class-level cache:
|
||||
|
||||
```python
|
||||
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
|
||||
```
|
||||
|
||||
Поведение осталось прежним:
|
||||
|
||||
```text
|
||||
Первый вызов
|
||||
│
|
||||
▼
|
||||
Новый acquisition pipeline
|
||||
│
|
||||
▼
|
||||
Compatibility mapping
|
||||
│
|
||||
▼
|
||||
_exchange_symbols_cache
|
||||
│
|
||||
▼
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Последующие вызовы:
|
||||
|
||||
```text
|
||||
_exchange_symbols_cache
|
||||
│
|
||||
▼
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
без повторного обращения к acquisition pipeline.
|
||||
|
||||
Cache заполняется только после успешной загрузки данных.
|
||||
|
||||
При ошибке acquisition cache остаётся незаполненным.
|
||||
|
||||
---
|
||||
|
||||
## Поведение при отключённой бирже
|
||||
|
||||
Сохранено прежнее поведение:
|
||||
|
||||
```python
|
||||
if not self.settings.exchange_enabled:
|
||||
return []
|
||||
```
|
||||
|
||||
Новый acquisition pipeline в этом случае не вызывается.
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Ошибки нового acquisition pipeline проходят через существующую систему `ExchangeService`.
|
||||
|
||||
При ошибке:
|
||||
|
||||
1. ошибка логируется через:
|
||||
|
||||
```python
|
||||
self._log_exchange_error(...)
|
||||
```
|
||||
|
||||
2. используется legacy endpoint identifier:
|
||||
|
||||
```text
|
||||
exchangeInfo
|
||||
```
|
||||
|
||||
3. вызывающему коду возвращается совместимая `ExchangeError`.
|
||||
|
||||
Это сохраняет существующее поведение старого бота и его журналирования.
|
||||
|
||||
---
|
||||
|
||||
## Удаление прямого legacy REST-пути
|
||||
|
||||
После Build 015 метод:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
больше не выполняет прямой вызов:
|
||||
|
||||
```python
|
||||
ExchangeRestClient().get_json("/api/v1/exchangeInfo")
|
||||
```
|
||||
|
||||
Фактический REST transport теперь инкапсулирован в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
```
|
||||
|
||||
через:
|
||||
|
||||
```python
|
||||
DzengiInstrumentDocumentSource
|
||||
```
|
||||
|
||||
и константу:
|
||||
|
||||
```python
|
||||
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
|
||||
```
|
||||
|
||||
Таким образом, ownership получения Instrument Reference Data перенесён из:
|
||||
|
||||
```text
|
||||
integrations/exchange
|
||||
```
|
||||
|
||||
в:
|
||||
|
||||
```text
|
||||
market_data/acquisition
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy helpers
|
||||
|
||||
В `ExchangeService` временно остаются legacy helpers:
|
||||
|
||||
```python
|
||||
_extract_exchange_symbols_raw()
|
||||
_parse_exchange_symbol()
|
||||
_parse_exchange_symbol_status()
|
||||
_parse_market_modes()
|
||||
_extract_filter_value()
|
||||
```
|
||||
|
||||
Они больше не являются частью нового production-пути `get_exchange_symbols()`.
|
||||
|
||||
Их немедленное удаление не выполнялось в Build 015, поскольку миграция проводится поэтапно и без ненужного расширения scope текущего Build.
|
||||
|
||||
Удаление legacy helpers должно выполняться отдельным контролируемым этапом после подтверждения отсутствия production-зависимостей и завершения необходимых migration/equivalence проверок.
|
||||
|
||||
---
|
||||
|
||||
## Добавленные тесты
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||||
```
|
||||
|
||||
Тестами проверяются:
|
||||
|
||||
- возврат пустого списка при отключённой бирже;
|
||||
- отсутствие вызова acquisition pipeline при отключённой бирже;
|
||||
- возврат существующего cache;
|
||||
- отсутствие повторного acquisition при наличии cache;
|
||||
- загрузка через новый acquisition pipeline;
|
||||
- заполнение `_exchange_symbols_cache`;
|
||||
- повторное использование cache;
|
||||
- сохранение legacy-типа `ExchangeSymbol`;
|
||||
- сохранение порядка инструментов;
|
||||
- корректное распространение ошибок;
|
||||
- отсутствие заполнения cache при ошибке;
|
||||
- сохранение существующего error logging;
|
||||
- отсутствие прямого legacy REST-вызова из `get_exchange_symbols()`;
|
||||
- корректная сборка нового acquisition pipeline;
|
||||
- использование compatibility mapper;
|
||||
- корректное поведение пустого результата.
|
||||
|
||||
---
|
||||
|
||||
## Исправление статической типизации теста
|
||||
|
||||
После первоначального завершения Build 015 в файле:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||||
```
|
||||
|
||||
были обнаружены две ошибки статической типизации Pylance.
|
||||
|
||||
### Типизация yield-fixture
|
||||
|
||||
Исходная аннотация:
|
||||
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_exchange_symbols_cache() -> None:
|
||||
```
|
||||
|
||||
была некорректна, поскольку функция содержит `yield` и является генератором.
|
||||
|
||||
Исправлено на:
|
||||
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_exchange_symbols_cache() -> Iterator[None]:
|
||||
ExchangeService._exchange_symbols_cache = None
|
||||
|
||||
yield
|
||||
|
||||
ExchangeService._exchange_symbols_cache = None
|
||||
```
|
||||
|
||||
Добавлен импорт:
|
||||
|
||||
```python
|
||||
from collections.abc import Iterator
|
||||
```
|
||||
|
||||
### Типизация тестовых settings
|
||||
|
||||
Тестовый helper создаёт `ExchangeService` без вызова его конструктора:
|
||||
|
||||
```python
|
||||
service = object.__new__(ExchangeService)
|
||||
```
|
||||
|
||||
Для изоляции теста используется `SimpleNamespace`, тогда как production-атрибут:
|
||||
|
||||
```python
|
||||
service.settings
|
||||
```
|
||||
|
||||
типизирован как `Settings`.
|
||||
|
||||
Для явного обозначения тестовой границы применён `cast`:
|
||||
|
||||
```python
|
||||
service.settings = cast(
|
||||
Settings,
|
||||
_settings(
|
||||
exchange_enabled=exchange_enabled,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
- production-код не изменялся;
|
||||
- тестовая изоляция сохранена;
|
||||
- `# type: ignore` не использовался;
|
||||
- ошибки Pylance устранены.
|
||||
|
||||
---
|
||||
|
||||
## Результаты окончательной проверки
|
||||
|
||||
### Проверка компиляции
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/service.py \
|
||||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Ошибок нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Unit-тесты Build 015
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_exchange_symbols.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
16 passed in 0.07s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Полный regression suite
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
220 passed in 0.15s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка production-пути
|
||||
|
||||
Выполнен поиск:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "get_exchange_symbols|_exchange_symbols_cache|_load_exchange_symbols_via_acquisition|DzengiInstrumentDocumentSource|InstrumentAcquisitionService|map_instruments_to_exchange_symbols|ExchangeRestClient.*exchangeInfo|exchangeInfo" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Проверка подтвердила:
|
||||
|
||||
- `get_exchange_symbols()` использует `_load_exchange_symbols_via_acquisition()`;
|
||||
- новый production-путь использует `DzengiInstrumentDocumentSource`;
|
||||
- используется `InstrumentAcquisitionService`;
|
||||
- используется compatibility mapper `map_instruments_to_exchange_symbols()`;
|
||||
- class-level cache `_exchange_symbols_cache` сохранён;
|
||||
- прямой legacy REST-вызов `exchangeInfo` удалён из `get_exchange_symbols()`;
|
||||
- существующие внешние потребители продолжают работать через прежний публичный контракт.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
До Build 015:
|
||||
|
||||
```text
|
||||
Legacy consumer
|
||||
│
|
||||
▼
|
||||
ExchangeService.get_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
ExchangeRestClient
|
||||
│
|
||||
▼
|
||||
exchangeInfo
|
||||
│
|
||||
▼
|
||||
Legacy parsing
|
||||
│
|
||||
▼
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
После Build 015:
|
||||
|
||||
```text
|
||||
Legacy consumer
|
||||
│
|
||||
▼
|
||||
ExchangeService.get_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
Instrument Acquisition Pipeline
|
||||
│
|
||||
▼
|
||||
Canonical Instrument
|
||||
│
|
||||
▼
|
||||
Compatibility Mapper
|
||||
│
|
||||
▼
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
- новый `market_data/acquisition` стал фактическим production-владельцем получения Instrument Reference Data;
|
||||
- legacy `ExchangeService` сохраняет прежний публичный API;
|
||||
- существующий бот продолжает работать без массового изменения потребителей;
|
||||
- создан безопасный compatibility boundary между новой и старой архитектурой;
|
||||
- переход выполнен без регрессий.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
```text
|
||||
BUILD 015 — COMPLETE
|
||||
```
|
||||
|
||||
Build 015 завершён.
|
||||
|
||||
`ExchangeService.get_exchange_symbols()` успешно переключён на новый Instrument Reference Data acquisition pipeline с сохранением:
|
||||
|
||||
- существующего публичного контракта;
|
||||
- legacy-модели `ExchangeSymbol`;
|
||||
- cache semantics;
|
||||
- обработки ошибок;
|
||||
- журналирования;
|
||||
- существующих потребителей старого бота.
|
||||
|
||||
Окончательные результаты проверки:
|
||||
|
||||
```text
|
||||
py_compile — успешно
|
||||
16 passed in 0.07s
|
||||
220 passed in 0.15s
|
||||
```
|
||||
544
docs/migrations/build_016.md
Normal file
544
docs/migrations/build_016.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# Build 016 — Перевод `normalize_symbol()` / `symbol_candidates()`
|
||||
|
||||
## Статус
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## Цель Build
|
||||
|
||||
Перенести каноническую реализацию функций нормализации и формирования кандидатов торгового символа из legacy-подсистемы:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/symbol_utils.py
|
||||
```
|
||||
|
||||
в новую подсистему Market Data Acquisition:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/symbols.py
|
||||
```
|
||||
|
||||
при этом:
|
||||
|
||||
- полностью сохранить существующее поведение;
|
||||
- не нарушить работу legacy-кода;
|
||||
- сохранить старый import path;
|
||||
- исключить дублирование реализации;
|
||||
- обеспечить постепенную миграцию без остановки работающего бота.
|
||||
|
||||
---
|
||||
|
||||
## Исходное состояние
|
||||
|
||||
До Build 016 функции:
|
||||
|
||||
```python
|
||||
normalize_symbol()
|
||||
symbol_candidates()
|
||||
```
|
||||
|
||||
были реализованы непосредственно в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/symbol_utils.py
|
||||
```
|
||||
|
||||
Их использовал:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
В частности, функции участвовали в:
|
||||
|
||||
- нормализации запрошенного торгового символа;
|
||||
- проверке существования инструмента;
|
||||
- формировании альтернативных представлений символа;
|
||||
- сопоставлении символов с данными `exchangeInfo`;
|
||||
- поиске торговой комиссии для инструмента.
|
||||
|
||||
Legacy-зависимость выглядела следующим образом:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
│
|
||||
▼
|
||||
src.integrations.exchange.symbol_utils
|
||||
│
|
||||
├── normalize_symbol()
|
||||
└── symbol_candidates()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Целевая архитектура
|
||||
|
||||
После Build 016 каноническая реализация находится в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/symbols.py
|
||||
```
|
||||
|
||||
Legacy-модуль:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/symbol_utils.py
|
||||
```
|
||||
|
||||
сохранён как compatibility facade.
|
||||
|
||||
Итоговая зависимость:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
│
|
||||
▼
|
||||
src.integrations.exchange.symbol_utils
|
||||
│
|
||||
│ compatibility facade
|
||||
▼
|
||||
src.market_data.acquisition.symbols
|
||||
│
|
||||
├── normalize_symbol()
|
||||
└── symbol_candidates()
|
||||
```
|
||||
|
||||
Это позволяет сохранить существующий production-код без массового изменения импортов и одновременно установить новую каноническую точку владения логикой.
|
||||
|
||||
---
|
||||
|
||||
## Созданный файл
|
||||
|
||||
Создан:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/symbols.py
|
||||
```
|
||||
|
||||
Он содержит единственную каноническую реализацию:
|
||||
|
||||
```python
|
||||
def normalize_symbol(raw_symbol: str) -> str:
|
||||
...
|
||||
|
||||
|
||||
def symbol_candidates(raw_symbol: str) -> list[str]:
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Изменённый legacy-модуль
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/symbol_utils.py
|
||||
```
|
||||
|
||||
больше не содержит собственной реализации алгоритмов.
|
||||
|
||||
Он импортирует функции непосредственно из:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.symbols
|
||||
```
|
||||
|
||||
и предоставляет их через прежний import path.
|
||||
|
||||
Таким образом:
|
||||
|
||||
```python
|
||||
legacy_normalize_symbol is new_normalize_symbol
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
legacy_symbol_candidates is new_symbol_candidates
|
||||
```
|
||||
|
||||
возвращают:
|
||||
|
||||
```text
|
||||
True
|
||||
```
|
||||
|
||||
Это подтверждает отсутствие копирования или дублирования функций.
|
||||
|
||||
---
|
||||
|
||||
## Зафиксированный контракт `normalize_symbol()`
|
||||
|
||||
Функция сохраняет существующее legacy-поведение.
|
||||
|
||||
### Нормализация регистра
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
btc/usd
|
||||
```
|
||||
|
||||
преобразуется в:
|
||||
|
||||
```text
|
||||
BTC/USD
|
||||
```
|
||||
|
||||
### Удаление внешних пробелов
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
btc/usd
|
||||
```
|
||||
|
||||
преобразуется в:
|
||||
|
||||
```text
|
||||
BTC/USD
|
||||
```
|
||||
|
||||
### Внутренние пробелы не удаляются
|
||||
|
||||
Функция `normalize_symbol()` не выполняет удаление внутренних пробелов.
|
||||
|
||||
### `%2F` не декодируется
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
BTC%2FUSD
|
||||
```
|
||||
|
||||
остаётся:
|
||||
|
||||
```text
|
||||
BTC%2FUSD
|
||||
```
|
||||
|
||||
Декодирование разделителя выполняется только на этапе формирования кандидатов.
|
||||
|
||||
### Суффикс `_LEVERAGE` не добавляется автоматически
|
||||
|
||||
Функция не модифицирует семантику инструмента и не добавляет leverage-суффикс.
|
||||
|
||||
### Существующий `_LEVERAGE` сохраняется
|
||||
|
||||
Если суффикс уже присутствует в исходном символе, он сохраняется.
|
||||
|
||||
---
|
||||
|
||||
## Зафиксированный контракт `symbol_candidates()`
|
||||
|
||||
Функция формирует упорядоченный список допустимых представлений символа.
|
||||
|
||||
### Пустое значение
|
||||
|
||||
Для пустого значения возвращается:
|
||||
|
||||
```python
|
||||
[]
|
||||
```
|
||||
|
||||
### Первый кандидат
|
||||
|
||||
Первым всегда является результат:
|
||||
|
||||
```python
|
||||
normalize_symbol(raw_symbol)
|
||||
```
|
||||
|
||||
### Декодирование `%2F`
|
||||
|
||||
Если символ содержит:
|
||||
|
||||
```text
|
||||
%2F
|
||||
```
|
||||
|
||||
добавляется кандидат с:
|
||||
|
||||
```text
|
||||
/
|
||||
```
|
||||
|
||||
### Удаление обычных внутренних пробелов
|
||||
|
||||
После декодирования разделителя формируется вариант без обычных пробелов.
|
||||
|
||||
### Порядок преобразований сохраняется
|
||||
|
||||
Порядок кандидатов является частью compatibility-контракта:
|
||||
|
||||
```text
|
||||
1. Нормализованное исходное значение.
|
||||
2. Значение после замены %2F на /.
|
||||
3. Значение после удаления обычных пробелов.
|
||||
```
|
||||
|
||||
### Дубликаты не добавляются
|
||||
|
||||
Если очередное преобразование не изменило значение, новый кандидат не создаётся.
|
||||
|
||||
### Каждый вызов возвращает новый список
|
||||
|
||||
Результат не переиспользует mutable list между вызовами.
|
||||
|
||||
### Исходная строка не изменяется
|
||||
|
||||
Функция не мутирует входное значение.
|
||||
|
||||
### Табуляция не удаляется
|
||||
|
||||
Удаляются только обычные пробелы:
|
||||
|
||||
```text
|
||||
" "
|
||||
```
|
||||
|
||||
Внутренняя табуляция сохраняется.
|
||||
|
||||
### Перевод строки не удаляется
|
||||
|
||||
Внутренний символ новой строки сохраняется.
|
||||
|
||||
### `_LEVERAGE` сохраняется
|
||||
|
||||
Функция не изменяет существующий leverage-суффикс.
|
||||
|
||||
---
|
||||
|
||||
## Добавленные тесты
|
||||
|
||||
Создан:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/test_symbols.py
|
||||
```
|
||||
|
||||
Тесты фиксируют канонический контракт новой реализации.
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
29 passed in 0.02s
|
||||
```
|
||||
|
||||
Также создан:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_symbol_utils.py
|
||||
```
|
||||
|
||||
Этот набор тестов проверяет compatibility facade и эквивалентность legacy и новой реализации.
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
22 passed in 0.01s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка identity compatibility
|
||||
|
||||
Отдельно подтверждено, что legacy facade возвращает непосредственно те же функции:
|
||||
|
||||
```python
|
||||
assert legacy_normalize_symbol is new_normalize_symbol
|
||||
assert legacy_symbol_candidates is new_symbol_candidates
|
||||
```
|
||||
|
||||
Следовательно:
|
||||
|
||||
- отдельной legacy-реализации больше нет;
|
||||
- wrapper-функции отсутствуют;
|
||||
- поведение не может разойтись из-за двух независимых реализаций.
|
||||
|
||||
---
|
||||
|
||||
## Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/symbols.py \
|
||||
src/integrations/exchange/symbol_utils.py \
|
||||
tests/unit/market_data/acquisition/test_symbols.py \
|
||||
tests/unit/integrations/exchange/test_symbol_utils.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Успешно.
|
||||
Синтаксических ошибок нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полный regression suite
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
271 passed in 0.17s
|
||||
```
|
||||
|
||||
До Build 016 полный набор проекта содержал:
|
||||
|
||||
```text
|
||||
220 passed
|
||||
```
|
||||
|
||||
Build 016 добавил:
|
||||
|
||||
```text
|
||||
51 test
|
||||
```
|
||||
|
||||
Итог:
|
||||
|
||||
```text
|
||||
220 + 51 = 271
|
||||
```
|
||||
|
||||
Все тесты проекта проходят успешно.
|
||||
|
||||
---
|
||||
|
||||
## Финальная проверка зависимостей
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "normalize_symbol|symbol_candidates|def normalize_symbol|def symbol_candidates" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Проверка подтвердила:
|
||||
|
||||
- `normalize_symbol()` реализована только в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/symbols.py
|
||||
```
|
||||
|
||||
- `symbol_candidates()` реализована только там же;
|
||||
- `src/integrations/exchange/symbol_utils.py` содержит только compatibility imports и exports;
|
||||
- `ExchangeService` продолжает использовать существующий стабильный legacy import path;
|
||||
- дублирование реализации отсутствует.
|
||||
|
||||
---
|
||||
|
||||
## Состояние `ExchangeService`
|
||||
|
||||
В рамках Build 016 файл:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
не требовал изменения import path.
|
||||
|
||||
Он продолжает использовать:
|
||||
|
||||
```python
|
||||
from src.integrations.exchange.symbol_utils import (
|
||||
normalize_symbol,
|
||||
symbol_candidates,
|
||||
)
|
||||
```
|
||||
|
||||
Это сделано намеренно.
|
||||
|
||||
Legacy import path остаётся стабильным, а фактическая реализация уже принадлежит новой подсистеме:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition
|
||||
```
|
||||
|
||||
Такой подход соответствует принятой стратегии постепенной миграции:
|
||||
|
||||
```text
|
||||
сначала новая каноническая реализация
|
||||
↓
|
||||
затем compatibility facade
|
||||
↓
|
||||
старый production-код продолжает работать
|
||||
↓
|
||||
последующая миграция потребителей выполняется отдельно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что не изменялось
|
||||
|
||||
В рамках Build 016 намеренно не изменялись:
|
||||
|
||||
- публичный контракт `ExchangeService`;
|
||||
- сигнатуры `normalize_symbol()`;
|
||||
- сигнатуры `symbol_candidates()`;
|
||||
- логика `validate_symbol()`;
|
||||
- формат `SymbolValidationResult`;
|
||||
- порядок кандидатов;
|
||||
- алгоритм сопоставления символов;
|
||||
- поведение mock mode;
|
||||
- production-поведение работающего бота.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
После Build 016 ответственность распределена следующим образом:
|
||||
|
||||
```text
|
||||
market_data/acquisition/symbols.py
|
||||
│
|
||||
└── каноническая логика нормализации
|
||||
и формирования кандидатов символа
|
||||
|
||||
integrations/exchange/symbol_utils.py
|
||||
│
|
||||
└── compatibility facade для legacy-кода
|
||||
|
||||
integrations/exchange/service.py
|
||||
│
|
||||
└── существующий production consumer
|
||||
```
|
||||
|
||||
Таким образом, новая подсистема получила владение логикой идентификации торговых символов без нарушения существующих зависимостей.
|
||||
|
||||
---
|
||||
|
||||
## Итоговый статус
|
||||
|
||||
Все критерии Build 016 выполнены:
|
||||
|
||||
- каноническая реализация перенесена в `market_data`;
|
||||
- legacy import path сохранён;
|
||||
- дублирование реализации устранено;
|
||||
- существующий контракт зафиксирован тестами;
|
||||
- compatibility facade проверен;
|
||||
- identity функций подтверждена;
|
||||
- компиляция успешна;
|
||||
- полный regression suite успешен;
|
||||
- production-поведение не изменено.
|
||||
|
||||
```text
|
||||
BUILD 016 — COMPLETE
|
||||
```
|
||||
564
docs/migrations/build_017.md
Normal file
564
docs/migrations/build_017.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# Build 017 — Переключение `validate_symbol()` на новый механизм разрешения символов
|
||||
|
||||
## Статус
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## Цель Build
|
||||
|
||||
Перевести существующий метод `ExchangeService.validate_symbol()` с legacy-алгоритма поиска инструмента на новый канонический механизм разрешения символов, расположенный в подсистеме `src/market_data/acquisition/`.
|
||||
|
||||
При этом необходимо сохранить без изменений существующий внешний контракт legacy-системы `SymbolValidationResult` и обеспечить полную обратную совместимость существующего бота.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный контекст
|
||||
|
||||
До Build 017 метод `ExchangeService.validate_symbol()` самостоятельно выполнял разрешение символа через `symbol_candidates()` и вложенный цикл по результату `get_exchange_symbols()`.
|
||||
|
||||
Фактически внутри `ExchangeService` находилась собственная логика поиска соответствующего торгового инструмента.
|
||||
|
||||
После завершения Build 016 канонические функции `normalize_symbol()` и `symbol_candidates()` уже были перенесены в:
|
||||
|
||||
src/market_data/acquisition/symbols.py
|
||||
|
||||
Build 017 продолжает этот переход и выносит непосредственно алгоритм разрешения символа в новую подсистему Market Data Acquisition.
|
||||
|
||||
---
|
||||
|
||||
## Реализованная архитектура
|
||||
|
||||
Каноническая логика работы с идентификаторами инструментов теперь находится в:
|
||||
|
||||
src/market_data/acquisition/symbols.py
|
||||
|
||||
Файл содержит:
|
||||
|
||||
normalize_symbol()
|
||||
symbol_candidates()
|
||||
resolve_symbol_index()
|
||||
|
||||
Распределение ответственности:
|
||||
|
||||
normalize_symbol()
|
||||
│
|
||||
▼
|
||||
Нормализация входного идентификатора
|
||||
│
|
||||
▼
|
||||
symbol_candidates()
|
||||
│
|
||||
▼
|
||||
Формирование упорядоченного набора кандидатов
|
||||
│
|
||||
▼
|
||||
resolve_symbol_index()
|
||||
│
|
||||
▼
|
||||
Поиск первого подходящего символа
|
||||
в доступной последовательности
|
||||
│
|
||||
▼
|
||||
ExchangeService.validate_symbol()
|
||||
│
|
||||
▼
|
||||
Формирование legacy SymbolValidationResult
|
||||
|
||||
Таким образом:
|
||||
|
||||
- `market_data/acquisition/symbols.py` отвечает за каноническую логику разрешения идентификатора;
|
||||
- `ExchangeService.validate_symbol()` отвечает за сохранение legacy API и формирование `SymbolValidationResult`;
|
||||
- `get_exchange_symbols()` остаётся compatibility boundary между новой моделью `Instrument` и legacy-моделью `ExchangeSymbol`.
|
||||
|
||||
---
|
||||
|
||||
## Новый канонический resolver
|
||||
|
||||
В файле:
|
||||
|
||||
src/market_data/acquisition/symbols.py
|
||||
|
||||
добавлена функция:
|
||||
|
||||
def resolve_symbol_index(
|
||||
raw_symbol: str,
|
||||
available_symbols: Sequence[str],
|
||||
) -> int | None:
|
||||
|
||||
Её ответственность:
|
||||
|
||||
1. Получить исходный идентификатор инструмента.
|
||||
2. Сформировать кандидаты через `symbol_candidates()`.
|
||||
3. Последовательно проверить кандидатов.
|
||||
4. Последовательно проверить доступные символы.
|
||||
5. Вернуть индекс первого совпавшего символа.
|
||||
6. Вернуть `None`, если соответствие отсутствует.
|
||||
|
||||
Возврат индекса, а не самого объекта, позволяет resolver оставаться независимым от:
|
||||
|
||||
ExchangeSymbol
|
||||
Instrument
|
||||
SymbolValidationResult
|
||||
ExchangeService
|
||||
|
||||
и работать только со строковыми идентификаторами.
|
||||
|
||||
---
|
||||
|
||||
## Сохранённый порядок разрешения
|
||||
|
||||
Build 017 сохраняет существующую семантику legacy-реализации.
|
||||
|
||||
Приоритет определяется в следующем порядке:
|
||||
|
||||
1. Порядок кандидатов из symbol_candidates()
|
||||
2. Порядок available_symbols
|
||||
3. Первое найденное совпадение
|
||||
|
||||
Это означает, что resolver сохраняет:
|
||||
|
||||
- приоритет исходного нормализованного значения;
|
||||
- приоритет декодированного варианта `%2F`;
|
||||
- приоритет варианта без внутренних пробелов;
|
||||
- порядок инструментов источника;
|
||||
- возврат первого совпадения при наличии дубликатов.
|
||||
|
||||
---
|
||||
|
||||
## Изменение `ExchangeService.validate_symbol()`
|
||||
|
||||
Метод:
|
||||
|
||||
ExchangeService.validate_symbol()
|
||||
|
||||
сохранил существующий публичный контракт:
|
||||
|
||||
def validate_symbol(
|
||||
self,
|
||||
raw_symbol: str,
|
||||
) -> SymbolValidationResult:
|
||||
|
||||
Сохраняются все существующие сценарии результата:
|
||||
|
||||
### Пустой символ
|
||||
|
||||
Возвращается:
|
||||
|
||||
SymbolValidationResult(
|
||||
requested_symbol=requested,
|
||||
normalized_symbol="",
|
||||
is_valid=False,
|
||||
message="Символ пустой.",
|
||||
symbol_info=None,
|
||||
)
|
||||
|
||||
### Mock mode
|
||||
|
||||
При отключённой реальной бирже возвращается успешный результат без `symbol_info`:
|
||||
|
||||
SymbolValidationResult(
|
||||
requested_symbol=requested,
|
||||
normalized_symbol=requested,
|
||||
is_valid=True,
|
||||
message="Mock mode active.",
|
||||
symbol_info=None,
|
||||
)
|
||||
|
||||
### Найденный символ
|
||||
|
||||
При успешном разрешении возвращается исходный объект `ExchangeSymbol` из списка `get_exchange_symbols()`:
|
||||
|
||||
SymbolValidationResult(
|
||||
requested_symbol=requested,
|
||||
normalized_symbol=normalize_symbol(symbol_info.symbol),
|
||||
is_valid=True,
|
||||
message="Символ найден в exchangeInfo.",
|
||||
symbol_info=symbol_info,
|
||||
)
|
||||
|
||||
### Символ не найден
|
||||
|
||||
Возвращается прежний отрицательный результат:
|
||||
|
||||
SymbolValidationResult(
|
||||
requested_symbol=requested,
|
||||
normalized_symbol=requested,
|
||||
is_valid=False,
|
||||
message=f"Символ '{requested}' не найден в exchangeInfo.",
|
||||
symbol_info=None,
|
||||
)
|
||||
|
||||
Таким образом, потребители `validate_symbol()` не требуют изменений.
|
||||
|
||||
---
|
||||
|
||||
## Новый поток данных
|
||||
|
||||
После Build 017 полный путь разрешения символа выглядит следующим образом:
|
||||
|
||||
raw_symbol
|
||||
│
|
||||
▼
|
||||
ExchangeService.validate_symbol()
|
||||
│
|
||||
▼
|
||||
normalize_symbol()
|
||||
│
|
||||
▼
|
||||
ExchangeService.get_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
tuple[Instrument, ...]
|
||||
│
|
||||
▼
|
||||
Compatibility mapper
|
||||
│
|
||||
▼
|
||||
list[ExchangeSymbol]
|
||||
│
|
||||
▼
|
||||
resolve_symbol_index()
|
||||
│
|
||||
▼
|
||||
matched index
|
||||
│
|
||||
▼
|
||||
исходный ExchangeSymbol
|
||||
│
|
||||
▼
|
||||
SymbolValidationResult
|
||||
|
||||
При этом `validate_symbol()`:
|
||||
|
||||
- не обращается напрямую к acquisition adapter;
|
||||
- не создаёт `InstrumentAcquisitionService`;
|
||||
- не выполняет REST-запрос;
|
||||
- не выполняет parsing;
|
||||
- не выполняет validation входного `exchangeInfo`;
|
||||
- не выполняет mapping `Instrument → ExchangeSymbol`;
|
||||
- не управляет cache;
|
||||
- использует только публичный legacy boundary `get_exchange_symbols()`.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение compatibility boundary
|
||||
|
||||
Build 017 намеренно не переводит `validate_symbol()` на прямую работу с `Instrument`.
|
||||
|
||||
Текущий compatibility boundary остаётся следующим:
|
||||
|
||||
Instrument Acquisition
|
||||
│
|
||||
▼
|
||||
tuple[Instrument, ...]
|
||||
│
|
||||
▼
|
||||
compatibility.py
|
||||
│
|
||||
▼
|
||||
list[ExchangeSymbol]
|
||||
│
|
||||
▼
|
||||
ExchangeService.get_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
ExchangeService.validate_symbol()
|
||||
│
|
||||
▼
|
||||
SymbolValidationResult
|
||||
|
||||
Это позволяет продолжать поэтапную миграцию без нарушения работы существующего бота.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение `SymbolValidationResult`
|
||||
|
||||
Legacy-модель:
|
||||
|
||||
src/integrations/exchange/models.py
|
||||
|
||||
остаётся без изменений.
|
||||
|
||||
Контракт:
|
||||
|
||||
class SymbolValidationResult:
|
||||
requested_symbol: str
|
||||
normalized_symbol: str
|
||||
is_valid: bool
|
||||
message: str
|
||||
symbol_info: ExchangeSymbol | None
|
||||
|
||||
сохранён полностью.
|
||||
|
||||
Это важно, поскольку `validate_symbol()` используется существующими runtime-компонентами, включая:
|
||||
|
||||
- получение статуса торгового инструмента;
|
||||
- получение комиссии;
|
||||
- получение свечей;
|
||||
- получение цены;
|
||||
- market snapshot;
|
||||
- execution snapshot;
|
||||
- свежий REST snapshot;
|
||||
- market stream;
|
||||
- market data runner;
|
||||
- Telegram handlers.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение object identity
|
||||
|
||||
При успешном разрешении символа `validate_symbol()` возвращает тот же экземпляр `ExchangeSymbol`, который находится в результате `get_exchange_symbols()`.
|
||||
|
||||
То есть сохраняется условие:
|
||||
|
||||
result.symbol_info is symbol
|
||||
|
||||
Это предотвращает:
|
||||
|
||||
- создание лишних копий legacy-моделей;
|
||||
- изменение object identity;
|
||||
- расхождение между cache и результатом validation;
|
||||
- скрытые изменения поведения существующих потребителей.
|
||||
|
||||
---
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
### `src/market_data/acquisition/symbols.py`
|
||||
|
||||
Добавлена каноническая функция:
|
||||
|
||||
resolve_symbol_index()
|
||||
|
||||
Функция выполняет независимое разрешение строкового идентификатора по последовательности доступных символов.
|
||||
|
||||
### `src/integrations/exchange/service.py`
|
||||
|
||||
Метод:
|
||||
|
||||
validate_symbol()
|
||||
|
||||
переключён со встроенного двойного цикла на:
|
||||
|
||||
resolve_symbol_index()
|
||||
|
||||
При этом сохранены:
|
||||
|
||||
- вызов `get_exchange_symbols()`;
|
||||
- `SymbolValidationResult`;
|
||||
- тексты сообщений;
|
||||
- mock mode;
|
||||
- порядок разрешения;
|
||||
- возврат исходного объекта `ExchangeSymbol`.
|
||||
|
||||
### `tests/unit/market_data/acquisition/test_symbols.py`
|
||||
|
||||
Добавлены unit-тесты для `resolve_symbol_index()`.
|
||||
|
||||
### `tests/unit/integrations/exchange/test_service_validate_symbol.py`
|
||||
|
||||
Добавлен отдельный набор unit-тестов для публичного legacy-контракта `ExchangeService.validate_symbol()`.
|
||||
|
||||
---
|
||||
|
||||
## Тестовое покрытие
|
||||
|
||||
### Канонический symbol resolver
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/test_symbols.py \
|
||||
-q
|
||||
|
||||
Результат:
|
||||
|
||||
41 passed in 0.02s
|
||||
|
||||
Проверены:
|
||||
|
||||
- точное совпадение;
|
||||
- регистронезависимое совпадение;
|
||||
- внешние пробелы;
|
||||
- `%2F`;
|
||||
- внутренние пробелы;
|
||||
- отсутствующий символ;
|
||||
- пустой запрос;
|
||||
- пустая последовательность доступных символов;
|
||||
- приоритет кандидатов;
|
||||
- порядок доступных символов;
|
||||
- первый дубликат;
|
||||
- отсутствие изменения входной последовательности.
|
||||
|
||||
---
|
||||
|
||||
## Тестирование `ExchangeService.validate_symbol()`
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py \
|
||||
-q
|
||||
|
||||
Результат:
|
||||
|
||||
16 passed in 0.08s
|
||||
|
||||
Проверены:
|
||||
|
||||
- отклонение пустого символа;
|
||||
- mock mode;
|
||||
- точное совпадение;
|
||||
- регистронезависимость;
|
||||
- внешние пробелы;
|
||||
- encoded separator `%2F`;
|
||||
- внутренние пробелы;
|
||||
- отсутствующий символ;
|
||||
- возврат исходного `ExchangeSymbol`;
|
||||
- нормализация фактически найденного символа;
|
||||
- сохранение success message;
|
||||
- единственный вызов `get_exchange_symbols()`;
|
||||
- отсутствие прямого обращения к acquisition;
|
||||
- сохранение candidate priority;
|
||||
- возврат первого duplicate;
|
||||
- сохранение типа `SymbolValidationResult`.
|
||||
|
||||
---
|
||||
|
||||
## Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/symbols.py \
|
||||
src/integrations/exchange/service.py \
|
||||
tests/unit/market_data/acquisition/test_symbols.py \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py
|
||||
|
||||
Результат:
|
||||
|
||||
Успешно.
|
||||
Ошибок компиляции нет.
|
||||
|
||||
---
|
||||
|
||||
## Полный regression suite
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m pytest -q
|
||||
|
||||
Результат:
|
||||
|
||||
299 passed in 0.17s
|
||||
|
||||
Регрессий в существующем проекте не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## Финальная архитектурная проверка
|
||||
|
||||
Выполнен поиск:
|
||||
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "validate_symbol|resolve_symbol_index|symbol_candidates|normalize_symbol|SymbolValidationResult|symbol_info" \
|
||||
src tests
|
||||
|
||||
Проверка подтвердила:
|
||||
|
||||
- каноническая реализация `normalize_symbol()` находится в `src/market_data/acquisition/symbols.py`;
|
||||
- каноническая реализация `symbol_candidates()` находится там же;
|
||||
- `resolve_symbol_index()` находится там же;
|
||||
- `ExchangeService.validate_symbol()` использует `resolve_symbol_index()`;
|
||||
- старого двойного цикла внутри `validate_symbol()` больше нет;
|
||||
- `ExchangeService` использует канонические функции новой подсистемы;
|
||||
- legacy facade `src/integrations/exchange/symbol_utils.py` сохранён;
|
||||
- `SymbolValidationResult` сохранён;
|
||||
- `symbol_info: ExchangeSymbol | None` сохранён;
|
||||
- новый cache не добавлен;
|
||||
- прямого обращения `validate_symbol()` к acquisition adapter нет;
|
||||
- дублирования алгоритма разрешения символа не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные гарантии после Build 017
|
||||
|
||||
После завершения Build 017 выполняются следующие гарантии:
|
||||
|
||||
1. Каноническая логика разрешения символов принадлежит `market_data/acquisition`.
|
||||
2. `ExchangeService.validate_symbol()` больше не содержит собственного алгоритма поиска совпадения.
|
||||
3. `validate_symbol()` продолжает использовать `get_exchange_symbols()` как compatibility boundary.
|
||||
4. Публичный контракт `SymbolValidationResult` не изменён.
|
||||
5. `symbol_info` продолжает иметь тип `ExchangeSymbol | None`.
|
||||
6. Object identity найденного `ExchangeSymbol` сохраняется.
|
||||
7. Порядок кандидатов сохраняется.
|
||||
8. Порядок доступных символов сохраняется.
|
||||
9. При дубликатах возвращается первый найденный объект.
|
||||
10. Новый cache не введён.
|
||||
11. Существующий `_exchange_symbols_cache` продолжает работать без изменений.
|
||||
12. Legacy facade `symbol_utils.py` сохранён.
|
||||
13. Все существующие тесты проекта проходят.
|
||||
|
||||
---
|
||||
|
||||
## Что намеренно не входит в Build 017
|
||||
|
||||
Build 017 не выполняет:
|
||||
|
||||
- удаление `SymbolValidationResult`;
|
||||
- перевод всех потребителей на `Instrument`;
|
||||
- удаление `ExchangeSymbol`;
|
||||
- удаление compatibility mapper;
|
||||
- удаление `_exchange_symbols_cache`;
|
||||
- прямую работу `validate_symbol()` с `tuple[Instrument, ...]`;
|
||||
- создание нового instrument cache;
|
||||
- изменение публичного API `ExchangeService`;
|
||||
- изменение runtime-логики бота;
|
||||
- изменение торговой логики.
|
||||
|
||||
Эти изменения должны выполняться отдельными контролируемыми Build-шагами.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
Build 017 завершён успешно.
|
||||
|
||||
Реализовано переключение:
|
||||
|
||||
ExchangeService.validate_symbol()
|
||||
│
|
||||
▼
|
||||
legacy inline symbol matching
|
||||
|
||||
на:
|
||||
|
||||
ExchangeService.validate_symbol()
|
||||
│
|
||||
▼
|
||||
market_data.acquisition.resolve_symbol_index()
|
||||
|
||||
При этом полностью сохранены:
|
||||
|
||||
- публичный legacy-контракт;
|
||||
- `SymbolValidationResult`;
|
||||
- `ExchangeSymbol`;
|
||||
- `symbol_info`;
|
||||
- object identity;
|
||||
- порядок разрешения;
|
||||
- mock mode;
|
||||
- compatibility boundary;
|
||||
- cache;
|
||||
- работа существующего бота.
|
||||
|
||||
Финальный результат:
|
||||
|
||||
BUILD 017 — COMPLETE
|
||||
|
||||
Полный regression suite:
|
||||
|
||||
299 passed in 0.17s
|
||||
589
docs/migrations/build_018.md
Normal file
589
docs/migrations/build_018.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# Build 018 — Переключение `get_symbol_runtime_status()`
|
||||
|
||||
## Статус
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Перевести определение торгового состояния инструмента, используемое методом:
|
||||
|
||||
```python
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
```
|
||||
|
||||
на каноническую классификацию статусов из подсистемы:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/
|
||||
```
|
||||
|
||||
при полном сохранении существующего внешнего контракта `ExchangeRuntimeStatus`, поведения legacy-кода, UI и runtime-потребителей.
|
||||
|
||||
---
|
||||
|
||||
## Исходное состояние
|
||||
|
||||
До Build 018 классификация биржевых статусов инструмента находилась непосредственно в legacy exchange-слое:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/status.py
|
||||
```
|
||||
|
||||
и основывалась на локальных наборах:
|
||||
|
||||
```python
|
||||
OPEN_STATUSES
|
||||
BREAK_STATUSES
|
||||
```
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
build_market_status_from_symbol_status()
|
||||
```
|
||||
|
||||
самостоятельно определял одно из состояний:
|
||||
|
||||
```text
|
||||
OPEN
|
||||
NOT_TRADABLE
|
||||
BREAK
|
||||
UNKNOWN
|
||||
```
|
||||
|
||||
Это означало, что предметная классификация торгового состояния инструмента оставалась внутри legacy exchange integration layer.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурное решение
|
||||
|
||||
Каноническая классификация статуса инструмента перенесена в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/status.py
|
||||
```
|
||||
|
||||
Добавлены следующие сущности:
|
||||
|
||||
```python
|
||||
InstrumentTradingState
|
||||
InstrumentStatusClassification
|
||||
classify_instrument_status()
|
||||
```
|
||||
|
||||
Теперь архитектурная цепочка имеет вид:
|
||||
|
||||
```text
|
||||
raw exchange status
|
||||
↓
|
||||
classify_instrument_status()
|
||||
↓
|
||||
InstrumentStatusClassification
|
||||
↓
|
||||
build_market_status_from_symbol_status()
|
||||
↓
|
||||
ExchangeRuntimeStatus
|
||||
↓
|
||||
legacy runtime / UI / trading consumers
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
- `market_data/acquisition` отвечает за предметную классификацию состояния инструмента;
|
||||
- `integrations/exchange/status.py` сохраняет compatibility-функцию преобразования результата в существующий `ExchangeRuntimeStatus`;
|
||||
- существующие runtime-потребители продолжают работать без изменения публичного контракта.
|
||||
|
||||
---
|
||||
|
||||
## Добавленный файл
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/status.py
|
||||
```
|
||||
|
||||
Файл содержит каноническую модель классификации торгового состояния инструмента.
|
||||
|
||||
Основные сущности:
|
||||
|
||||
```python
|
||||
class InstrumentTradingState(StrEnum):
|
||||
OPEN = "OPEN"
|
||||
NOT_TRADABLE = "NOT_TRADABLE"
|
||||
BREAK = "BREAK"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
```
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InstrumentStatusClassification:
|
||||
state: InstrumentTradingState
|
||||
normalized_status: str | None
|
||||
```
|
||||
|
||||
Основная функция:
|
||||
|
||||
```python
|
||||
def classify_instrument_status(
|
||||
raw_status: str | None,
|
||||
) -> InstrumentStatusClassification:
|
||||
```
|
||||
|
||||
Она выполняет:
|
||||
|
||||
1. нормализацию входного статуса;
|
||||
2. классификацию открытого рынка;
|
||||
3. классификацию неторгуемого инструмента;
|
||||
4. классификацию временной остановки торгов;
|
||||
5. возврат `UNKNOWN` для неизвестного или отсутствующего статуса.
|
||||
|
||||
---
|
||||
|
||||
## Канонические состояния
|
||||
|
||||
Подсистема `market_data/acquisition` различает четыре предметных состояния:
|
||||
|
||||
```text
|
||||
OPEN
|
||||
NOT_TRADABLE
|
||||
BREAK
|
||||
UNKNOWN
|
||||
```
|
||||
|
||||
### `OPEN`
|
||||
|
||||
Инструмент доступен для торговли.
|
||||
|
||||
Поддерживаются существующие legacy-статусы:
|
||||
|
||||
```text
|
||||
TRADING
|
||||
OPEN
|
||||
ACTIVE
|
||||
ENABLED
|
||||
ONLINE
|
||||
```
|
||||
|
||||
### `NOT_TRADABLE`
|
||||
|
||||
Инструмент существует, но недоступен для обычной торговли.
|
||||
|
||||
Поддерживаются:
|
||||
|
||||
```text
|
||||
NOT_TRADABLE
|
||||
TRADING_DISABLED
|
||||
MARKET_DISABLED
|
||||
UNAVAILABLE_FOR_TRADING
|
||||
CLOSE_ONLY
|
||||
REDUCE_ONLY
|
||||
VIEW_ONLY
|
||||
```
|
||||
|
||||
### `BREAK`
|
||||
|
||||
Торги временно остановлены или приостановлены.
|
||||
|
||||
Поддерживаются:
|
||||
|
||||
```text
|
||||
BREAK
|
||||
CLOSED
|
||||
HALT
|
||||
HALTED
|
||||
PAUSED
|
||||
SUSPENDED
|
||||
DISABLED
|
||||
SETTLING
|
||||
POST_ONLY
|
||||
```
|
||||
|
||||
### `UNKNOWN`
|
||||
|
||||
Используется для:
|
||||
|
||||
- неизвестного статуса;
|
||||
- пустой строки;
|
||||
- `None`;
|
||||
- значения, отсутствующего в известных классификационных наборах.
|
||||
|
||||
---
|
||||
|
||||
## Изменение legacy exchange-слоя
|
||||
|
||||
Из файла:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/status.py
|
||||
```
|
||||
|
||||
удалена собственная предметная классификация через публичные наборы:
|
||||
|
||||
```python
|
||||
OPEN_STATUSES
|
||||
BREAK_STATUSES
|
||||
```
|
||||
|
||||
Вместо неё используются:
|
||||
|
||||
```python
|
||||
from src.market_data.acquisition.models.status import (
|
||||
InstrumentTradingState,
|
||||
classify_instrument_status,
|
||||
)
|
||||
```
|
||||
|
||||
Функция:
|
||||
|
||||
```python
|
||||
build_market_status_from_symbol_status()
|
||||
```
|
||||
|
||||
теперь сначала вызывает:
|
||||
|
||||
```python
|
||||
classification = classify_instrument_status(raw_status)
|
||||
```
|
||||
|
||||
а затем преобразует каноническое состояние в существующий legacy-контракт:
|
||||
|
||||
```text
|
||||
InstrumentTradingState.OPEN
|
||||
↓
|
||||
ExchangeRuntimeStatus(code=OPEN)
|
||||
|
||||
InstrumentTradingState.NOT_TRADABLE
|
||||
↓
|
||||
ExchangeRuntimeStatus(code=BREAK, reason=market_not_tradable)
|
||||
|
||||
InstrumentTradingState.BREAK
|
||||
↓
|
||||
ExchangeRuntimeStatus(code=BREAK, reason=market_break)
|
||||
|
||||
InstrumentTradingState.UNKNOWN
|
||||
↓
|
||||
ExchangeRuntimeStatus(code=UNKNOWN, reason=market_status_unknown)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сохранённый публичный контракт
|
||||
|
||||
Build 018 не изменяет структуру:
|
||||
|
||||
```python
|
||||
ExchangeRuntimeStatus
|
||||
```
|
||||
|
||||
Сохранены поля:
|
||||
|
||||
```text
|
||||
code
|
||||
is_open
|
||||
is_available
|
||||
is_auth_ok
|
||||
title
|
||||
message
|
||||
ui_line
|
||||
reason
|
||||
symbol
|
||||
raw_status
|
||||
raw_error
|
||||
```
|
||||
|
||||
Также сохранён compatibility-метод:
|
||||
|
||||
```python
|
||||
ExchangeRuntimeStatus.as_dict()
|
||||
```
|
||||
|
||||
Это позволяет не изменять существующие runtime-, UI- и trading-потребители.
|
||||
|
||||
---
|
||||
|
||||
## Поведение `get_symbol_runtime_status()`
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
```
|
||||
|
||||
сохранил существующий внешний контракт и последовательность обработки.
|
||||
|
||||
Архитектурно поток остаётся следующим:
|
||||
|
||||
```text
|
||||
requested symbol
|
||||
↓
|
||||
validate_symbol()
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
raw symbol status
|
||||
↓
|
||||
build_market_status_from_symbol_status()
|
||||
↓
|
||||
classify_instrument_status()
|
||||
↓
|
||||
ExchangeRuntimeStatus
|
||||
```
|
||||
|
||||
Для открытого рынка дополнительно сохраняется проверка свежести рыночного snapshot:
|
||||
|
||||
```text
|
||||
OPEN
|
||||
↓
|
||||
get_fresh_market_snapshot()
|
||||
↓
|
||||
age_seconds > 60
|
||||
↓
|
||||
STALE_MARKET_DATA / BREAK
|
||||
```
|
||||
|
||||
Stale threshold сохранён:
|
||||
|
||||
```text
|
||||
60 секунд
|
||||
```
|
||||
|
||||
Проверка stale market data выполняется только для рынка, первоначально классифицированного как `OPEN`.
|
||||
|
||||
---
|
||||
|
||||
## Сохранённое поведение
|
||||
|
||||
Build 018 сохраняет следующие legacy-сценарии:
|
||||
|
||||
- mock exchange;
|
||||
- явный `symbol`;
|
||||
- использование `default_symbol`, если аргумент равен `None`;
|
||||
- invalid symbol;
|
||||
- ошибка при validation;
|
||||
- `OPEN`;
|
||||
- `NOT_TRADABLE`;
|
||||
- `BREAK`;
|
||||
- `UNKNOWN`;
|
||||
- stale market data;
|
||||
- отсутствие `age_seconds`;
|
||||
- ошибка получения snapshot;
|
||||
- нормализованный matched symbol;
|
||||
- существующий `ExchangeRuntimeStatus`;
|
||||
- существующие `reason`;
|
||||
- существующие UI-тексты;
|
||||
- существующие `raw_status`.
|
||||
|
||||
---
|
||||
|
||||
## Кэширование и сетевые запросы
|
||||
|
||||
Build 018 не добавляет:
|
||||
|
||||
- новый cache;
|
||||
- новый REST-запрос;
|
||||
- дополнительную загрузку `exchangeInfo`;
|
||||
- дополнительный acquisition service;
|
||||
- отдельный status feed runtime.
|
||||
|
||||
`get_symbol_runtime_status()` продолжает использовать существующий путь:
|
||||
|
||||
```text
|
||||
validate_symbol()
|
||||
↓
|
||||
get_exchange_symbols()
|
||||
↓
|
||||
existing ExchangeService cache
|
||||
↓
|
||||
Instrument Acquisition path
|
||||
```
|
||||
|
||||
Таким образом, миграция не создаёт параллельного источника Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
## Пустые status feed-файлы
|
||||
|
||||
На момент Build 018 следующие файлы существуют, но остаются пустыми:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/status.py
|
||||
src/market_data/acquisition/handlers/status_handler.py
|
||||
src/market_data/acquisition/feeds/status_feed.py
|
||||
```
|
||||
|
||||
После Build 018 файл:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/status.py
|
||||
```
|
||||
|
||||
получил реализацию канонической классификации торгового состояния инструмента.
|
||||
|
||||
Файлы:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/handlers/status_handler.py
|
||||
src/market_data/acquisition/feeds/status_feed.py
|
||||
```
|
||||
|
||||
в рамках Build 018 намеренно не реализуются.
|
||||
|
||||
Причина: текущая задача не создаёт отдельный Status Feed и не должна вводить новый источник сетевых запросов или параллельный runtime-путь.
|
||||
|
||||
---
|
||||
|
||||
## Добавленные тесты
|
||||
|
||||
Добавлен тестовый файл:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/models/test_instrument_status.py
|
||||
```
|
||||
|
||||
Он проверяет каноническую классификацию:
|
||||
|
||||
- все открытые статусы;
|
||||
- все неторгуемые статусы;
|
||||
- все break-статусы;
|
||||
- регистронезависимость;
|
||||
- удаление внешних пробелов;
|
||||
- неизвестный статус;
|
||||
- пустой статус;
|
||||
- `None`;
|
||||
- immutable-контракт результата.
|
||||
|
||||
Также расширено покрытие:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_status.py
|
||||
```
|
||||
|
||||
для проверки compatibility-преобразования:
|
||||
|
||||
```text
|
||||
InstrumentStatusClassification
|
||||
↓
|
||||
ExchangeRuntimeStatus
|
||||
```
|
||||
|
||||
Целевой regression-набор метода находится в:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py
|
||||
```
|
||||
|
||||
Дублирующий тестовый файл:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_runtime_status.py
|
||||
```
|
||||
|
||||
удалён.
|
||||
|
||||
---
|
||||
|
||||
## Проверка целевого runtime-контракта
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
34 passed in 0.09s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/models/status.py \
|
||||
src/integrations/exchange/status.py \
|
||||
src/integrations/exchange/service.py \
|
||||
tests/unit/market_data/acquisition/models/test_instrument_status.py \
|
||||
tests/unit/integrations/exchange/test_status.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Успешно.
|
||||
Ошибок компиляции нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полный regression suite
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
387 passed in 0.20s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
После Build 018 ответственность разделена следующим образом:
|
||||
|
||||
```text
|
||||
market_data/acquisition/models/status.py
|
||||
↓
|
||||
каноническая предметная классификация статуса инструмента
|
||||
|
||||
integrations/exchange/status.py
|
||||
↓
|
||||
compatibility mapping в legacy ExchangeRuntimeStatus
|
||||
|
||||
integrations/exchange/service.py
|
||||
↓
|
||||
runtime orchestration, validation и stale market data check
|
||||
|
||||
runtime / UI / trading consumers
|
||||
↓
|
||||
продолжают использовать существующий ExchangeRuntimeStatus
|
||||
```
|
||||
|
||||
В результате:
|
||||
|
||||
- предметная классификация статуса инструмента больше не принадлежит legacy exchange-слою;
|
||||
- дублирование `OPEN_STATUSES` / `BREAK_STATUSES` устранено;
|
||||
- `ExchangeRuntimeStatus` сохранён как compatibility boundary;
|
||||
- существующие потребители не требуют массового рефакторинга;
|
||||
- новый REST-путь не создан;
|
||||
- новый cache не создан;
|
||||
- поведение работающего бота сохранено.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
```text
|
||||
BUILD 018 — COMPLETE
|
||||
```
|
||||
|
||||
Build 018 завершён при полном прохождении целевых тестов, компиляции и общего regression suite:
|
||||
|
||||
```text
|
||||
34 targeted tests passed
|
||||
387 total tests passed
|
||||
```
|
||||
618
docs/migrations/build_019.md
Normal file
618
docs/migrations/build_019.md
Normal file
@@ -0,0 +1,618 @@
|
||||
# Build 019 — Подготовка переноса кэша в Storage
|
||||
|
||||
## Статус
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Подготовить независимый storage-контракт для хранения канонического справочника инструментов перед последующим переносом legacy-кэша:
|
||||
|
||||
```python
|
||||
ExchangeService._exchange_symbols_cache
|
||||
```
|
||||
|
||||
из слоя:
|
||||
|
||||
```text
|
||||
src/integrations/exchange
|
||||
```
|
||||
|
||||
в слой:
|
||||
|
||||
```text
|
||||
src/storage
|
||||
```
|
||||
|
||||
без изменения текущего production-пути и без нарушения работы существующего бота.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный принцип
|
||||
|
||||
До Build 019 кэш справочника торговых инструментов находился непосредственно внутри legacy-интеграционного сервиса:
|
||||
|
||||
```python
|
||||
ExchangeService._exchange_symbols_cache
|
||||
```
|
||||
|
||||
Это создаёт архитектурную связь между:
|
||||
|
||||
- получением Instrument Reference Data;
|
||||
- legacy-моделью `ExchangeSymbol`;
|
||||
- интеграционным слоем биржи;
|
||||
- runtime-хранением загруженного справочника.
|
||||
|
||||
В новой архитектуре ответственность разделяется:
|
||||
|
||||
```text
|
||||
Market Data Acquisition
|
||||
│
|
||||
▼
|
||||
tuple[Instrument, ...]
|
||||
│
|
||||
▼
|
||||
Storage
|
||||
│
|
||||
▼
|
||||
Compatibility Layer
|
||||
│
|
||||
▼
|
||||
Legacy ExchangeSymbol
|
||||
```
|
||||
|
||||
Build 019 создаёт только новый storage-контракт и его in-memory реализацию.
|
||||
|
||||
Переключение production-кода на новый store в рамках Build 019 не выполняется.
|
||||
|
||||
---
|
||||
|
||||
## Созданные файлы
|
||||
|
||||
```text
|
||||
app/src/storage/exceptions.py
|
||||
app/src/storage/instrument_store.py
|
||||
app/tests/unit/storage/test_instrument_store.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `src/storage/exceptions.py`
|
||||
|
||||
Создана базовая иерархия ошибок storage-слоя:
|
||||
|
||||
```text
|
||||
Exception
|
||||
│
|
||||
▼
|
||||
StorageError
|
||||
│
|
||||
▼
|
||||
InstrumentStoreError
|
||||
```
|
||||
|
||||
### `StorageError`
|
||||
|
||||
Базовая ошибка storage-слоя.
|
||||
|
||||
### `InstrumentStoreError`
|
||||
|
||||
Специализированная ошибка операций и нарушений контракта хранилища справочника инструментов.
|
||||
|
||||
---
|
||||
|
||||
## `src/storage/instrument_store.py`
|
||||
|
||||
Созданы:
|
||||
|
||||
```text
|
||||
InstrumentStoreProtocol
|
||||
InMemoryInstrumentStore
|
||||
```
|
||||
|
||||
### `InstrumentStoreProtocol`
|
||||
|
||||
Определяет независимый контракт runtime-хранилища канонического справочника инструментов.
|
||||
|
||||
Поддерживаемые операции:
|
||||
|
||||
```python
|
||||
get(
|
||||
source_name: str,
|
||||
) -> tuple[Instrument, ...] | None
|
||||
```
|
||||
|
||||
```python
|
||||
set(
|
||||
source_name: str,
|
||||
instruments: tuple[Instrument, ...],
|
||||
) -> None
|
||||
```
|
||||
|
||||
```python
|
||||
clear(
|
||||
source_name: str | None = None,
|
||||
) -> None
|
||||
```
|
||||
|
||||
Контракт не зависит от:
|
||||
|
||||
- `ExchangeService`;
|
||||
- `ExchangeSymbol`;
|
||||
- Dzengi REST API;
|
||||
- PostgreSQL;
|
||||
- Redis;
|
||||
- Telegram UI;
|
||||
- legacy compatibility mapper.
|
||||
|
||||
---
|
||||
|
||||
## Семантика `get()`
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
get(source_name)
|
||||
```
|
||||
|
||||
возвращает:
|
||||
|
||||
```text
|
||||
None
|
||||
```
|
||||
|
||||
если данные для источника никогда не сохранялись.
|
||||
|
||||
Это означает:
|
||||
|
||||
```text
|
||||
cache miss
|
||||
```
|
||||
|
||||
Если в store был успешно сохранён пустой справочник:
|
||||
|
||||
```python
|
||||
()
|
||||
```
|
||||
|
||||
метод возвращает именно:
|
||||
|
||||
```python
|
||||
()
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
```text
|
||||
None != ()
|
||||
```
|
||||
|
||||
и состояния:
|
||||
|
||||
```text
|
||||
данные отсутствуют
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
успешно загружен пустой справочник
|
||||
```
|
||||
|
||||
не смешиваются.
|
||||
|
||||
---
|
||||
|
||||
## Семантика `set()`
|
||||
|
||||
Метод принимает исключительно:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Это сохраняет immutable-контракт новой модели Instrument Reference Data.
|
||||
|
||||
Store не выполняет:
|
||||
|
||||
- копирование tuple;
|
||||
- сортировку;
|
||||
- преобразование элементов;
|
||||
- mapping в `ExchangeSymbol`;
|
||||
- нормализацию `Instrument`;
|
||||
- изменение порядка элементов.
|
||||
|
||||
Сохраняется исходный объект tuple.
|
||||
|
||||
Следовательно:
|
||||
|
||||
```python
|
||||
store.set("dzengi", instruments)
|
||||
|
||||
assert store.get("dzengi") is instruments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Семантика `clear()`
|
||||
|
||||
Поддерживаются два режима.
|
||||
|
||||
Очистка конкретного источника:
|
||||
|
||||
```python
|
||||
store.clear("dzengi")
|
||||
```
|
||||
|
||||
Полная очистка store:
|
||||
|
||||
```python
|
||||
store.clear()
|
||||
```
|
||||
|
||||
Очистка неизвестного источника является идемпотентной и не вызывает ошибку.
|
||||
|
||||
---
|
||||
|
||||
## Изоляция источников
|
||||
|
||||
Store поддерживает независимое хранение нескольких источников:
|
||||
|
||||
```text
|
||||
dzengi
|
||||
secondary
|
||||
other-source
|
||||
```
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
InMemoryInstrumentStore
|
||||
├── dzengi
|
||||
│ └── tuple[Instrument, ...]
|
||||
│
|
||||
└── secondary
|
||||
└── tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Изменение или очистка одного источника не влияет на остальные.
|
||||
|
||||
---
|
||||
|
||||
## Нормализация имени источника
|
||||
|
||||
Внешние пробелы удаляются:
|
||||
|
||||
```text
|
||||
" dzengi "
|
||||
```
|
||||
|
||||
нормализуется в:
|
||||
|
||||
```text
|
||||
"dzengi"
|
||||
```
|
||||
|
||||
Регистр сохраняется.
|
||||
|
||||
Следовательно:
|
||||
|
||||
```text
|
||||
dzengi
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
DZENGI
|
||||
```
|
||||
|
||||
являются разными ключами.
|
||||
|
||||
Пустые имена источников запрещены:
|
||||
|
||||
```text
|
||||
""
|
||||
" "
|
||||
" "
|
||||
"\t"
|
||||
"\n"
|
||||
```
|
||||
|
||||
и приводят к:
|
||||
|
||||
```python
|
||||
InstrumentStoreError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runtime-валидация
|
||||
|
||||
`InMemoryInstrumentStore.set()` проверяет:
|
||||
|
||||
1. что набор передан как `tuple`;
|
||||
2. что каждый элемент является экземпляром `Instrument`.
|
||||
|
||||
Нарушение контракта приводит к:
|
||||
|
||||
```python
|
||||
InstrumentStoreError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что намеренно не изменялось
|
||||
|
||||
Build 019 не изменяет:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/market_data/acquisition/service.py
|
||||
app/src/market_data/acquisition/compatibility.py
|
||||
app/src/storage/session.py
|
||||
app/src/storage/schema.py
|
||||
app/src/storage/models.py
|
||||
app/src/storage/repositories/*
|
||||
app/src/storage/__init__.py
|
||||
```
|
||||
|
||||
Не добавлялись:
|
||||
|
||||
- PostgreSQL-таблицы;
|
||||
- Redis;
|
||||
- новый database repository;
|
||||
- dependency injection в `ExchangeService`;
|
||||
- production singleton store;
|
||||
- глобальный storage registry.
|
||||
|
||||
---
|
||||
|
||||
## Legacy-кэш
|
||||
|
||||
После Build 019 legacy-кэш остаётся на прежнем месте:
|
||||
|
||||
```python
|
||||
class ExchangeService:
|
||||
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
|
||||
```
|
||||
|
||||
Текущий production-путь остаётся неизменным:
|
||||
|
||||
```text
|
||||
ExchangeService.get_exchange_symbols()
|
||||
│
|
||||
├── cache hit
|
||||
│ │
|
||||
│ ▼
|
||||
│ _exchange_symbols_cache
|
||||
│
|
||||
└── cache miss
|
||||
│
|
||||
▼
|
||||
_load_exchange_symbols_via_acquisition()
|
||||
│
|
||||
▼
|
||||
InstrumentAcquisitionService
|
||||
│
|
||||
▼
|
||||
tuple[Instrument, ...]
|
||||
│
|
||||
▼
|
||||
map_instruments_to_exchange_symbols()
|
||||
│
|
||||
▼
|
||||
list[ExchangeSymbol]
|
||||
│
|
||||
▼
|
||||
_exchange_symbols_cache
|
||||
```
|
||||
|
||||
Новый `InMemoryInstrumentStore` в этот production-путь пока не подключён.
|
||||
|
||||
---
|
||||
|
||||
## Тестовое покрытие
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/storage/test_instrument_store.py
|
||||
```
|
||||
|
||||
Проверены:
|
||||
|
||||
- соответствие `InstrumentStoreProtocol`;
|
||||
- cache miss;
|
||||
- сохранение и получение данных;
|
||||
- сохранение identity исходного tuple;
|
||||
- различие между `None` и пустым tuple;
|
||||
- изоляция разных источников;
|
||||
- очистка одного источника;
|
||||
- полная очистка store;
|
||||
- замена ранее сохранённого значения;
|
||||
- нормализация внешних пробелов имени источника;
|
||||
- сохранение регистра имени источника;
|
||||
- отклонение пустых имён источников;
|
||||
- отклонение списка вместо tuple;
|
||||
- отклонение объектов, не являющихся `Instrument`;
|
||||
- сохранение порядка инструментов;
|
||||
- отсутствие изменения входного tuple;
|
||||
- изоляция разных экземпляров store;
|
||||
- идемпотентная очистка неизвестного источника;
|
||||
- наследование `InstrumentStoreError` от `StorageError`.
|
||||
|
||||
---
|
||||
|
||||
## Результаты проверок
|
||||
|
||||
### Unit-тесты нового store
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/storage/test_instrument_store.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
32 passed in 0.02s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проверка компиляции
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/storage/exceptions.py \
|
||||
src/storage/instrument_store.py \
|
||||
tests/unit/storage/test_instrument_store.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Полный regression suite
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
419 passed in 0.21s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Архитектурная проверка
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "InstrumentStoreProtocol|InMemoryInstrumentStore|InstrumentStoreError|StorageError|_exchange_symbols_cache|instrument_store" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Проверка подтвердила:
|
||||
|
||||
- `InstrumentStoreProtocol` определён в `src/storage/instrument_store.py`;
|
||||
- `InMemoryInstrumentStore` определён в `src/storage/instrument_store.py`;
|
||||
- `StorageError` и `InstrumentStoreError` находятся в `src/storage/exceptions.py`;
|
||||
- новый store используется только собственными unit-тестами;
|
||||
- production-код на новый store не переключён;
|
||||
- `_exchange_symbols_cache` остаётся в `ExchangeService`;
|
||||
- существующие legacy-тесты кэша продолжают работать.
|
||||
|
||||
---
|
||||
|
||||
## Итоговая архитектура после Build 019
|
||||
|
||||
```text
|
||||
External Exchange API
|
||||
│
|
||||
▼
|
||||
Market Data Acquisition
|
||||
│
|
||||
▼
|
||||
tuple[Instrument, ...]
|
||||
│
|
||||
├──────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
InMemoryInstrumentStore Compatibility Layer
|
||||
│ │
|
||||
│ ▼
|
||||
│ list[ExchangeSymbol]
|
||||
│ │
|
||||
│ ▼
|
||||
│ ExchangeService._exchange_symbols_cache
|
||||
│
|
||||
▼
|
||||
готов к будущему подключению
|
||||
в production-путь
|
||||
```
|
||||
|
||||
На текущем этапе новый store существует независимо от legacy-кэша.
|
||||
|
||||
---
|
||||
|
||||
## Граница Build 019
|
||||
|
||||
Build 019 считается завершённым, потому что:
|
||||
|
||||
1. создан независимый storage-контракт для `Instrument`;
|
||||
2. создана in-memory реализация store;
|
||||
3. сохранена семантика immutable `tuple[Instrument, ...]`;
|
||||
4. определено различие между cache miss и пустым справочником;
|
||||
5. обеспечена изоляция источников;
|
||||
6. добавлена специализированная иерархия storage-ошибок;
|
||||
7. production-код не изменён;
|
||||
8. legacy-кэш не удалён;
|
||||
9. полный regression suite проходит успешно.
|
||||
|
||||
---
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
Следующий логический этап:
|
||||
|
||||
```text
|
||||
Build 020 — Подключение Instrument Store к production-пути
|
||||
```
|
||||
|
||||
Цель следующего этапа:
|
||||
|
||||
```text
|
||||
переключить хранение канонического tuple[Instrument, ...]
|
||||
с legacy-кэша ExchangeService
|
||||
на InMemoryInstrumentStore
|
||||
```
|
||||
|
||||
при сохранении внешнего legacy-контракта:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols() -> list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
и без нарушения работы существующего бота.
|
||||
|
||||
На Build 020 необходимо отдельно определить:
|
||||
|
||||
- где создаётся production-экземпляр `InMemoryInstrumentStore`;
|
||||
- как `ExchangeService` получает доступ к нему;
|
||||
- сохраняется ли временно `_exchange_symbols_cache` как compatibility-кэш;
|
||||
- в какой точке выполняется mapping `Instrument -> ExchangeSymbol`;
|
||||
- как сохранить существующую identity-семантику `get_exchange_symbols()`;
|
||||
- как обеспечить безопасный rollback без изменения внешнего API.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
**Build 019 завершён успешно.**
|
||||
|
||||
Новый storage-контракт создан и полностью покрыт unit-тестами.
|
||||
|
||||
Существующий бот продолжает использовать прежний production-путь без изменений.
|
||||
|
||||
Следующий этап — **Build 020: безопасное подключение `InstrumentStore` к production-пути с сохранением legacy-совместимости**.
|
||||
863
docs/migrations/build_020.md
Normal file
863
docs/migrations/build_020.md
Normal file
@@ -0,0 +1,863 @@
|
||||
# Build 020 — Перенос кэша инструментов в Storage Layer
|
||||
|
||||
**Engineering Migration Record**
|
||||
|
||||
---
|
||||
|
||||
## Контроль документа
|
||||
|
||||
| Свойство | Значение |
|
||||
|---|---|
|
||||
| Документ | Build 020 — Перенос кэша инструментов в Storage Layer |
|
||||
| Тип документа | Engineering Migration Record |
|
||||
| Статус | **Complete** |
|
||||
| Проект | Dzentra |
|
||||
| Подсистема | Instrument Reference Data |
|
||||
| Build | 020 |
|
||||
| Язык | Русский |
|
||||
| Целевой файл | `docs/migrations/instrument_reference_data/build_020.md` |
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 020
|
||||
|
||||
Цель Build 020 — удалить каноническое хранение справочных данных инструментов из legacy-кэша `ExchangeService._exchange_symbols_cache` и перенести его в специализированный Storage Layer.
|
||||
|
||||
До выполнения Build 020 `ExchangeService` одновременно отвечал за:
|
||||
|
||||
- получение справочных данных инструментов;
|
||||
- запуск acquisition pipeline;
|
||||
- преобразование канонических моделей `Instrument` в legacy-модели `ExchangeSymbol`;
|
||||
- хранение результата в собственном class-level cache.
|
||||
|
||||
Это создавало архитектурную зависимость канонических справочных данных от legacy exchange layer.
|
||||
|
||||
После Build 020 канонические данные инструментов хранятся в специализированном:
|
||||
|
||||
```text
|
||||
InMemoryInstrumentStore
|
||||
```
|
||||
|
||||
в виде:
|
||||
|
||||
```text
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Legacy-модели `ExchangeSymbol` больше не являются каноническим представлением справочных данных.
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектурное состояние до Build 020
|
||||
|
||||
До миграции `ExchangeService` содержал:
|
||||
|
||||
```python
|
||||
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
|
||||
```
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
работал по следующей схеме:
|
||||
|
||||
```text
|
||||
get_exchange_symbols()
|
||||
↓
|
||||
_exchange_symbols_cache
|
||||
↓ cache miss
|
||||
_load_exchange_symbols_via_acquisition()
|
||||
↓
|
||||
Instrument Acquisition Pipeline
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
map_instruments_to_exchange_symbols()
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
↓
|
||||
_exchange_symbols_cache
|
||||
```
|
||||
|
||||
Таким образом, результат новой Instrument Reference Data pipeline сразу преобразовывался в legacy-модель, и именно legacy-представление сохранялось как основной кэш.
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектурная проблема
|
||||
|
||||
Старый подход имел несколько принципиальных недостатков.
|
||||
|
||||
### 3.1. Legacy-модель использовалась как каноническое хранилище
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
содержит полные справочные данные инструмента.
|
||||
|
||||
Legacy-модель:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
является сокращённой compatibility-проекцией и содержит только часть этих данных.
|
||||
|
||||
Хранение только `ExchangeSymbol` означало потерю полноценного канонического представления после завершения acquisition pipeline.
|
||||
|
||||
### 3.2. Exchange Layer владел состоянием справочных данных
|
||||
|
||||
Поле:
|
||||
|
||||
```python
|
||||
ExchangeService._exchange_symbols_cache
|
||||
```
|
||||
|
||||
делало `ExchangeService` владельцем справочных данных инструментов.
|
||||
|
||||
Это противоречило целевой архитектуре:
|
||||
|
||||
```text
|
||||
Acquisition Layer
|
||||
↓
|
||||
Canonical Instrument Models
|
||||
↓
|
||||
Storage Layer
|
||||
↓
|
||||
Consumers / Compatibility Projections
|
||||
```
|
||||
|
||||
### 3.3. Канонические данные и legacy-проекция были объединены
|
||||
|
||||
Результат acquisition pipeline немедленно преобразовывался:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
и сохранялся только после преобразования.
|
||||
|
||||
Это не позволяло независимо использовать полный `Instrument` в будущих подсистемах Dzentra.
|
||||
|
||||
---
|
||||
|
||||
## 4. Реализованное архитектурное решение
|
||||
|
||||
В Build 020 введено разделение между:
|
||||
|
||||
1. каноническим хранилищем инструментов;
|
||||
2. legacy compatibility projection cache.
|
||||
|
||||
Теперь `ExchangeService` использует:
|
||||
|
||||
```python
|
||||
_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore()
|
||||
```
|
||||
|
||||
для хранения канонических моделей:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
и отдельный:
|
||||
|
||||
```python
|
||||
_exchange_symbols_projection_cache: list[ExchangeSymbol] | None = None
|
||||
```
|
||||
|
||||
для временного кэширования legacy-проекции.
|
||||
|
||||
Итоговая схема:
|
||||
|
||||
```text
|
||||
DzengiInstrumentDocumentSource
|
||||
↓
|
||||
InstrumentFeed
|
||||
↓
|
||||
DzengiInstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentAcquisitionService
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
InMemoryInstrumentStore
|
||||
↓
|
||||
Canonical Instrument Reference Data
|
||||
↓
|
||||
map_instruments_to_exchange_symbols()
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
↓
|
||||
Legacy Compatibility Projection Cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Каноническое хранилище
|
||||
|
||||
Канонические справочные данные теперь находятся в:
|
||||
|
||||
```python
|
||||
ExchangeService._instrument_store
|
||||
```
|
||||
|
||||
Тип зависимости:
|
||||
|
||||
```python
|
||||
InstrumentStoreProtocol
|
||||
```
|
||||
|
||||
Текущая реализация:
|
||||
|
||||
```python
|
||||
InMemoryInstrumentStore
|
||||
```
|
||||
|
||||
Store хранит:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Ключ источника:
|
||||
|
||||
```text
|
||||
dzengi
|
||||
```
|
||||
|
||||
Таким образом, канонические справочные данные больше не зависят от legacy-модели `ExchangeSymbol`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Новый production flow
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
сохраняет прежний внешний контракт:
|
||||
|
||||
```python
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Это необходимо для сохранения работоспособности существующего бота во время поэтапной миграции.
|
||||
|
||||
Внутренний flow теперь выглядит следующим образом:
|
||||
|
||||
```text
|
||||
get_exchange_symbols()
|
||||
↓
|
||||
Проверка exchange_enabled
|
||||
↓
|
||||
Проверка _exchange_symbols_projection_cache
|
||||
↓ cache miss
|
||||
Чтение InstrumentStore
|
||||
↓ store miss
|
||||
_load_instruments_via_acquisition()
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
Сохранение в InstrumentStore
|
||||
↓
|
||||
map_instruments_to_exchange_symbols()
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
↓
|
||||
Сохранение в _exchange_symbols_projection_cache
|
||||
↓
|
||||
Возврат legacy-результата
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Разделение канонического кэша и compatibility projection cache
|
||||
|
||||
После Build 020 существуют два разных уровня состояния.
|
||||
|
||||
### 7.1. Каноническое состояние
|
||||
|
||||
```python
|
||||
_instrument_store
|
||||
```
|
||||
|
||||
Хранит:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Назначение:
|
||||
|
||||
- хранение полной справочной модели инструмента;
|
||||
- повторное использование канонических данных;
|
||||
- основа для будущих market intelligence consumers;
|
||||
- независимость от legacy exchange models.
|
||||
|
||||
### 7.2. Compatibility projection cache
|
||||
|
||||
```python
|
||||
_exchange_symbols_projection_cache
|
||||
```
|
||||
|
||||
Хранит:
|
||||
|
||||
```python
|
||||
list[ExchangeSymbol] | None
|
||||
```
|
||||
|
||||
Назначение:
|
||||
|
||||
- сохранить старый контракт `get_exchange_symbols()`;
|
||||
- не выполнять повторный compatibility mapping при каждом вызове;
|
||||
- обеспечить безопасную постепенную миграцию legacy-кода.
|
||||
|
||||
`_exchange_symbols_projection_cache` не является источником истины.
|
||||
|
||||
Источником истины является:
|
||||
|
||||
```text
|
||||
InstrumentStore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Изменение acquisition loader
|
||||
|
||||
Старый метод:
|
||||
|
||||
```python
|
||||
_load_exchange_symbols_via_acquisition()
|
||||
```
|
||||
|
||||
удалён.
|
||||
|
||||
Он одновременно:
|
||||
|
||||
- запускал acquisition pipeline;
|
||||
- получал `Instrument`;
|
||||
- выполнял compatibility mapping;
|
||||
- возвращал `ExchangeSymbol`.
|
||||
|
||||
Вместо него используется:
|
||||
|
||||
```python
|
||||
_load_instruments_via_acquisition()
|
||||
```
|
||||
|
||||
Новый метод возвращает:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
и не выполняет:
|
||||
|
||||
```python
|
||||
map_instruments_to_exchange_symbols()
|
||||
```
|
||||
|
||||
Это обеспечивает чистую архитектурную границу:
|
||||
|
||||
```text
|
||||
Acquisition Pipeline
|
||||
↓
|
||||
Canonical Instrument Models
|
||||
```
|
||||
|
||||
Compatibility mapping выполняется отдельно только там, где действительно требуется legacy-контракт.
|
||||
|
||||
---
|
||||
|
||||
## 9. Сохранение обратной совместимости
|
||||
|
||||
Build 020 не меняет публичный контракт:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
Он по-прежнему возвращает:
|
||||
|
||||
```python
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Благодаря этому продолжают работать существующие consumers, включая:
|
||||
|
||||
```text
|
||||
validate_symbol()
|
||||
currency_ui.py
|
||||
legacy exchange UI
|
||||
существующие unit tests
|
||||
```
|
||||
|
||||
Миграция выполнена без обязательного одновременного переписывания всех legacy consumers.
|
||||
|
||||
---
|
||||
|
||||
## 10. Поведение при отключённой бирже
|
||||
|
||||
При:
|
||||
|
||||
```python
|
||||
exchange_enabled = False
|
||||
```
|
||||
|
||||
метод:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
возвращает:
|
||||
|
||||
```python
|
||||
[]
|
||||
```
|
||||
|
||||
При этом он не должен:
|
||||
|
||||
- читать `InstrumentStore`;
|
||||
- использовать существующий projection cache;
|
||||
- запускать acquisition pipeline;
|
||||
- обращаться к реальной бирже.
|
||||
|
||||
Это сохраняет прежнее поведение mock/disabled режима.
|
||||
|
||||
---
|
||||
|
||||
## 11. Поведение при cache hit
|
||||
|
||||
Если существует:
|
||||
|
||||
```python
|
||||
_exchange_symbols_projection_cache
|
||||
```
|
||||
|
||||
метод возвращает существующий объект legacy-проекции без:
|
||||
|
||||
- чтения acquisition source;
|
||||
- повторной обработки документа;
|
||||
- повторного compatibility mapping.
|
||||
|
||||
Если projection cache отсутствует, но канонические данные уже существуют в:
|
||||
|
||||
```python
|
||||
InstrumentStore
|
||||
```
|
||||
|
||||
то acquisition pipeline не запускается повторно.
|
||||
|
||||
Вместо этого выполняется:
|
||||
|
||||
```text
|
||||
InstrumentStore
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
Compatibility Mapper
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Пустой канонический набор:
|
||||
|
||||
```python
|
||||
()
|
||||
```
|
||||
|
||||
также считается валидным cache hit и не должен ошибочно интерпретироваться как отсутствие данных.
|
||||
|
||||
---
|
||||
|
||||
## 12. Поведение при cache miss
|
||||
|
||||
Если одновременно отсутствуют:
|
||||
|
||||
```text
|
||||
_exchange_symbols_projection_cache
|
||||
InstrumentStore entry for "dzengi"
|
||||
```
|
||||
|
||||
выполняется полный production flow:
|
||||
|
||||
```text
|
||||
Instrument Source
|
||||
↓
|
||||
Instrument Handler
|
||||
↓
|
||||
Instrument Acquisition Service
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
InstrumentStore.set(...)
|
||||
↓
|
||||
Compatibility Mapper
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
После успешной загрузки:
|
||||
|
||||
- канонический `tuple[Instrument, ...]` сохраняется в `InstrumentStore`;
|
||||
- legacy-проекция создаётся отдельно;
|
||||
- legacy-проекция сохраняется в `_exchange_symbols_projection_cache`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Поведение при ошибках
|
||||
|
||||
Если acquisition pipeline завершается ошибкой:
|
||||
|
||||
- ошибка логируется как exchange request error;
|
||||
- endpoint сохраняется как:
|
||||
|
||||
```text
|
||||
exchangeInfo
|
||||
```
|
||||
|
||||
- исходная ошибка становится причиной `ExchangeError`;
|
||||
- канонический store не должен получать частичные или некорректные данные;
|
||||
- projection cache не должен заполняться.
|
||||
|
||||
Таким образом, ошибка acquisition не создаёт ложное успешное состояние.
|
||||
|
||||
Если ошибка возникает на этапе compatibility mapping:
|
||||
|
||||
- канонические данные уже могут находиться в `InstrumentStore`;
|
||||
- projection cache не должен заполняться некорректным результатом;
|
||||
- следующий вызов может повторно построить legacy-проекцию из сохранённых канонических данных без повторного запуска acquisition pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 14. Инварианты Build 020
|
||||
|
||||
После завершения Build 020 действуют следующие обязательные инварианты.
|
||||
|
||||
1. `ExchangeService._exchange_symbols_cache` отсутствует в production-коде.
|
||||
|
||||
2. Канонические справочные данные хранятся как:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
3. Владельцем канонического состояния является:
|
||||
|
||||
```text
|
||||
InstrumentStore
|
||||
```
|
||||
|
||||
4. Текущей реализацией store является:
|
||||
|
||||
```python
|
||||
InMemoryInstrumentStore
|
||||
```
|
||||
|
||||
5. `ExchangeService` зависит от абстракции:
|
||||
|
||||
```python
|
||||
InstrumentStoreProtocol
|
||||
```
|
||||
|
||||
6. Старый loader:
|
||||
|
||||
```python
|
||||
_load_exchange_symbols_via_acquisition()
|
||||
```
|
||||
|
||||
отсутствует.
|
||||
|
||||
7. Новый loader:
|
||||
|
||||
```python
|
||||
_load_instruments_via_acquisition()
|
||||
```
|
||||
|
||||
возвращает только:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
8. Новый loader не вызывает:
|
||||
|
||||
```python
|
||||
map_instruments_to_exchange_symbols()
|
||||
```
|
||||
|
||||
9. Compatibility mapping выполняется после получения канонических моделей из store или acquisition pipeline.
|
||||
|
||||
10. `_exchange_symbols_projection_cache` не является каноническим источником данных.
|
||||
|
||||
11. Публичный контракт:
|
||||
|
||||
```python
|
||||
get_exchange_symbols() -> list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
сохранён для обратной совместимости.
|
||||
|
||||
12. `validate_symbol()` продолжает работать через `get_exchange_symbols()`.
|
||||
|
||||
13. При отключённой бирже store, projection cache и acquisition pipeline не используются.
|
||||
|
||||
14. Ошибка acquisition не заполняет канонический store.
|
||||
|
||||
15. Ошибка acquisition не заполняет projection cache.
|
||||
|
||||
16. Пустой `tuple[Instrument, ...]` является валидным сохранённым значением и должен отличаться от отсутствия записи в store.
|
||||
|
||||
---
|
||||
|
||||
## 15. Изменённые production-файлы
|
||||
|
||||
Основные изменения Build 020 выполнены в:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
Используются ранее подготовленные компоненты Storage Layer:
|
||||
|
||||
```text
|
||||
app/src/storage/instrument_store.py
|
||||
app/src/storage/exceptions.py
|
||||
```
|
||||
|
||||
Используется compatibility mapper:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/compatibility.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Тестовое покрытие
|
||||
|
||||
Основные проверки миграции находятся в:
|
||||
|
||||
```text
|
||||
app/tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||||
```
|
||||
|
||||
Дополнительно проверена совместимость:
|
||||
|
||||
```text
|
||||
app/tests/unit/integrations/exchange/test_service_validate_symbol.py
|
||||
```
|
||||
|
||||
И отдельно сохраняется тестовое покрытие самого store:
|
||||
|
||||
```text
|
||||
app/tests/unit/storage/test_instrument_store.py
|
||||
```
|
||||
|
||||
Проверены следующие сценарии:
|
||||
|
||||
- отключённая биржа возвращает пустой список;
|
||||
- отключённая биржа не читает существующий `InstrumentStore`;
|
||||
- cache hit legacy-проекции не запускает acquisition pipeline;
|
||||
- store hit не запускает acquisition pipeline;
|
||||
- store miss запускает acquisition pipeline;
|
||||
- загруженные `Instrument` сохраняются в store;
|
||||
- пустой tuple корректно обрабатывается как cache hit;
|
||||
- compatibility mapping получает именно канонические `Instrument`;
|
||||
- legacy-проекция сохраняет прежний тип `list[ExchangeSymbol]`;
|
||||
- сохраняется порядок инструментов;
|
||||
- acquisition error оборачивается в `ExchangeError`;
|
||||
- acquisition error логируется с endpoint `exchangeInfo`;
|
||||
- acquisition error не заполняет store;
|
||||
- acquisition error не заполняет projection cache;
|
||||
- acquisition loader использует реальную processing pipeline;
|
||||
- acquisition loader использует registry key `dzengi`;
|
||||
- acquisition loader возвращает `tuple[Instrument, ...]`;
|
||||
- acquisition loader не вызывает compatibility mapper;
|
||||
- `validate_symbol()` сохраняет прежнее поведение.
|
||||
|
||||
---
|
||||
|
||||
## 17. Результаты проверок
|
||||
|
||||
Целевая проверка `get_exchange_symbols()`:
|
||||
|
||||
```text
|
||||
23 passed in 0.12s
|
||||
```
|
||||
|
||||
Целевая проверка `validate_symbol()`:
|
||||
|
||||
```text
|
||||
16 passed in 0.07s
|
||||
```
|
||||
|
||||
Проверка синтаксической компиляции:
|
||||
|
||||
```text
|
||||
py_compile — успешно
|
||||
```
|
||||
|
||||
Полный regression suite:
|
||||
|
||||
```text
|
||||
426 passed in 0.23s
|
||||
```
|
||||
|
||||
Финальная архитектурная проверка подтвердила:
|
||||
|
||||
```text
|
||||
старый _exchange_symbols_cache отсутствует в production-коде;
|
||||
старый _load_exchange_symbols_via_acquisition отсутствует;
|
||||
канонический store подключён через InstrumentStoreProtocol;
|
||||
текущая реализация store — InMemoryInstrumentStore;
|
||||
legacy projection cache отделён от канонического store;
|
||||
compatibility mapper не вызывается внутри acquisition loader;
|
||||
новый acquisition loader возвращает канонические Instrument.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Архитектурный результат
|
||||
|
||||
До Build 020:
|
||||
|
||||
```text
|
||||
Exchange API
|
||||
↓
|
||||
Acquisition Pipeline
|
||||
↓
|
||||
Instrument
|
||||
↓
|
||||
Compatibility Mapper
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
ExchangeService class-level cache
|
||||
```
|
||||
|
||||
После Build 020:
|
||||
|
||||
```text
|
||||
Exchange API
|
||||
↓
|
||||
Acquisition Pipeline
|
||||
↓
|
||||
Instrument
|
||||
↓
|
||||
InstrumentStore
|
||||
↓
|
||||
Canonical Instrument Reference Data
|
||||
↓
|
||||
Compatibility Mapper
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
Temporary Legacy Projection Cache
|
||||
```
|
||||
|
||||
Ключевое изменение:
|
||||
|
||||
```text
|
||||
ExchangeSymbol больше не является канонически сохраняемой моделью Instrument Reference Data.
|
||||
```
|
||||
|
||||
Каноническим представлением теперь является:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
а владельцем его состояния является:
|
||||
|
||||
```text
|
||||
Storage Layer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Ограничения текущего этапа
|
||||
|
||||
Build 020 намеренно не выполняет полную миграцию всех consumers на каноническую модель `Instrument`.
|
||||
|
||||
На текущем этапе сохраняются:
|
||||
|
||||
```python
|
||||
get_exchange_symbols() -> list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
_exchange_symbols_projection_cache
|
||||
```
|
||||
|
||||
Они являются compatibility-механизмами переходного периода.
|
||||
|
||||
Также Build 020 не переносит в новый `InstrumentStore`:
|
||||
|
||||
- market price cache;
|
||||
- execution price cache;
|
||||
- balance snapshots;
|
||||
- journal storage;
|
||||
- другие runtime caches.
|
||||
|
||||
Build 020 касается исключительно хранения канонических Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
## 20. Критерии завершения
|
||||
|
||||
Build 020 считается завершённым, если одновременно выполняются следующие условия:
|
||||
|
||||
- [x] созданный ранее `InstrumentStore` используется production-кодом;
|
||||
- [x] канонические данные хранятся как `tuple[Instrument, ...]`;
|
||||
- [x] старый `_exchange_symbols_cache` удалён;
|
||||
- [x] старый `_load_exchange_symbols_via_acquisition()` удалён;
|
||||
- [x] новый `_load_instruments_via_acquisition()` возвращает канонические модели;
|
||||
- [x] acquisition loader не выполняет compatibility mapping;
|
||||
- [x] legacy API `get_exchange_symbols()` сохранён;
|
||||
- [x] `validate_symbol()` сохраняет прежнее поведение;
|
||||
- [x] ошибки acquisition не создают ложное состояние кэша;
|
||||
- [x] целевые тесты проходят;
|
||||
- [x] `py_compile` проходит;
|
||||
- [x] полный regression suite проходит;
|
||||
- [x] финальная архитектурная grep-проверка выполнена.
|
||||
|
||||
---
|
||||
|
||||
## 21. Итоговый статус
|
||||
|
||||
```text
|
||||
BUILD 020 — COMPLETE
|
||||
```
|
||||
|
||||
Build 020 завершает фактический перенос канонического кэша Instrument Reference Data из legacy `ExchangeService._exchange_symbols_cache` в специализированный Storage Layer.
|
||||
|
||||
Существующий бот продолжает работать через сохранённый compatibility-контракт:
|
||||
|
||||
```python
|
||||
get_exchange_symbols() -> list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
При этом новая архитектура уже располагает полноценным каноническим хранилищем:
|
||||
|
||||
```text
|
||||
Instrument Acquisition Pipeline
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
InstrumentStore
|
||||
```
|
||||
|
||||
Это создаёт основу для дальнейшего поэтапного перевода consumers с legacy `ExchangeSymbol` на каноническую модель `Instrument` без нарушения работоспособности существующего бота.
|
||||
814
docs/migrations/build_021.md
Normal file
814
docs/migrations/build_021.md
Normal file
@@ -0,0 +1,814 @@
|
||||
# Build 021 — Перевод первой группы потребителей на канонический Instrument API
|
||||
|
||||
## Статус
|
||||
|
||||
**Завершён**
|
||||
|
||||
---
|
||||
|
||||
## Цель Build
|
||||
|
||||
Перевести первую группу production-потребителей справочника инструментов с legacy-модели:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
и legacy API:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
на каноническую модель:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
и новый публичный API:
|
||||
|
||||
```python
|
||||
ExchangeService.get_instruments()
|
||||
```
|
||||
|
||||
При этом необходимо:
|
||||
|
||||
- сохранить работоспособность существующего бота;
|
||||
- не удалять legacy API преждевременно;
|
||||
- не нарушить существующие runtime-контуры;
|
||||
- сохранить прежнее поведение потребителей;
|
||||
- продолжить постепенный переход к каноническому Instrument Reference Data pipeline;
|
||||
- не создавать параллельный источник истины для справочника инструментов.
|
||||
|
||||
---
|
||||
|
||||
## Исходное состояние
|
||||
|
||||
После завершения Build 020 канонический справочник инструментов уже сохранялся в:
|
||||
|
||||
```python
|
||||
InstrumentStoreProtocol
|
||||
```
|
||||
|
||||
с текущей in-memory реализацией:
|
||||
|
||||
```python
|
||||
InMemoryInstrumentStore
|
||||
```
|
||||
|
||||
В `ExchangeService` существовали два уровня представления данных:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
get_exchange_symbols()
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
legacy consumers
|
||||
```
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
уже являлась источником полных reference data инструмента, однако production-потребители продолжали использовать legacy-модель:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
Первым выбранным потребителем стал:
|
||||
|
||||
```text
|
||||
app/src/telegram/ui/currency_ui.py
|
||||
```
|
||||
|
||||
До миграции он получал список инструментов через:
|
||||
|
||||
```python
|
||||
exchange_service.get_exchange_symbols()
|
||||
```
|
||||
|
||||
и работал с:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Принятое архитектурное решение
|
||||
|
||||
В `ExchangeService` добавлен публичный канонический API:
|
||||
|
||||
```python
|
||||
def get_instruments(self) -> tuple[Instrument, ...]:
|
||||
...
|
||||
```
|
||||
|
||||
Теперь новые потребители должны получать reference data через:
|
||||
|
||||
```python
|
||||
ExchangeService.get_instruments()
|
||||
```
|
||||
|
||||
Legacy API:
|
||||
|
||||
```python
|
||||
ExchangeService.get_exchange_symbols()
|
||||
```
|
||||
|
||||
сохраняется как compatibility API для ещё не переведённых потребителей.
|
||||
|
||||
Целевая архитектура на текущем этапе:
|
||||
|
||||
```text
|
||||
Dzengi exchangeInfo
|
||||
↓
|
||||
DzengiInstrumentDocumentSource
|
||||
↓
|
||||
InstrumentFeed
|
||||
↓
|
||||
DzengiInstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentAcquisitionService
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
ExchangeService.get_instruments()
|
||||
├──→ new consumers
|
||||
│
|
||||
└──→ get_exchange_symbols()
|
||||
↓
|
||||
map_instruments_to_exchange_symbols()
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
↓
|
||||
legacy consumers
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
- `Instrument` является канонической моделью;
|
||||
- `Instrument Store` является runtime-хранилищем канонического справочника;
|
||||
- `get_instruments()` является публичным API для новых и мигрированных потребителей;
|
||||
- `get_exchange_symbols()` является временным compatibility API;
|
||||
- `ExchangeSymbol` не является источником истины.
|
||||
|
||||
---
|
||||
|
||||
## Реализованные изменения
|
||||
|
||||
### 1. Добавлен публичный `get_instruments()`
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
добавлен публичный метод:
|
||||
|
||||
```python
|
||||
def get_instruments(self) -> tuple[Instrument, ...]:
|
||||
...
|
||||
```
|
||||
|
||||
Метод обеспечивает единый доступ к каноническому справочнику инструментов.
|
||||
|
||||
Его поведение:
|
||||
|
||||
```text
|
||||
exchange disabled
|
||||
↓
|
||||
return ()
|
||||
|
||||
exchange enabled
|
||||
↓
|
||||
Instrument Store lookup
|
||||
↓
|
||||
┌── cache hit ──→ return tuple[Instrument, ...]
|
||||
│
|
||||
└── cache miss
|
||||
↓
|
||||
acquisition pipeline
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
return instruments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Сохранено поведение при выключенной бирже
|
||||
|
||||
Если:
|
||||
|
||||
```python
|
||||
exchange_enabled is False
|
||||
```
|
||||
|
||||
метод:
|
||||
|
||||
```python
|
||||
get_instruments()
|
||||
```
|
||||
|
||||
возвращает:
|
||||
|
||||
```python
|
||||
()
|
||||
```
|
||||
|
||||
При этом:
|
||||
|
||||
- `Instrument Store` не читается;
|
||||
- acquisition pipeline не запускается;
|
||||
- внешние запросы к бирже не выполняются.
|
||||
|
||||
---
|
||||
|
||||
### 3. Реализовано чтение из Instrument Store
|
||||
|
||||
При вызове:
|
||||
|
||||
```python
|
||||
get_instruments()
|
||||
```
|
||||
|
||||
сначала проверяется каноническое хранилище:
|
||||
|
||||
```python
|
||||
ExchangeService._instrument_store
|
||||
```
|
||||
|
||||
Если данные уже присутствуют, метод возвращает сохранённый:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
без повторного запуска acquisition pipeline.
|
||||
|
||||
Это устраняет повторную загрузку reference data и сохраняет единый runtime-источник канонических инструментов.
|
||||
|
||||
---
|
||||
|
||||
### 4. Реализована загрузка при отсутствии данных в Store
|
||||
|
||||
Если `Instrument Store` не содержит данных для текущего источника, `get_instruments()` запускает существующий acquisition pipeline:
|
||||
|
||||
```text
|
||||
DzengiInstrumentDocumentSource
|
||||
↓
|
||||
InstrumentFeed
|
||||
↓
|
||||
DzengiInstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentAcquisitionService
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
Полученные канонические модели:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
сохраняются в:
|
||||
|
||||
```python
|
||||
Instrument Store
|
||||
```
|
||||
|
||||
и затем возвращаются вызывающему коду.
|
||||
|
||||
---
|
||||
|
||||
### 5. `get_exchange_symbols()` переведён на канонический `get_instruments()`
|
||||
|
||||
Legacy API:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
больше не должен самостоятельно загружать справочник инструментов.
|
||||
|
||||
Теперь его роль ограничена compatibility projection:
|
||||
|
||||
```text
|
||||
get_instruments()
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
map_instruments_to_exchange_symbols()
|
||||
↓
|
||||
list[ExchangeSymbol]
|
||||
```
|
||||
|
||||
Таким образом, оба API используют один канонический источник данных:
|
||||
|
||||
```text
|
||||
Instrument Store
|
||||
```
|
||||
|
||||
а параллельная загрузка reference data отсутствует.
|
||||
|
||||
---
|
||||
|
||||
### 6. Переведён первый production-потребитель
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
app/src/telegram/ui/currency_ui.py
|
||||
```
|
||||
|
||||
переведён с:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
До миграции использовался resolver:
|
||||
|
||||
```python
|
||||
_resolve_asset_quote_symbol()
|
||||
```
|
||||
|
||||
После миграции используется:
|
||||
|
||||
```python
|
||||
_resolve_asset_quote_instrument()
|
||||
```
|
||||
|
||||
До миграции:
|
||||
|
||||
```python
|
||||
symbols = exchange_service.get_exchange_symbols()
|
||||
```
|
||||
|
||||
После миграции:
|
||||
|
||||
```python
|
||||
instruments = exchange_service.get_instruments()
|
||||
```
|
||||
|
||||
Теперь `currency_ui.py` больше не зависит от:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Поведение `currency_ui.py` после миграции
|
||||
|
||||
Логика выбора торгового инструмента сохранена.
|
||||
|
||||
Для заданного актива:
|
||||
|
||||
```python
|
||||
BTC
|
||||
```
|
||||
|
||||
resolver получает:
|
||||
|
||||
```python
|
||||
tuple[Instrument, ...]
|
||||
```
|
||||
|
||||
и выбирает кандидатов, у которых:
|
||||
|
||||
```text
|
||||
base_asset == BTC
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
quote_asset ∈ {USD, USDT}
|
||||
```
|
||||
|
||||
Затем кандидаты сортируются по прежним приоритетам:
|
||||
|
||||
```text
|
||||
1. quote asset
|
||||
2. trading status
|
||||
3. market type
|
||||
4. symbol
|
||||
```
|
||||
|
||||
Приоритет котируемой валюты:
|
||||
|
||||
```text
|
||||
USD → 3
|
||||
USDT → 2
|
||||
other → 0
|
||||
```
|
||||
|
||||
Приоритет статуса:
|
||||
|
||||
```text
|
||||
TRADING → 2
|
||||
other status → 1
|
||||
HALT/BREAK → 0
|
||||
```
|
||||
|
||||
Приоритет типа рынка:
|
||||
|
||||
```text
|
||||
SPOT → 3
|
||||
LEVERAGE → 2
|
||||
other → 1
|
||||
```
|
||||
|
||||
Таким образом, поведение выбора инструмента осталось совместимым с предыдущей реализацией.
|
||||
|
||||
---
|
||||
|
||||
## Получение USD-оценки актива
|
||||
|
||||
Функция:
|
||||
|
||||
```python
|
||||
get_asset_usd_rate()
|
||||
```
|
||||
|
||||
теперь использует канонический `Instrument`.
|
||||
|
||||
Последовательность:
|
||||
|
||||
```text
|
||||
currency
|
||||
↓
|
||||
USD / USDT?
|
||||
├── yes → 1.0
|
||||
│
|
||||
└── no
|
||||
↓
|
||||
price cache hit?
|
||||
├── yes → cached rate
|
||||
│
|
||||
└── no
|
||||
↓
|
||||
_resolve_asset_quote_instrument()
|
||||
↓
|
||||
Instrument | None
|
||||
↓
|
||||
exchange_service.get_price(instrument.symbol)
|
||||
↓
|
||||
rate
|
||||
```
|
||||
|
||||
При этом существующая логика:
|
||||
|
||||
```python
|
||||
USDT ~= USD
|
||||
```
|
||||
|
||||
сохранена без изменения.
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Если:
|
||||
|
||||
```python
|
||||
exchange_service.get_instruments()
|
||||
```
|
||||
|
||||
вызывает:
|
||||
|
||||
```python
|
||||
ExchangeError
|
||||
```
|
||||
|
||||
resolver возвращает:
|
||||
|
||||
```python
|
||||
None
|
||||
```
|
||||
|
||||
Если подходящий инструмент отсутствует:
|
||||
|
||||
```python
|
||||
None
|
||||
```
|
||||
|
||||
Если получение цены вызывает:
|
||||
|
||||
```python
|
||||
ExchangeError
|
||||
```
|
||||
|
||||
в price cache сохраняется:
|
||||
|
||||
```python
|
||||
None
|
||||
```
|
||||
|
||||
и функция возвращает:
|
||||
|
||||
```python
|
||||
None
|
||||
```
|
||||
|
||||
Таким образом, прежняя отказоустойчивая семантика `currency_ui.py` сохранена.
|
||||
|
||||
---
|
||||
|
||||
## Добавленные тесты
|
||||
|
||||
Добавлен отдельный тестовый файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/integrations/exchange/test_service_instruments.py
|
||||
```
|
||||
|
||||
Он проверяет канонический API:
|
||||
|
||||
```python
|
||||
ExchangeService.get_instruments()
|
||||
```
|
||||
|
||||
В том числе:
|
||||
|
||||
- возврат пустого tuple при выключенной бирже;
|
||||
- отсутствие чтения Store при выключенной бирже;
|
||||
- отсутствие запуска acquisition pipeline при выключенной бирже;
|
||||
- чтение существующих инструментов из Store;
|
||||
- загрузку через acquisition pipeline при cache miss;
|
||||
- сохранение загруженных инструментов в Store;
|
||||
- повторное использование Store;
|
||||
- общее состояние Store между экземплярами `ExchangeService`;
|
||||
- сохранение ошибок acquisition pipeline;
|
||||
- отсутствие fallback на legacy REST path;
|
||||
- использование `get_instruments()` внутри `get_exchange_symbols()`;
|
||||
- преобразование канонических `Instrument` в legacy `ExchangeSymbol`;
|
||||
- сохранение identity compatibility projection cache.
|
||||
|
||||
---
|
||||
|
||||
## Добавлены тесты для первого production-потребителя
|
||||
|
||||
Добавлен файл:
|
||||
|
||||
```text
|
||||
app/tests/unit/telegram/ui/test_currency_ui.py
|
||||
```
|
||||
|
||||
Тестами зафиксировано, что `currency_ui.py`:
|
||||
|
||||
- использует `get_instruments()`;
|
||||
- не использует `get_exchange_symbols()`;
|
||||
- работает с `Instrument`;
|
||||
- выбирает инструмент с `USD` раньше `USDT`;
|
||||
- учитывает статус инструмента;
|
||||
- учитывает тип рынка;
|
||||
- сохраняет детерминированную сортировку;
|
||||
- возвращает `None`, если подходящего инструмента нет;
|
||||
- возвращает `None` при ошибке получения справочника;
|
||||
- не выполняет instrument lookup при наличии цены в cache;
|
||||
- сохраняет прежнее поведение USD/USDT;
|
||||
- корректно получает цену через `instrument.symbol`;
|
||||
- сохраняет `None` в cache при ошибке получения цены;
|
||||
- корректно рассчитывает USD-оценку баланса.
|
||||
|
||||
---
|
||||
|
||||
## Результаты тестирования
|
||||
|
||||
Проверка канонического API:
|
||||
|
||||
```text
|
||||
14 passed in 0.11s
|
||||
```
|
||||
|
||||
Проверка первого production-потребителя:
|
||||
|
||||
```text
|
||||
20 passed in 0.08s
|
||||
```
|
||||
|
||||
Совместная проверка затронутого migration-контура:
|
||||
|
||||
```text
|
||||
73 passed in 0.11s
|
||||
```
|
||||
|
||||
Полный regression suite:
|
||||
|
||||
```text
|
||||
460 passed in 0.26s
|
||||
```
|
||||
|
||||
Все тесты проходят успешно.
|
||||
|
||||
---
|
||||
|
||||
## Проверка фактического состояния кода
|
||||
|
||||
После завершения Build 021 файл:
|
||||
|
||||
```text
|
||||
app/src/telegram/ui/currency_ui.py
|
||||
```
|
||||
|
||||
содержит:
|
||||
|
||||
```python
|
||||
from src.market_data.acquisition.models.instrument import Instrument
|
||||
```
|
||||
|
||||
и использует:
|
||||
|
||||
```python
|
||||
exchange_service.get_instruments()
|
||||
```
|
||||
|
||||
Legacy-зависимости в этом production-потребителе отсутствуют:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
get_exchange_symbols()
|
||||
_resolve_asset_quote_symbol()
|
||||
```
|
||||
|
||||
В `ExchangeService` одновременно существуют:
|
||||
|
||||
```python
|
||||
def get_instruments(self) -> tuple[Instrument, ...]:
|
||||
...
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
|
||||
...
|
||||
```
|
||||
|
||||
Это ожидаемое промежуточное состояние миграции.
|
||||
|
||||
---
|
||||
|
||||
## Что намеренно не сделано в Build 021
|
||||
|
||||
В рамках этого Build не удалялись:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
```python
|
||||
SymbolValidationResult
|
||||
```
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
```python
|
||||
validate_symbol()
|
||||
```
|
||||
|
||||
```python
|
||||
map_instruments_to_exchange_symbols()
|
||||
```
|
||||
|
||||
```python
|
||||
_exchange_symbols_projection_cache
|
||||
```
|
||||
|
||||
Они остаются необходимыми для ещё не переведённых legacy-потребителей.
|
||||
|
||||
Также не переводились следующие runtime-контуры:
|
||||
|
||||
```text
|
||||
ExchangeService internal runtime methods
|
||||
market_stream.py
|
||||
market_data_runner.py
|
||||
telegram/handlers/market.py
|
||||
```
|
||||
|
||||
Их миграция должна выполняться отдельными Build с собственными regression tests.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
До Build 021:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
ExchangeSymbol projection
|
||||
↓
|
||||
all production consumers
|
||||
```
|
||||
|
||||
После Build 021:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
ExchangeService.get_instruments()
|
||||
┌─────┴─────┐
|
||||
↓ ↓
|
||||
new consumers compatibility
|
||||
↓ ↓
|
||||
currency_ui get_exchange_symbols()
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
legacy consumers
|
||||
```
|
||||
|
||||
Первый production-потребитель полностью переведён на канонический `Instrument API`.
|
||||
|
||||
---
|
||||
|
||||
## Критерии завершения Build 021
|
||||
|
||||
Build считается завершённым, поскольку выполнены все критерии:
|
||||
|
||||
- [x] добавлен публичный `ExchangeService.get_instruments()`;
|
||||
- [x] `get_instruments()` использует `Instrument Store`;
|
||||
- [x] при cache miss используется acquisition pipeline;
|
||||
- [x] при выключенной бирже не читается Store и не запускается acquisition;
|
||||
- [x] `get_exchange_symbols()` получает данные через `get_instruments()`;
|
||||
- [x] первый production-потребитель переведён на `Instrument`;
|
||||
- [x] `currency_ui.py` больше не использует `ExchangeSymbol`;
|
||||
- [x] `currency_ui.py` больше не вызывает `get_exchange_symbols()`;
|
||||
- [x] legacy API сохранён для остальных потребителей;
|
||||
- [x] добавлены unit tests для `get_instruments()`;
|
||||
- [x] добавлены unit tests для `currency_ui.py`;
|
||||
- [x] migration-контур проходит `73` теста;
|
||||
- [x] полный regression suite проходит `460` тестов;
|
||||
- [x] существующий бот остаётся работоспособным.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
**Build 021 завершён успешно.**
|
||||
|
||||
В проекте появился публичный канонический API:
|
||||
|
||||
```python
|
||||
ExchangeService.get_instruments()
|
||||
```
|
||||
|
||||
Первый production-потребитель:
|
||||
|
||||
```text
|
||||
app/src/telegram/ui/currency_ui.py
|
||||
```
|
||||
|
||||
переведён с legacy-модели:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
на каноническую:
|
||||
|
||||
```python
|
||||
Instrument
|
||||
```
|
||||
|
||||
При этом legacy compatibility layer сохранён для остальных потребителей, а полный regression suite подтверждает отсутствие регрессий:
|
||||
|
||||
```text
|
||||
460 passed in 0.26s
|
||||
```
|
||||
1169
docs/migrations/build_022.md
Normal file
1169
docs/migrations/build_022.md
Normal file
File diff suppressed because it is too large
Load Diff
335
docs/migrations/build_023.md
Normal file
335
docs/migrations/build_023.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Build 023 — Миграция runtime-потребителей на канонический Instrument Reference
|
||||
|
||||
**Статус:** ✅ Завершён
|
||||
**Дата:** 2026-07-12
|
||||
**Подсистема:** Exchange Integration
|
||||
**Этап:** Instrument Reference Migration
|
||||
|
||||
---
|
||||
|
||||
# Цель Build
|
||||
|
||||
Перевести runtime-компоненты получения рыночных данных на использование канонического справочника инструментов (`Instrument Reference`), полностью отказавшись от любых зависимостей от legacy-проекций `ExchangeSymbol`.
|
||||
|
||||
Build является логическим продолжением Build 022 и завершает миграцию всех runtime-потребителей ExchangeService.
|
||||
|
||||
---
|
||||
|
||||
# Причина изменений
|
||||
|
||||
После завершения Build 022 основная логика ExchangeService уже использовала новый механизм:
|
||||
|
||||
```
|
||||
Instrument Store
|
||||
↓
|
||||
get_instruments()
|
||||
↓
|
||||
validate_symbol()
|
||||
```
|
||||
|
||||
Однако требовалось убедиться, что runtime-компоненты также используют исключительно новый контракт и не имеют скрытых зависимостей от legacy-моделей.
|
||||
|
||||
Проверке подлежали:
|
||||
|
||||
- `market_stream.py`
|
||||
- `market_data_runner.py`
|
||||
|
||||
---
|
||||
|
||||
# Выполненный анализ
|
||||
|
||||
Проведён аудит файлов:
|
||||
|
||||
```
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Проверено использование:
|
||||
|
||||
- validate_symbol()
|
||||
- normalized_symbol
|
||||
- symbol_info
|
||||
- get_exchange_symbols()
|
||||
- ExchangeSymbol
|
||||
- Instrument
|
||||
|
||||
---
|
||||
|
||||
# Результаты анализа
|
||||
|
||||
Установлено следующее.
|
||||
|
||||
## Market Stream
|
||||
|
||||
Используется:
|
||||
|
||||
```python
|
||||
validation = service.validate_symbol(...)
|
||||
```
|
||||
|
||||
После успешной проверки используется:
|
||||
|
||||
```python
|
||||
validation.normalized_symbol
|
||||
```
|
||||
|
||||
Объект `symbol_info` нигде не читается.
|
||||
|
||||
Модель `ExchangeSymbol` не используется.
|
||||
|
||||
Получение списка инструментов напрямую отсутствует.
|
||||
|
||||
---
|
||||
|
||||
## Market Data Runner
|
||||
|
||||
Используется:
|
||||
|
||||
```python
|
||||
validation = ExchangeService().validate_symbol(...)
|
||||
```
|
||||
|
||||
После успешной проверки используется:
|
||||
|
||||
```python
|
||||
validation.normalized_symbol
|
||||
```
|
||||
|
||||
При ошибке валидации сохраняется безопасный fallback:
|
||||
|
||||
```text
|
||||
исходный symbol
|
||||
```
|
||||
|
||||
Обращений к:
|
||||
|
||||
- symbol_info
|
||||
- ExchangeSymbol
|
||||
- get_exchange_symbols()
|
||||
|
||||
не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
# Изменения Production-кода
|
||||
|
||||
Изменения Production-кода не потребовались.
|
||||
|
||||
Build подтвердил, что оба runtime-компонента уже полностью соответствуют новой архитектуре Instrument Reference.
|
||||
|
||||
---
|
||||
|
||||
# Добавлены Unit-тесты
|
||||
|
||||
Создан:
|
||||
|
||||
```
|
||||
tests/unit/integrations/exchange/test_market_stream.py
|
||||
```
|
||||
|
||||
Проверяются следующие сценарии.
|
||||
|
||||
### Использование normalized_symbol
|
||||
|
||||
Проверяется, что WebSocket запускается для канонического символа.
|
||||
|
||||
---
|
||||
|
||||
### Отсутствие зависимости от symbol_info
|
||||
|
||||
Проверяется отсутствие чтения:
|
||||
|
||||
```python
|
||||
validation.symbol_info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Обработка невалидного символа
|
||||
|
||||
Проверяется, что:
|
||||
|
||||
- WebSocket не создаётся;
|
||||
- используется результат validate_symbol().
|
||||
|
||||
---
|
||||
|
||||
### Exchange Disabled
|
||||
|
||||
Проверяется, что при отключённой бирже:
|
||||
|
||||
- ExchangeService не создаётся;
|
||||
- runtime сразу завершается.
|
||||
|
||||
---
|
||||
|
||||
Использованы синхронные тесты через:
|
||||
|
||||
```python
|
||||
asyncio.run(...)
|
||||
```
|
||||
|
||||
что позволило избежать зависимости проекта от `pytest-asyncio`.
|
||||
|
||||
---
|
||||
|
||||
# Проверка Market Data Runner
|
||||
|
||||
Подтверждены существующие тесты:
|
||||
|
||||
```
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Проверяется:
|
||||
|
||||
- использование validate_symbol();
|
||||
- использование normalized_symbol;
|
||||
- отсутствие зависимости от symbol_info;
|
||||
- fallback при ошибке;
|
||||
- fallback при invalid symbol.
|
||||
|
||||
---
|
||||
|
||||
# Проверка компиляции
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/market_stream.py \
|
||||
src/integrations/exchange/market_data_runner.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
Ошибок нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Проверка целевого набора тестов
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
61 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Полный Regression Suite
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
471 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Финальный аудит зависимостей
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "validate_symbol|symbol_info|get_instruments|get_exchange_symbols|ExchangeSymbol|Instrument" \
|
||||
src/integrations/exchange/market_stream.py \
|
||||
src/integrations/exchange/market_data_runner.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
Production-код использует только:
|
||||
|
||||
```
|
||||
validate_symbol()
|
||||
```
|
||||
|
||||
через:
|
||||
|
||||
```
|
||||
normalized_symbol
|
||||
```
|
||||
|
||||
Прямых зависимостей от:
|
||||
|
||||
- ExchangeSymbol
|
||||
- get_exchange_symbols()
|
||||
- symbol_info
|
||||
- Instrument
|
||||
|
||||
не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
# Архитектурный итог
|
||||
|
||||
После Build 023 runtime-компоненты полностью используют канонический путь работы с инструментами:
|
||||
|
||||
```text
|
||||
Instrument Reference
|
||||
│
|
||||
▼
|
||||
Instrument Store
|
||||
│
|
||||
▼
|
||||
get_instruments()
|
||||
│
|
||||
▼
|
||||
validate_symbol()
|
||||
│
|
||||
▼
|
||||
normalized_symbol
|
||||
│
|
||||
▼
|
||||
Market Stream
|
||||
Market Data Runner
|
||||
```
|
||||
|
||||
Legacy-проекция `ExchangeSymbol` больше не участвует в работе runtime-компонентов.
|
||||
|
||||
---
|
||||
|
||||
# Итог Build
|
||||
|
||||
Build 023 полностью завершён.
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- runtime-компоненты используют канонический Instrument Reference;
|
||||
- используется единая точка валидации символов через `validate_symbol()`;
|
||||
- используется только `normalized_symbol`;
|
||||
- отсутствуют зависимости от `ExchangeSymbol` и `symbol_info`;
|
||||
- Production-код соответствует новой архитектуре без дополнительных изменений;
|
||||
- добавлены Unit-тесты для `market_stream.py`;
|
||||
- подтверждена корректность `market_data_runner.py`;
|
||||
- полный Regression Suite успешно пройден (**471 passed**).
|
||||
|
||||
**Статус Build:** ✅ Завершён.
|
||||
290
docs/migrations/build_024.md
Normal file
290
docs/migrations/build_024.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Build 024 — Удаление legacy Telegram Market Handler
|
||||
|
||||
**Статус:** ✅ Завершён
|
||||
**Дата:** 2026-07-12
|
||||
**Подсистема:** Telegram UI
|
||||
**Этап:** Legacy Cleanup
|
||||
|
||||
---
|
||||
|
||||
# Цель Build
|
||||
|
||||
Полностью удалить устаревший экран **Market** из Telegram UI после подтверждения, что он больше не используется работающим приложением.
|
||||
|
||||
Build является этапом очистки архитектуры после перехода на новую концепцию Dzentra, где:
|
||||
|
||||
- экран **«Автоторговля»** является основным рабочим интерфейсом;
|
||||
- диагностика рынка встроена непосредственно в AutoTrade;
|
||||
- отдельный экран **«Рынок»** больше не существует.
|
||||
|
||||
---
|
||||
|
||||
# Предпосылки
|
||||
|
||||
Ранее были удалены:
|
||||
|
||||
- меню Market;
|
||||
- переходы на Market;
|
||||
- экран Monitoring;
|
||||
- интеграция диагностики рынка в отдельный экран.
|
||||
|
||||
Однако файл:
|
||||
|
||||
```text
|
||||
src/telegram/handlers/market.py
|
||||
```
|
||||
|
||||
оставался в проекте.
|
||||
|
||||
Требовалось убедиться, что он действительно является неиспользуемым legacy-компонентом перед его физическим удалением.
|
||||
|
||||
---
|
||||
|
||||
# Выполненный аудит
|
||||
|
||||
Проверены регистрации Telegram Router.
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/telegram/routers.py
|
||||
```
|
||||
|
||||
Подключает только:
|
||||
|
||||
```text
|
||||
start
|
||||
home
|
||||
portfolio
|
||||
auto
|
||||
journal
|
||||
debug_auto
|
||||
debug
|
||||
system
|
||||
```
|
||||
|
||||
Router Market отсутствует.
|
||||
|
||||
---
|
||||
|
||||
Проверены:
|
||||
|
||||
```text
|
||||
src/main.py
|
||||
```
|
||||
|
||||
```text
|
||||
src/telegram/handlers/__init__.py
|
||||
```
|
||||
|
||||
Импортов:
|
||||
|
||||
```python
|
||||
src.telegram.handlers.market
|
||||
```
|
||||
|
||||
не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
# Поиск скрытых зависимостей
|
||||
|
||||
Выполнен аудит проекта.
|
||||
|
||||
Проверены:
|
||||
|
||||
- import market handler;
|
||||
- include_router();
|
||||
- callback_data;
|
||||
- screen="market";
|
||||
- open_market();
|
||||
- open_market_from_monitoring();
|
||||
- runtime-события.
|
||||
|
||||
Выполнены проверки:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
--exclude="market.py" \
|
||||
-E "open_market|open_market_from_monitoring|market:retry|market_open_requested|market_open_success|market_open_error|screen=['\"]market['\"]|router.*market|market_router" \
|
||||
src tests
|
||||
```
|
||||
|
||||
и
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
--exclude="market.py" \
|
||||
-E "src\.telegram\.handlers\.market|telegram\.handlers\.market|handlers\.market" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
Внешних зависимостей не обнаружено.
|
||||
```
|
||||
|
||||
Единственное совпадение:
|
||||
|
||||
```text
|
||||
test_get_symbol_runtime_status_checks_freshness_only_for_open_market
|
||||
```
|
||||
|
||||
оказалось названием unit-теста и не связано с Telegram Market Handler.
|
||||
|
||||
---
|
||||
|
||||
# Выполненные изменения
|
||||
|
||||
Удалён файл:
|
||||
|
||||
```text
|
||||
src/telegram/handlers/market.py
|
||||
```
|
||||
|
||||
После удаления очищены Python cache:
|
||||
|
||||
```bash
|
||||
find src tests \
|
||||
-type d \
|
||||
-name "__pycache__" \
|
||||
-prune \
|
||||
-exec rm -rf {} +
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Проверка компиляции
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/telegram/routers.py \
|
||||
src/main.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
Ошибок нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Полный Regression Suite
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
471 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Финальный аудит
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "src\.telegram\.handlers\.market|telegram\.handlers\.market|handlers\.market|screen=['\"]market['\"]|market:retry|market_open_requested|market_open_success|market_open_error|open_market_from_monitoring" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```
|
||||
Совпадений не найдено.
|
||||
```
|
||||
|
||||
Дополнительно подтверждено отсутствие файла:
|
||||
|
||||
```bash
|
||||
test ! -f src/telegram/handlers/market.py \
|
||||
&& echo "legacy market handler removed"
|
||||
```
|
||||
|
||||
Получен результат:
|
||||
|
||||
```text
|
||||
legacy market handler removed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Архитектурный итог
|
||||
|
||||
После завершения Build 024 структура Telegram UI окончательно соответствует новой архитектуре Dzentra.
|
||||
|
||||
Активные Router:
|
||||
|
||||
```text
|
||||
Telegram
|
||||
│
|
||||
├── start
|
||||
├── home
|
||||
├── portfolio
|
||||
├── auto
|
||||
├── journal
|
||||
├── debug_auto
|
||||
├── debug
|
||||
└── system
|
||||
```
|
||||
|
||||
Legacy Market Handler полностью исключён из проекта.
|
||||
|
||||
---
|
||||
|
||||
# Удалённые legacy-компоненты
|
||||
|
||||
Полностью устранены:
|
||||
|
||||
```text
|
||||
src/telegram/handlers/market.py
|
||||
|
||||
screen="market"
|
||||
|
||||
market:retry
|
||||
|
||||
market_open_requested
|
||||
|
||||
market_open_success
|
||||
|
||||
market_open_error
|
||||
|
||||
open_market_from_monitoring
|
||||
```
|
||||
|
||||
Никаких ссылок на экран Market в проекте больше не существует.
|
||||
|
||||
---
|
||||
|
||||
# Итог Build
|
||||
|
||||
Build 024 полностью завершён.
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- выполнён полный аудит подключений Telegram Router;
|
||||
- подтверждено отсутствие использования Market Handler;
|
||||
- безопасно удалён `src/telegram/handlers/market.py`;
|
||||
- удалены все остаточные ссылки на экран Market;
|
||||
- проект успешно проходит компиляцию;
|
||||
- полный Regression Suite успешно пройден (**471 passed**);
|
||||
- архитектура Telegram UI очищена от legacy-компонентов.
|
||||
|
||||
**Статус Build:** ✅ Завершён.
|
||||
599
docs/migrations/build_025.md
Normal file
599
docs/migrations/build_025.md
Normal file
@@ -0,0 +1,599 @@
|
||||
# Build 025 — Удаление legacy `ExchangeSymbol` compatibility layer
|
||||
|
||||
**Статус:** ✅ Завершён
|
||||
**Дата:** 2026-07-12
|
||||
**Подсистема:** Instrument Reference / Exchange Integration
|
||||
**Этап:** Завершение миграции на каноническую модель `Instrument`
|
||||
|
||||
---
|
||||
|
||||
# Цель Build
|
||||
|
||||
Полностью удалить временный compatibility layer, использовавшийся во время поэтапного перехода от legacy-модели:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
к канонической модели:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
```
|
||||
|
||||
После завершения Build все production-потребители работают через единый канонический контур:
|
||||
|
||||
```text
|
||||
Instrument Acquisition
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
get_instruments()
|
||||
↓
|
||||
validate_symbol()
|
||||
↓
|
||||
Instrument
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Предпосылки
|
||||
|
||||
На предыдущих этапах были выполнены:
|
||||
|
||||
- создание канонической модели `Instrument`;
|
||||
- построение Instrument Acquisition pipeline;
|
||||
- создание `InstrumentStoreProtocol`;
|
||||
- реализация `InMemoryInstrumentStore`;
|
||||
- перенос кэша справочника инструментов в Storage;
|
||||
- добавление `ExchangeService.get_instruments()`;
|
||||
- перевод `validate_symbol()` на канонический справочник;
|
||||
- перевод Telegram UI и runtime-потребителей на `Instrument`;
|
||||
- удаление неиспользуемого legacy Market Handler.
|
||||
|
||||
После этого legacy-контур сохранялся только как временная проекция:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
compatibility.py
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
Реальных production-потребителей этого контура больше не осталось.
|
||||
|
||||
---
|
||||
|
||||
# Предварительный аудит
|
||||
|
||||
Выполнен поиск production-зависимостей:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "ExchangeSymbol|get_exchange_symbols|map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols|_exchange_symbols_projection_cache" \
|
||||
src
|
||||
```
|
||||
|
||||
Установлено, что все найденные элементы находились только внутри самого legacy-контура:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/compatibility.py
|
||||
src/integrations/exchange/models.py
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
Канонические production-потребители уже использовали:
|
||||
|
||||
```text
|
||||
get_instruments()
|
||||
validate_symbol()
|
||||
Instrument
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Аудит legacy parser helpers
|
||||
|
||||
Проверены методы:
|
||||
|
||||
```text
|
||||
_extract_exchange_symbols_raw()
|
||||
_parse_exchange_symbol()
|
||||
_parse_exchange_symbol_status()
|
||||
_parse_market_modes()
|
||||
_extract_filter_value()
|
||||
```
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "_extract_exchange_symbols_raw|_parse_exchange_symbol\(|_parse_exchange_symbol_status|_parse_market_modes|_extract_filter_value" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Подтверждено, что эти методы использовались только устаревшим equivalence-тестом и больше не требовались production-коду.
|
||||
|
||||
---
|
||||
|
||||
# Подготовка тестового контура
|
||||
|
||||
Перед удалением production compatibility layer были обновлены канонические тесты.
|
||||
|
||||
## Обновлён файл
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_instruments.py
|
||||
```
|
||||
|
||||
Из него удалены тесты legacy-проекции:
|
||||
|
||||
```text
|
||||
test_get_exchange_symbols_uses_get_instruments
|
||||
test_get_exchange_symbols_maps_canonical_instruments
|
||||
test_get_exchange_symbols_projection_cache_preserves_identity
|
||||
```
|
||||
|
||||
Сохранены все тесты канонического поведения:
|
||||
|
||||
- отключённая биржа;
|
||||
- Instrument Store hit;
|
||||
- Instrument Store miss;
|
||||
- загрузка через acquisition;
|
||||
- сохранение результата в Store;
|
||||
- повторное использование Store;
|
||||
- поддержка пустого immutable-набора;
|
||||
- общий Store между экземплярами `ExchangeService`;
|
||||
- обработка acquisition errors;
|
||||
- логирование ошибок;
|
||||
- отсутствие заполнения Store при ошибке.
|
||||
|
||||
---
|
||||
|
||||
## Обновлён файл
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py
|
||||
```
|
||||
|
||||
Legacy-проверка через monkeypatch метода:
|
||||
|
||||
```text
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
заменена проверкой прямого использования:
|
||||
|
||||
```text
|
||||
get_instruments()
|
||||
```
|
||||
|
||||
Дополнительно добавлен отрицательный архитектурный тест:
|
||||
|
||||
```python
|
||||
def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None:
|
||||
assert not hasattr(
|
||||
ExchangeService,
|
||||
"get_exchange_symbols",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обновлён файл
|
||||
|
||||
```text
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
```
|
||||
|
||||
Legacy-проверка отсутствия вызова `get_exchange_symbols()` заменена прямой проверкой использования канонического:
|
||||
|
||||
```text
|
||||
get_instruments()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Проверка подготовительного пакета
|
||||
|
||||
Выполнена компиляция:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
tests/unit/integrations/exchange/test_service_instruments.py \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Ошибок нет.
|
||||
```
|
||||
|
||||
Выполнены целевые тесты:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_instruments.py \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
49 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Удалённые migration-only файлы
|
||||
|
||||
Полностью удалены:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/compatibility.py
|
||||
|
||||
tests/unit/market_data/acquisition/test_compatibility.py
|
||||
tests/unit/market_data/acquisition/test_equivalence_comparator.py
|
||||
|
||||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||||
|
||||
tests/integration/market_data/acquisition/test_instrument_reference_equivalence.py
|
||||
tests/support/instrument_reference_equivalence.py
|
||||
```
|
||||
|
||||
Эти файлы обслуживали только временный compatibility/equivalence-контур и завершили свою миграционную задачу.
|
||||
|
||||
---
|
||||
|
||||
# Изменения в `models.py`
|
||||
|
||||
Из файла:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
полностью удалена legacy-модель:
|
||||
|
||||
```python
|
||||
@dataclass(slots=True)
|
||||
class ExchangeSymbol:
|
||||
...
|
||||
```
|
||||
|
||||
Модель:
|
||||
|
||||
```python
|
||||
SymbolValidationResult
|
||||
```
|
||||
|
||||
сохраняет канонический контракт:
|
||||
|
||||
```python
|
||||
symbol_info: Instrument | None
|
||||
```
|
||||
|
||||
Импорт `Instrument` выполняется через:
|
||||
|
||||
```python
|
||||
TYPE_CHECKING
|
||||
```
|
||||
|
||||
что исключает runtime-cycle и сохраняет корректную типизацию.
|
||||
|
||||
---
|
||||
|
||||
# Изменения в `service.py`
|
||||
|
||||
Из файла:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
удалены следующие элементы.
|
||||
|
||||
## Legacy imports
|
||||
|
||||
Удалены:
|
||||
|
||||
```python
|
||||
ExchangeSymbol
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
from src.market_data.acquisition.compatibility import (
|
||||
map_instruments_to_exchange_symbols,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy projection cache
|
||||
|
||||
Удалено поле:
|
||||
|
||||
```python
|
||||
_exchange_symbols_projection_cache
|
||||
```
|
||||
|
||||
Теперь `ExchangeService` хранит только канонический Store:
|
||||
|
||||
```python
|
||||
_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy API
|
||||
|
||||
Полностью удалён метод:
|
||||
|
||||
```python
|
||||
get_exchange_symbols()
|
||||
```
|
||||
|
||||
Единственным публичным API справочника инструментов остаётся:
|
||||
|
||||
```python
|
||||
get_instruments()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy parser helpers
|
||||
|
||||
Удалены методы:
|
||||
|
||||
```text
|
||||
_extract_exchange_symbols_raw()
|
||||
_parse_exchange_symbol()
|
||||
_parse_exchange_symbol_status()
|
||||
_parse_market_modes()
|
||||
_extract_filter_value()
|
||||
```
|
||||
|
||||
Их функции полностью заменены новой pipeline:
|
||||
|
||||
```text
|
||||
Dzengi REST document
|
||||
↓
|
||||
Dzengi parser
|
||||
↓
|
||||
validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
Instrument
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сохранённый helper
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
_safe_str()
|
||||
```
|
||||
|
||||
сохранён, так как продолжает использоваться обработкой trading fee payload.
|
||||
|
||||
---
|
||||
|
||||
# Финальный production-контур
|
||||
|
||||
После удаления compatibility layer работа со справочником инструментов выполняется так:
|
||||
|
||||
```text
|
||||
DzengiInstrumentDocumentSource
|
||||
↓
|
||||
DzengiInstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentFeed
|
||||
↓
|
||||
InstrumentFeedRegistry
|
||||
↓
|
||||
InstrumentAcquisitionService
|
||||
↓
|
||||
tuple[Instrument, ...]
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
ExchangeService.get_instruments()
|
||||
```
|
||||
|
||||
Проверка пользовательского символа выполняется через:
|
||||
|
||||
```text
|
||||
validate_symbol()
|
||||
↓
|
||||
SymbolValidationResult
|
||||
↓
|
||||
symbol_info: Instrument | None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Архитектурные отрицательные тесты
|
||||
|
||||
Добавлены проверки физического отсутствия legacy API.
|
||||
|
||||
## Отсутствие legacy-метода
|
||||
|
||||
```python
|
||||
def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None:
|
||||
assert not hasattr(
|
||||
ExchangeService,
|
||||
"get_exchange_symbols",
|
||||
)
|
||||
```
|
||||
|
||||
## Отсутствие legacy projection cache
|
||||
|
||||
```python
|
||||
def test_exchange_service_has_no_legacy_projection_cache() -> None:
|
||||
assert not hasattr(
|
||||
ExchangeService,
|
||||
"_exchange_symbols_projection_cache",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Финальный аудит
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "ExchangeSymbol|get_exchange_symbols|map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols|_exchange_symbols_projection_cache|market_data\.acquisition\.compatibility" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Остались только ожидаемые упоминания в отрицательных архитектурных тестах:
|
||||
|
||||
```text
|
||||
test_exchange_service_has_no_legacy_get_exchange_symbols
|
||||
test_exchange_service_has_no_legacy_projection_cache
|
||||
```
|
||||
|
||||
Других production- или test-зависимостей не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
# Проверка компиляции
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/models.py \
|
||||
src/integrations/exchange/service.py \
|
||||
tests/unit/integrations/exchange/test_service_instruments.py \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Ошибок нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Целевой Regression Suite
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_instruments.py \
|
||||
tests/unit/integrations/exchange/test_service_validate_symbol.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
94 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Полный Regression Suite
|
||||
|
||||
Выполнено:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
423 passed
|
||||
```
|
||||
|
||||
Снижение общего числа тестов относительно предыдущего Build является ожидаемым, поскольку были удалены временные compatibility- и equivalence-тесты вместе с соответствующим legacy-кодом.
|
||||
|
||||
---
|
||||
|
||||
# Архитектурный итог
|
||||
|
||||
До Build 025:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
├── canonical consumers
|
||||
└── compatibility mapper
|
||||
↓
|
||||
ExchangeSymbol
|
||||
↓
|
||||
legacy projection cache
|
||||
```
|
||||
|
||||
После Build 025:
|
||||
|
||||
```text
|
||||
Instrument
|
||||
↓
|
||||
Instrument Store
|
||||
↓
|
||||
canonical consumers
|
||||
```
|
||||
|
||||
В проекте больше не существует:
|
||||
|
||||
```text
|
||||
ExchangeSymbol
|
||||
compatibility.py
|
||||
get_exchange_symbols()
|
||||
_exchange_symbols_projection_cache
|
||||
Instrument → ExchangeSymbol mapping
|
||||
legacy exchangeInfo parser helpers
|
||||
migration equivalence framework
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Итог Build
|
||||
|
||||
Build 025 полностью завершён.
|
||||
|
||||
Подтверждено:
|
||||
|
||||
- все production-потребители переведены на `Instrument`;
|
||||
- удалена legacy-модель `ExchangeSymbol`;
|
||||
- удалён временный compatibility mapper;
|
||||
- удалён legacy-метод `get_exchange_symbols()`;
|
||||
- удалён projection cache;
|
||||
- удалены неиспользуемые parser helpers;
|
||||
- удалены migration-only и equivalence-тесты;
|
||||
- сохранены и усилены канонические тесты Instrument Store;
|
||||
- добавлены архитектурные тесты отсутствия legacy API;
|
||||
- компиляция проходит без ошибок;
|
||||
- целевой Regression Suite успешно пройден — **94 passed**;
|
||||
- полный Regression Suite успешно пройден — **423 passed**.
|
||||
|
||||
**Статус Build:** ✅ Завершён.
|
||||
799
docs/migrations/build_026.md
Normal file
799
docs/migrations/build_026.md
Normal file
@@ -0,0 +1,799 @@
|
||||
# Build 026 — Аудит текущего контура Quotes Feed
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Функциональный модуль:** Quotes Feed
|
||||
**Проект:** Dzentra
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Провести полный аудит существующего контура получения, обработки, кэширования и потребления текущих рыночных котировок перед началом миграции в целевую подсистему:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/
|
||||
```
|
||||
|
||||
Основная задача Build — определить:
|
||||
|
||||
- где сейчас реализовано получение котировок;
|
||||
- какие REST- и WebSocket-источники используются;
|
||||
- какие модели представляют котировку;
|
||||
- где выполняются parsing, validation и mapping;
|
||||
- как работает оперативный кэш котировок;
|
||||
- какие компоненты являются фактическими потребителями ценовых данных;
|
||||
- какие обязанности относятся непосредственно к Quotes Feed;
|
||||
- какие обязанности должны остаться за пределами Acquisition;
|
||||
- в какой последовательности выполнять безопасную миграцию без нарушения работы существующего бота.
|
||||
|
||||
---
|
||||
|
||||
## 2. Итог аудита
|
||||
|
||||
Текущий бот уже имеет функционально работающий контур получения и использования котировок.
|
||||
|
||||
Котировки поступают из двух источников:
|
||||
|
||||
1. REST API;
|
||||
2. WebSocket depth stream.
|
||||
|
||||
При этом архитектурно логика распределена между:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/rest_client.py
|
||||
src/integrations/exchange/ws_client.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Единой канонической модели `Quote` в production-контуре пока нет.
|
||||
|
||||
Одна и та же концепция текущей рыночной котировки представлена несколькими различными контрактами:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
MarketPriceSnapshot
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
Это подтверждает необходимость поэтапной миграции в каноническую модель Quotes Feed.
|
||||
|
||||
---
|
||||
|
||||
## 3. Текущий REST-контур котировок
|
||||
|
||||
Основная реализация находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
Используются следующие методы:
|
||||
|
||||
```python
|
||||
refresh_price_cache()
|
||||
refresh_market_snapshot_cache()
|
||||
get_price()
|
||||
get_market_snapshot()
|
||||
get_execution_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
_get_real_price()
|
||||
```
|
||||
|
||||
Основной источник данных:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
```
|
||||
|
||||
Из ответа используются поля:
|
||||
|
||||
```text
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
```
|
||||
|
||||
Текущая цепочка выглядит следующим образом:
|
||||
|
||||
```text
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
↓
|
||||
ExchangeRestClient.get_json()
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
lastPrice / bidPrice / askPrice
|
||||
↓
|
||||
legacy dict snapshot
|
||||
```
|
||||
|
||||
REST-транспорт реализован в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/rest_client.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Текущий WebSocket-контур котировок
|
||||
|
||||
В проекте существуют две реализации обработки WebSocket/depth-данных:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
WebSocket-транспорт находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/ws_client.py
|
||||
```
|
||||
|
||||
Для получения данных используется:
|
||||
|
||||
```python
|
||||
ExchangeWebSocketClient.stream_depth()
|
||||
```
|
||||
|
||||
Из depth payload извлекаются:
|
||||
|
||||
```text
|
||||
best bid
|
||||
best ask
|
||||
```
|
||||
|
||||
После чего рассчитывается:
|
||||
|
||||
```text
|
||||
midpoint = (best_bid + best_ask) / 2
|
||||
```
|
||||
|
||||
Результат записывается в:
|
||||
|
||||
```text
|
||||
MarketPriceCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Текущие модели котировок
|
||||
|
||||
### 5.1. TickerPrice
|
||||
|
||||
Находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Текущий контракт:
|
||||
|
||||
```python
|
||||
@dataclass(slots=True)
|
||||
class TickerPrice:
|
||||
symbol: str
|
||||
price: float
|
||||
source: str
|
||||
updated_at: str
|
||||
```
|
||||
|
||||
Используется как упрощённое представление текущей цены инструмента.
|
||||
|
||||
---
|
||||
|
||||
### 5.2. ExecutionPriceSnapshot
|
||||
|
||||
Находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Содержит:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
is_fresh
|
||||
age_seconds
|
||||
freshness_status
|
||||
spread_percent
|
||||
```
|
||||
|
||||
Эта модель относится прежде всего к execution layer и не должна становиться канонической моделью Quotes Feed.
|
||||
|
||||
---
|
||||
|
||||
### 5.3. MarketPriceSnapshot
|
||||
|
||||
Находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
Содержит:
|
||||
|
||||
```text
|
||||
symbol
|
||||
price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
runtime_key
|
||||
received_monotonic
|
||||
```
|
||||
|
||||
Одновременно выполняет роль:
|
||||
|
||||
- модели записи кэша;
|
||||
- контейнера рыночной цены;
|
||||
- источника информации о возрасте записи.
|
||||
|
||||
---
|
||||
|
||||
### 5.4. Словарные snapshot-контракты
|
||||
|
||||
Ряд методов `ExchangeService` возвращает:
|
||||
|
||||
```python
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
с ключами:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
age_seconds
|
||||
```
|
||||
|
||||
Такие словарные контракты используются многими существующими потребителями и должны быть удалены только после их полного перевода на новые типизированные контракты.
|
||||
|
||||
---
|
||||
|
||||
## 6. Основные архитектурные проблемы
|
||||
|
||||
### 6.1. Отсутствует единая каноническая модель Quote
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/quote.py
|
||||
```
|
||||
|
||||
существует в целевой структуре, но текущий production-контур ещё не использует единую каноническую модель `Quote`.
|
||||
|
||||
Вместо неё используются:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
MarketPriceSnapshot
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
Целевая архитектура должна иметь одну внутреннюю каноническую модель котировки.
|
||||
|
||||
---
|
||||
|
||||
### 6.2. ExchangeService перегружен обязанностями
|
||||
|
||||
В текущем состоянии `ExchangeService` одновременно:
|
||||
|
||||
- вызывает REST API;
|
||||
- получает ticker response;
|
||||
- разбирает поля ответа;
|
||||
- проверяет значения;
|
||||
- создаёт snapshot;
|
||||
- читает кэш;
|
||||
- обновляет кэш;
|
||||
- оценивает freshness;
|
||||
- создаёт execution snapshot;
|
||||
- поддерживает legacy API для существующих потребителей.
|
||||
|
||||
Эти обязанности должны быть постепенно разделены между:
|
||||
|
||||
```text
|
||||
adapters/dzengi/
|
||||
validation/
|
||||
models/
|
||||
handlers/
|
||||
feeds/
|
||||
service.py
|
||||
storage/
|
||||
execution/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.3. WebSocket parsing дублируется
|
||||
|
||||
Сходная логика присутствует одновременно в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Дублируются следующие операции:
|
||||
|
||||
- извлечение вложенного payload;
|
||||
- извлечение `bids`;
|
||||
- извлечение `asks`;
|
||||
- получение первой цены;
|
||||
- преобразование значения в `float`;
|
||||
- проверка положительности цены;
|
||||
- расчёт midpoint.
|
||||
|
||||
Эта логика должна быть централизована в Dzengi adapter:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/parser.py
|
||||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.4. Quotes Feed и Order Book Feed частично смешаны
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
stream_depth()
|
||||
```
|
||||
|
||||
получает depth-сообщение, относящееся к данным стакана.
|
||||
|
||||
Однако текущие потребители используют из него только:
|
||||
|
||||
```text
|
||||
best bid
|
||||
best ask
|
||||
```
|
||||
|
||||
Для Quotes Feed это допустимый источник Level I quote.
|
||||
|
||||
При этом полный depth не должен переноситься в Quotes Feed, поскольку полный стакан относится к отдельной будущей подсистеме:
|
||||
|
||||
```text
|
||||
Order Book Feed
|
||||
```
|
||||
|
||||
Таким образом, Quotes Feed должен получать из depth только необходимую информацию верхнего уровня:
|
||||
|
||||
```text
|
||||
best bid
|
||||
best ask
|
||||
```
|
||||
|
||||
и формировать из неё канонический `Quote`.
|
||||
|
||||
---
|
||||
|
||||
### 6.5. Кэш расположен в integration layer
|
||||
|
||||
Текущий кэш находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
Он отвечает одновременно за:
|
||||
|
||||
- модель snapshot;
|
||||
- хранение;
|
||||
- runtime partitioning;
|
||||
- возраст записи;
|
||||
- форматирование локального времени.
|
||||
|
||||
В целевой архитектуре хранение котировок не должно принадлежать Acquisition или exchange integration layer.
|
||||
|
||||
Канонический Quote Store должен находиться в storage layer.
|
||||
|
||||
---
|
||||
|
||||
### 6.6. Внутреннее время представлено UI-строкой
|
||||
|
||||
Текущее представление:
|
||||
|
||||
```text
|
||||
DD.MM.YYYY HH:MM:SS
|
||||
```
|
||||
|
||||
например:
|
||||
|
||||
```text
|
||||
10.07.2026 12:00:00
|
||||
```
|
||||
|
||||
является человекочитаемым UI-представлением, а не подходящим внутренним временным контрактом.
|
||||
|
||||
Каноническая модель должна хранить машинное время, например:
|
||||
|
||||
```text
|
||||
exchange_timestamp_ms
|
||||
received_timestamp_ms
|
||||
```
|
||||
|
||||
или timezone-aware `datetime`.
|
||||
|
||||
Форматирование времени для пользователя должно происходить только на UI-границе.
|
||||
|
||||
---
|
||||
|
||||
### 6.7. REST client содержит дублирование
|
||||
|
||||
В:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/rest_client.py
|
||||
```
|
||||
|
||||
существуют два метода:
|
||||
|
||||
```python
|
||||
get_payload()
|
||||
get_json()
|
||||
```
|
||||
|
||||
которые в значительной степени дублируют транспортную реализацию.
|
||||
|
||||
Исправление этого дублирования не является задачей первого этапа Quotes Feed.
|
||||
|
||||
Однако при дальнейшем развитии Dzengi REST adapter не следует создавать дополнительное дублирование транспорта.
|
||||
|
||||
---
|
||||
|
||||
### 6.8. Текущий WebSocket не является обычной push-subscription
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
stream_depth()
|
||||
```
|
||||
|
||||
работает следующим образом:
|
||||
|
||||
```text
|
||||
открыть постоянное WebSocket-соединение
|
||||
↓
|
||||
отправить новый request
|
||||
↓
|
||||
получить один response
|
||||
↓
|
||||
сделать sleep
|
||||
↓
|
||||
повторить request
|
||||
```
|
||||
|
||||
Таким образом, текущая реализация ближе к polling поверх постоянного WebSocket-соединения, чем к классической push-subscription.
|
||||
|
||||
Кроме того, запуск WebSocket stream из:
|
||||
|
||||
```text
|
||||
src/main.py
|
||||
```
|
||||
|
||||
временно отключён, поскольку runtime probe не подтвердил рабочий endpoint с WebSocket Upgrade 101.
|
||||
|
||||
Поэтому на текущем этапе архитектурно зафиксировано:
|
||||
|
||||
```text
|
||||
REST — рабочий основной источник котировок
|
||||
WebSocket — сохраняемый экспериментальный или резервный транспорт
|
||||
```
|
||||
|
||||
Первая версия нового Quotes Feed не должна зависеть от гарантированной доступности WebSocket.
|
||||
|
||||
---
|
||||
|
||||
## 7. Граница ответственности канонической модели Quote
|
||||
|
||||
Каноническая модель должна представлять непосредственно полученную рыночную котировку.
|
||||
|
||||
В неё должны входить данные уровня:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
exchange_timestamp
|
||||
received_timestamp
|
||||
source
|
||||
```
|
||||
|
||||
Дополнительно могут быть предусмотрены:
|
||||
|
||||
```text
|
||||
sequence_id
|
||||
event_id
|
||||
```
|
||||
|
||||
но только если соответствующий источник Dzengi действительно предоставляет такие значения.
|
||||
|
||||
---
|
||||
|
||||
## 8. Что не должно входить в базовую модель Quote
|
||||
|
||||
В каноническую модель не следует помещать:
|
||||
|
||||
```text
|
||||
runtime_key
|
||||
age_seconds
|
||||
is_fresh
|
||||
freshness_status
|
||||
spread_percent
|
||||
execution side
|
||||
entry price
|
||||
UI-formatted updated_at
|
||||
```
|
||||
|
||||
Причины:
|
||||
|
||||
| Поле | Правильная ответственность |
|
||||
|---|---|
|
||||
| `runtime_key` | Storage |
|
||||
| `age_seconds` | Storage / Access layer |
|
||||
| `is_fresh` | Политика конкретного потребителя |
|
||||
| `freshness_status` | Runtime / consumer policy |
|
||||
| `spread_percent` | Производная метрика |
|
||||
| `execution side` | Execution layer |
|
||||
| `entry price` | Execution layer |
|
||||
| `updated_at` в UI-формате | UI formatting |
|
||||
|
||||
---
|
||||
|
||||
## 9. Фактические потребители котировок
|
||||
|
||||
### 9.1. Потребители `get_price()`
|
||||
|
||||
```text
|
||||
src/telegram/ui/currency_ui.py
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/trading/auto/execution_quality.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9.2. Потребители `get_market_snapshot()`
|
||||
|
||||
```text
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/auto/execution_quality.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
src/trading/diagnostics/snapshot.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9.3. Потребители `get_execution_snapshot()`
|
||||
|
||||
```text
|
||||
src/trading/execution/pricing.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9.4. Потребители `get_fresh_market_snapshot()`
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/trading/debug/execution.py
|
||||
```
|
||||
|
||||
Кроме того, этот метод используется внутри runtime-проверки статуса инструмента.
|
||||
|
||||
---
|
||||
|
||||
## 10. Текущий MarketPriceCache
|
||||
|
||||
Реализация находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
Основные операции:
|
||||
|
||||
```python
|
||||
MarketPriceCache.set_price()
|
||||
MarketPriceCache.get_price()
|
||||
MarketPriceCache.clear()
|
||||
```
|
||||
|
||||
Ключ записи:
|
||||
|
||||
```text
|
||||
(runtime_key, symbol)
|
||||
```
|
||||
|
||||
Кэш используется из:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
На текущем этапе `MarketPriceCache` нельзя удалять, поскольку он является частью рабочего production-контура.
|
||||
|
||||
Он будет заменён только после появления канонического Quote Store и перевода всех производителей и потребителей.
|
||||
|
||||
---
|
||||
|
||||
## 11. Целевая архитектурная цепочка REST Quotes Feed
|
||||
|
||||
```text
|
||||
Dzengi GET /api/v1/ticker/24hr
|
||||
↓
|
||||
adapters/dzengi/rest.py
|
||||
↓
|
||||
adapters/dzengi/models.py
|
||||
↓
|
||||
adapters/dzengi/parser.py
|
||||
↓
|
||||
validation/schema.py
|
||||
↓
|
||||
validation/values.py
|
||||
↓
|
||||
adapters/dzengi/mapper.py
|
||||
↓
|
||||
models/quote.py
|
||||
↓
|
||||
handlers/quotes_handler.py
|
||||
↓
|
||||
feeds/quotes_feed.py
|
||||
↓
|
||||
acquisition/service.py
|
||||
↓
|
||||
legacy ExchangeService facade
|
||||
↓
|
||||
существующие потребители бота
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Целевая архитектурная цепочка WebSocket Quotes Feed
|
||||
|
||||
```text
|
||||
Dzengi WebSocket depth message
|
||||
↓
|
||||
adapters/dzengi/websocket.py
|
||||
↓
|
||||
adapters/dzengi/parser.py
|
||||
↓
|
||||
извлечение best bid / best ask
|
||||
↓
|
||||
validation/
|
||||
↓
|
||||
adapters/dzengi/mapper.py
|
||||
↓
|
||||
models/quote.py
|
||||
↓
|
||||
handlers/quotes_handler.py
|
||||
↓
|
||||
feeds/quotes_feed.py
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
runtime consumers
|
||||
```
|
||||
|
||||
Полный order book при этом не является частью Quotes Feed и должен в будущем обрабатываться отдельной подсистемой:
|
||||
|
||||
```text
|
||||
Order Book Feed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Принцип безопасной миграции
|
||||
|
||||
Миграция должна выполняться без одномоментной замены рабочего контура.
|
||||
|
||||
Основной принцип:
|
||||
|
||||
```text
|
||||
новая реализация создаётся параллельно
|
||||
↓
|
||||
покрывается тестами
|
||||
↓
|
||||
подключается под существующий facade
|
||||
↓
|
||||
потребители переводятся поэтапно
|
||||
↓
|
||||
legacy удаляется только после подтверждения отсутствия потребителей
|
||||
```
|
||||
|
||||
На переходном этапе сохраняются:
|
||||
|
||||
```text
|
||||
ExchangeService.get_price()
|
||||
ExchangeService.get_market_snapshot()
|
||||
ExchangeService.get_execution_snapshot()
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
MarketPriceCache
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
Удаление допускается только в соответствующих поздних Build после полного перевода потребителей.
|
||||
|
||||
---
|
||||
|
||||
## 14. Утверждённый план миграции 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
|
||||
```
|
||||
|
||||
Положение WebSocket-этапа после рабочего REST-контура является намеренным.
|
||||
|
||||
Бот должен сохранить гарантированный рабочий способ получения котировок даже при отсутствии подтверждённого production WebSocket endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 15. Результат Build 026
|
||||
|
||||
В результате Build 026:
|
||||
|
||||
- полностью определён существующий REST-контур котировок;
|
||||
- полностью определён существующий WebSocket/depth-контур;
|
||||
- найдены все текущие модели ценовых данных;
|
||||
- определены прямые производители и потребители котировок;
|
||||
- проанализирован `MarketPriceCache`;
|
||||
- обнаружено дублирование WebSocket parsing;
|
||||
- определена граница между Quotes Feed и Order Book Feed;
|
||||
- определена граница между Acquisition, Storage, Execution и UI;
|
||||
- подтверждена необходимость сохранения legacy facade на время миграции;
|
||||
- определена безопасная последовательность Build 027–040.
|
||||
|
||||
---
|
||||
|
||||
## 16. Статус завершения
|
||||
|
||||
**Build 026 завершён полностью.**
|
||||
|
||||
Дополнительных изменений кода в рамках Build 026 не требуется.
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 027 — Каноническая модель Quote и специализированные контракты
|
||||
```
|
||||
700
docs/migrations/build_027.md
Normal file
700
docs/migrations/build_027.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# Build 027 — Каноническая модель Quote и специализированные контракты
|
||||
|
||||
## Статус
|
||||
|
||||
**Завершён**
|
||||
|
||||
---
|
||||
|
||||
## Цель Build
|
||||
|
||||
Создать каноническую внутреннюю модель текущей рыночной котировки `Quote` и специализированные контракты подсистемы `Quotes Feed`.
|
||||
|
||||
Build должен сформировать независимую от конкретной биржи модель рыночной котировки и определить архитектурные границы между:
|
||||
|
||||
- источником сырого документа котировки;
|
||||
- обработчиком документа;
|
||||
- готовым потоком котировок;
|
||||
- потребителями `Market Data Acquisition`.
|
||||
|
||||
При этом существующий legacy-контур получения и использования цен не должен изменяться.
|
||||
|
||||
---
|
||||
|
||||
## Место в плане миграции
|
||||
|
||||
Build 027 является вторым этапом миграции подсистемы `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
|
||||
|
||||
Build 027 ограничен двумя задачами:
|
||||
|
||||
1. создание канонической модели `Quote`;
|
||||
2. создание специализированных контрактов `Quotes Feed`.
|
||||
|
||||
В рамках Build не реализуются:
|
||||
|
||||
- REST-запрос котировки Dzengi;
|
||||
- модели REST-ответа Dzengi;
|
||||
- parsing ответа `ticker/24hr`;
|
||||
- schema validation ответа Dzengi;
|
||||
- value validation полей котировки;
|
||||
- mapping модели Dzengi в `Quote`;
|
||||
- `QuotesHandler`;
|
||||
- `QuotesFeed`;
|
||||
- регистрация потока в `Acquisition Service`;
|
||||
- хранение котировок;
|
||||
- WebSocket parsing;
|
||||
- изменение `ExchangeService`;
|
||||
- изменение `MarketPriceCache`;
|
||||
- изменение market runtime;
|
||||
- перевод UI-потребителей;
|
||||
- перевод execution-потребителей.
|
||||
|
||||
Эти изменения относятся к следующим Build.
|
||||
|
||||
---
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/
|
||||
├── models/
|
||||
│ └── quote.py
|
||||
└── protocol.py
|
||||
```
|
||||
|
||||
Всего изменено:
|
||||
|
||||
- **2 файла**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Каноническая модель Quote
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/quote.py
|
||||
```
|
||||
|
||||
Создана независимая от конкретной биржи immutable-модель:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Quote:
|
||||
symbol: str
|
||||
|
||||
last_price: Decimal
|
||||
bid_price: Decimal
|
||||
ask_price: Decimal
|
||||
|
||||
exchange_timestamp: datetime | None
|
||||
received_at: datetime
|
||||
|
||||
source: str
|
||||
```
|
||||
|
||||
### Назначение модели
|
||||
|
||||
`Quote` представляет текущий рыночный факт о котировке одного инструмента.
|
||||
|
||||
Модель является внутренней моделью слоя:
|
||||
|
||||
```text
|
||||
Market Data
|
||||
↓
|
||||
Market Data Acquisition
|
||||
↓
|
||||
Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Она не зависит от:
|
||||
|
||||
- API Dzengi;
|
||||
- формата `ticker/24hr`;
|
||||
- WebSocket-сообщений;
|
||||
- legacy-моделей `integrations/exchange`;
|
||||
- UI;
|
||||
- Execution;
|
||||
- конкретного способа хранения данных.
|
||||
|
||||
---
|
||||
|
||||
## 2. Поля модели Quote
|
||||
|
||||
### `symbol`
|
||||
|
||||
```python
|
||||
symbol: str
|
||||
```
|
||||
|
||||
Каноническое обозначение инструмента.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
BTC/USD
|
||||
```
|
||||
|
||||
Поле не должно содержать транспортное или биржевое представление, специфичное для конкретного API, если оно отличается от канонического обозначения Dzentra.
|
||||
|
||||
---
|
||||
|
||||
### `last_price`
|
||||
|
||||
```python
|
||||
last_price: Decimal
|
||||
```
|
||||
|
||||
Последняя известная цена инструмента, полученная от источника.
|
||||
|
||||
---
|
||||
|
||||
### `bid_price`
|
||||
|
||||
```python
|
||||
bid_price: Decimal
|
||||
```
|
||||
|
||||
Лучшая доступная цена покупки.
|
||||
|
||||
---
|
||||
|
||||
### `ask_price`
|
||||
|
||||
```python
|
||||
ask_price: Decimal
|
||||
```
|
||||
|
||||
Лучшая доступная цена продажи.
|
||||
|
||||
---
|
||||
|
||||
### `exchange_timestamp`
|
||||
|
||||
```python
|
||||
exchange_timestamp: datetime | None
|
||||
```
|
||||
|
||||
Время рыночного события на стороне источника данных.
|
||||
|
||||
Поле является optional, поскольку конкретный источник или endpoint может не предоставлять достоверный timestamp события.
|
||||
|
||||
---
|
||||
|
||||
### `received_at`
|
||||
|
||||
```python
|
||||
received_at: datetime
|
||||
```
|
||||
|
||||
Время получения рыночных данных системой Dzentra.
|
||||
|
||||
Это позволяет независимо от наличия `exchange_timestamp` фиксировать момент поступления данных в систему.
|
||||
|
||||
---
|
||||
|
||||
### `source`
|
||||
|
||||
```python
|
||||
source: str
|
||||
```
|
||||
|
||||
Идентификатор источника рыночных данных.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
dzengi
|
||||
```
|
||||
|
||||
Модель не фиксирует конкретный набор допустимых источников на уровне класса `Quote`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Использование Decimal
|
||||
|
||||
Для канонических цен используется:
|
||||
|
||||
```python
|
||||
Decimal
|
||||
```
|
||||
|
||||
а не:
|
||||
|
||||
```python
|
||||
float
|
||||
```
|
||||
|
||||
Это позволяет избежать привязки новой внутренней модели к ограничениям legacy-кода и уменьшает риск потери точности при работе с денежными значениями.
|
||||
|
||||
Legacy-потребители при необходимости смогут получать преобразованное значение `float` через compatibility/facade-слой на следующих этапах миграции.
|
||||
|
||||
---
|
||||
|
||||
## 4. Immutable-модель
|
||||
|
||||
Модель объявлена как:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
```
|
||||
|
||||
Это означает:
|
||||
|
||||
- экземпляр `Quote` не изменяется после создания;
|
||||
- исключается случайная мутация рыночного факта;
|
||||
- модель имеет компактное представление через `slots`;
|
||||
- объект подходит для передачи между слоями системы как immutable value object.
|
||||
|
||||
Такой подход соответствует уже принятому направлению построения канонических моделей `Market Data Acquisition`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Что сознательно не включено в Quote
|
||||
|
||||
В каноническую модель не включены поля:
|
||||
|
||||
```text
|
||||
is_fresh
|
||||
age_seconds
|
||||
freshness_status
|
||||
spread_percent
|
||||
runtime_key
|
||||
received_monotonic
|
||||
```
|
||||
|
||||
Причина: эти значения не являются исходным фактом котировки.
|
||||
|
||||
Они относятся к другим обязанностям системы.
|
||||
|
||||
### Freshness
|
||||
|
||||
```text
|
||||
is_fresh
|
||||
age_seconds
|
||||
freshness_status
|
||||
```
|
||||
|
||||
Это runtime-оценка актуальности данных.
|
||||
|
||||
Она должна вычисляться на основании времени получения или хранения котировки, а не быть частью исходного объекта `Quote`.
|
||||
|
||||
### Spread
|
||||
|
||||
```text
|
||||
spread_percent
|
||||
```
|
||||
|
||||
Это производное значение:
|
||||
|
||||
```text
|
||||
ask_price - bid_price
|
||||
```
|
||||
|
||||
или его процентное представление.
|
||||
|
||||
Оно может быть вычислено отдельным processing/runtime-компонентом.
|
||||
|
||||
### Runtime identity
|
||||
|
||||
```text
|
||||
runtime_key
|
||||
```
|
||||
|
||||
Это идентификатор runtime-контекста, а не свойство рыночной котировки.
|
||||
|
||||
### Monotonic clock
|
||||
|
||||
```text
|
||||
received_monotonic
|
||||
```
|
||||
|
||||
Это внутренний технический механизм runtime/storage-слоя для измерения возраста данных.
|
||||
|
||||
Он не должен загрязнять каноническую модель рыночного факта.
|
||||
|
||||
---
|
||||
|
||||
## 6. Специализированные контракты Quotes Feed
|
||||
|
||||
В файл:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/protocol.py
|
||||
```
|
||||
|
||||
добавлены три специализированных контракта:
|
||||
|
||||
```text
|
||||
QuoteDocumentSource
|
||||
QuoteDocumentHandler
|
||||
QuoteFeedProtocol
|
||||
```
|
||||
|
||||
Архитектурная цепочка:
|
||||
|
||||
```text
|
||||
QuoteDocumentSource
|
||||
↓
|
||||
сырой документ
|
||||
↓
|
||||
QuoteDocumentHandler
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
QuoteFeedProtocol
|
||||
↓
|
||||
Acquisition Service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. QuoteDocumentSource
|
||||
|
||||
Контракт:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class QuoteDocumentSource(Protocol):
|
||||
def fetch_quote_document(
|
||||
self,
|
||||
symbol: str,
|
||||
) -> object:
|
||||
"""
|
||||
Получить декодированный транспортный документ текущей котировки.
|
||||
|
||||
Источник не выполняет schema validation, parsing, value validation
|
||||
или mapping во внутреннюю модель Quote.
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### Ответственность
|
||||
|
||||
`QuoteDocumentSource` отвечает только за получение сырого декодированного транспортного документа.
|
||||
|
||||
Он не должен:
|
||||
|
||||
- проверять схему;
|
||||
- проверять значения;
|
||||
- выполнять mapping;
|
||||
- создавать `Quote`;
|
||||
- хранить котировку;
|
||||
- вычислять freshness;
|
||||
- обслуживать UI или Execution.
|
||||
|
||||
Для Dzengi конкретная реализация будет создана на следующих этапах.
|
||||
|
||||
---
|
||||
|
||||
## 8. QuoteDocumentHandler
|
||||
|
||||
Контракт:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class QuoteDocumentHandler(Protocol):
|
||||
def handle_quote_document(
|
||||
self,
|
||||
document: object,
|
||||
) -> Quote:
|
||||
"""
|
||||
Преобразовать сырой документ в проверенную внутреннюю модель Quote.
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### Ответственность
|
||||
|
||||
`QuoteDocumentHandler` определяет границу между сырым внешним документом и проверенной канонической моделью `Quote`.
|
||||
|
||||
Конкретная реализация должна организовать последовательность:
|
||||
|
||||
```text
|
||||
сырой документ
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Сам контракт не зависит от конкретной биржи.
|
||||
|
||||
---
|
||||
|
||||
## 9. QuoteFeedProtocol
|
||||
|
||||
Контракт:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class QuoteFeedProtocol(Protocol):
|
||||
def load_quote(
|
||||
self,
|
||||
symbol: str,
|
||||
) -> Quote:
|
||||
"""
|
||||
Получить внутреннюю модель текущей котировки инструмента.
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### Ответственность
|
||||
|
||||
`QuoteFeedProtocol` представляет готовый поток получения канонической текущей котировки для `Acquisition Service`.
|
||||
|
||||
Потребитель этого контракта не должен знать:
|
||||
|
||||
- какая биржа является источником;
|
||||
- используется REST или другой транспорт;
|
||||
- как устроен внешний payload;
|
||||
- как выполняется parsing;
|
||||
- как выполняется validation;
|
||||
- как выполняется mapping.
|
||||
|
||||
Для потребителя существует только операция:
|
||||
|
||||
```text
|
||||
symbol → Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Соответствие паттерну Instrument Reference Data
|
||||
|
||||
Build 027 продолжает архитектурный подход, уже реализованный для `Instrument Reference Data`.
|
||||
|
||||
### Instrument Reference Data
|
||||
|
||||
```text
|
||||
InstrumentDocumentSource
|
||||
↓
|
||||
InstrumentDocumentHandler
|
||||
↓
|
||||
InstrumentFeedProtocol
|
||||
↓
|
||||
Instrument
|
||||
```
|
||||
|
||||
### Quotes Feed
|
||||
|
||||
```text
|
||||
QuoteDocumentSource
|
||||
↓
|
||||
QuoteDocumentHandler
|
||||
↓
|
||||
QuoteFeedProtocol
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Таким образом, новая вертикаль `Quotes Feed` строится в соответствии с уже принятой архитектурой `Market Data Acquisition`, без создания альтернативного или параллельного архитектурного подхода.
|
||||
|
||||
---
|
||||
|
||||
## 11. Legacy-контур
|
||||
|
||||
Build 027 не изменяет существующие legacy-компоненты:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
src/integrations/exchange/ws_client.py
|
||||
```
|
||||
|
||||
Продолжают работать без изменений:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
MarketPriceSnapshot
|
||||
MarketPriceCache
|
||||
ExchangeService.get_price()
|
||||
ExchangeService.get_market_snapshot()
|
||||
ExchangeService.get_execution_snapshot()
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
```
|
||||
|
||||
На данном этапе новая модель `Quote` существует параллельно legacy-контуру и ещё не используется работающим ботом.
|
||||
|
||||
Это соответствует утверждённой стратегии безопасной миграции:
|
||||
|
||||
```text
|
||||
создать новый контур
|
||||
↓
|
||||
проверить новый контур
|
||||
↓
|
||||
подключить его под legacy facade
|
||||
↓
|
||||
поэтапно перевести потребителей
|
||||
↓
|
||||
удалить legacy только после полного переключения
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Обратная совместимость
|
||||
|
||||
Build 027 полностью обратно совместим с существующим ботом.
|
||||
|
||||
Не изменены:
|
||||
|
||||
- публичные методы `ExchangeService`;
|
||||
- форматы legacy snapshot;
|
||||
- `MarketPriceCache`;
|
||||
- market runtime;
|
||||
- Telegram UI;
|
||||
- trading strategies;
|
||||
- Execution;
|
||||
- существующие модели интеграционного слоя.
|
||||
|
||||
Новая модель и контракты пока не участвуют в runtime работающего приложения.
|
||||
|
||||
---
|
||||
|
||||
## 13. Проверка синтаксиса
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/models/quote.py \
|
||||
src/market_data/acquisition/protocol.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Успешно.
|
||||
Ошибок синтаксиса и импортов не обнаружено.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
423 passed in 0.24s
|
||||
```
|
||||
|
||||
Все существующие тесты проекта проходят.
|
||||
|
||||
Регрессий не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## 15. Критерии завершения
|
||||
|
||||
Build 027 считается завершённым, поскольку выполнены все его критерии:
|
||||
|
||||
- [x] создана каноническая модель `Quote`;
|
||||
- [x] модель не зависит от Dzengi;
|
||||
- [x] цены представлены через `Decimal`;
|
||||
- [x] модель immutable;
|
||||
- [x] разделены `exchange_timestamp` и `received_at`;
|
||||
- [x] runtime-поля не включены в каноническую модель;
|
||||
- [x] создан `QuoteDocumentSource`;
|
||||
- [x] создан `QuoteDocumentHandler`;
|
||||
- [x] создан `QuoteFeedProtocol`;
|
||||
- [x] сохранён архитектурный паттерн существующей вертикали `Instrument`;
|
||||
- [x] legacy-контур не изменён;
|
||||
- [x] синтаксическая проверка проходит;
|
||||
- [x] полная регрессия проходит;
|
||||
- [x] `423` теста проходят успешно.
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
В рамках Build 027 создан фундамент канонической вертикали `Quotes Feed`.
|
||||
|
||||
Теперь архитектура содержит независимое представление текущей рыночной котировки:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
и три специализированных контракта:
|
||||
|
||||
```text
|
||||
QuoteDocumentSource
|
||||
QuoteDocumentHandler
|
||||
QuoteFeedProtocol
|
||||
```
|
||||
|
||||
Целевая архитектурная цепочка сформирована как:
|
||||
|
||||
```text
|
||||
Внешний источник
|
||||
↓
|
||||
QuoteDocumentSource
|
||||
↓
|
||||
сырой транспортный документ
|
||||
↓
|
||||
QuoteDocumentHandler
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
QuoteFeedProtocol
|
||||
↓
|
||||
Acquisition Service
|
||||
```
|
||||
|
||||
Build 027 завершён без изменения поведения работающего бота и без преждевременного вмешательства в legacy-контур.
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 028 — Dzengi REST quote models, parser и validation
|
||||
```
|
||||
591
docs/migrations/build_028.md
Normal file
591
docs/migrations/build_028.md
Normal 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
|
||||
```
|
||||
717
docs/migrations/build_029.md
Normal file
717
docs/migrations/build_029.md
Normal file
@@ -0,0 +1,717 @@
|
||||
# Build 029 — Dzengi Mapper и Quotes Handler
|
||||
|
||||
## Статус
|
||||
|
||||
**Завершён**
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 029
|
||||
|
||||
Цель Build 029 — реализовать преобразование специализированной модели REST-котировки Dzengi в каноническую модель `Quote` и создать обработчик полного цикла преобразования сырого REST-документа в проверенную внутреннюю модель котировки.
|
||||
|
||||
Build является частью поэтапной миграции подсистемы:
|
||||
|
||||
**Quotes Feed**
|
||||
|
||||
в новую архитектуру:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/
|
||||
```
|
||||
|
||||
На данном этапе реализованы:
|
||||
|
||||
- специализированный Dzengi quote mapper;
|
||||
- преобразование `DzengiTicker24hrResponse` в канонический `Quote`;
|
||||
- преобразование цен в `Decimal`;
|
||||
- преобразование биржевого timestamp в timezone-aware UTC `datetime`;
|
||||
- фиксация времени получения котировки;
|
||||
- специализированный `QuotesHandler`;
|
||||
- полная handler-цепочка обработки сырого REST-документа.
|
||||
|
||||
Подключение `Quotes Feed`, registry, `Acquisition Service`, `Quote Store`, `ExchangeService` facade и runtime-потребителей в данный Build не входит.
|
||||
|
||||
---
|
||||
|
||||
## 2. Место Build 029 в плане миграции 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 029 продолжает фундамент, созданный в Builds 027–028.
|
||||
|
||||
После его завершения сформирована цепочка:
|
||||
|
||||
```text
|
||||
raw Dzengi REST document
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
Dzengi quote parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
DzengiTicker24hrResponse
|
||||
↓
|
||||
Dzengi quote mapper
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Исходное состояние перед Build 029
|
||||
|
||||
До начала Build 029 уже были реализованы:
|
||||
|
||||
### Build 027
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/quote.py
|
||||
src/market_data/acquisition/protocol.py
|
||||
```
|
||||
|
||||
Были определены:
|
||||
|
||||
- каноническая модель `Quote`;
|
||||
- контракт источника сырого quote-документа;
|
||||
- контракт обработчика quote-документа;
|
||||
- контракт готового `Quotes Feed`.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
Были реализованы:
|
||||
|
||||
- модель REST-ответа `/api/v1/ticker/24hr`;
|
||||
- parser quote payload;
|
||||
- schema validation;
|
||||
- value validation;
|
||||
- специализированные ошибки Acquisition layer.
|
||||
|
||||
Отсутствовал слой, преобразующий проверенную Dzengi-specific модель в канонический `Quote`, а также единая точка оркестрации всей цепочки обработки сырого документа.
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменённые и добавленные файлы
|
||||
|
||||
В рамках Build 029 изменены только два исходных файла:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||||
src/market_data/acquisition/handlers/quotes_handler.py
|
||||
```
|
||||
|
||||
Добавлены два специализированных файла тестов:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py
|
||||
tests/unit/market_data/acquisition/handlers/test_quotes_handler.py
|
||||
```
|
||||
|
||||
Другие файлы в рамках фактически применённого Build 029 не изменялись.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dzengi Quote Mapper
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||||
```
|
||||
|
||||
реализовано преобразование:
|
||||
|
||||
```text
|
||||
DzengiTicker24hrResponse
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Mapper является архитектурной границей между:
|
||||
|
||||
```text
|
||||
exchange-specific adapter model
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
canonical Acquisition model
|
||||
```
|
||||
|
||||
Его ответственность:
|
||||
|
||||
- принять проверенную модель `DzengiTicker24hrResponse`;
|
||||
- преобразовать биржевые значения цен в канонический тип;
|
||||
- преобразовать биржевой timestamp;
|
||||
- определить источник данных;
|
||||
- зафиксировать время получения котировки;
|
||||
- создать канонический `Quote`.
|
||||
|
||||
Mapper не должен:
|
||||
|
||||
- выполнять REST-запрос;
|
||||
- разбирать сырой JSON payload;
|
||||
- выполнять schema validation сырого документа;
|
||||
- управлять store или cache;
|
||||
- обращаться к `ExchangeService`;
|
||||
- содержать UI-логику;
|
||||
- содержать execution-логику.
|
||||
|
||||
---
|
||||
|
||||
## 6. Преобразование модели Dzengi в канонический Quote
|
||||
|
||||
Исходная модель адаптера содержит данные, соответствующие REST-ответу Dzengi:
|
||||
|
||||
```text
|
||||
symbol
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
closeTime
|
||||
```
|
||||
|
||||
После parsing и validation эти данные представлены специализированной моделью:
|
||||
|
||||
```text
|
||||
DzengiTicker24hrResponse
|
||||
```
|
||||
|
||||
Mapper преобразует её в:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
с канонической семантикой:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
source_timestamp
|
||||
received_at
|
||||
source
|
||||
```
|
||||
|
||||
Таким образом, API-specific имена:
|
||||
|
||||
```text
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
closeTime
|
||||
```
|
||||
|
||||
не выходят за пределы Dzengi adapter layer.
|
||||
|
||||
---
|
||||
|
||||
## 7. Использование Decimal для цен
|
||||
|
||||
Цены преобразуются в `Decimal`.
|
||||
|
||||
Целевая семантика:
|
||||
|
||||
```text
|
||||
last_price: Decimal
|
||||
bid_price: Decimal
|
||||
ask_price: Decimal
|
||||
```
|
||||
|
||||
Это решение исключает ненужную потерю точности при преобразовании рыночных цен через бинарный `float`.
|
||||
|
||||
Архитектурная цепочка:
|
||||
|
||||
```text
|
||||
Dzengi string price
|
||||
↓
|
||||
Decimal
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
"64159.45"
|
||||
↓
|
||||
Decimal("64159.45")
|
||||
```
|
||||
|
||||
Mapper не должен сначала преобразовывать строку в `float`, а затем создавать `Decimal`, поскольку такой путь способен внести артефакты двоичного представления числа.
|
||||
|
||||
---
|
||||
|
||||
## 8. Преобразование биржевого timestamp
|
||||
|
||||
Поле Dzengi:
|
||||
|
||||
```text
|
||||
closeTime
|
||||
```
|
||||
|
||||
содержит Unix timestamp в миллисекундах.
|
||||
|
||||
Mapper преобразует его в timezone-aware UTC `datetime`.
|
||||
|
||||
Семантика преобразования:
|
||||
|
||||
```text
|
||||
closeTime milliseconds
|
||||
↓
|
||||
UTC datetime
|
||||
↓
|
||||
Quote.source_timestamp
|
||||
```
|
||||
|
||||
Использование timezone-aware значения необходимо для однозначного представления времени рыночного события и последующих операций:
|
||||
|
||||
- freshness calculation;
|
||||
- sequence validation;
|
||||
- event ordering;
|
||||
- диагностика задержек;
|
||||
- сопоставление данных из нескольких источников.
|
||||
|
||||
---
|
||||
|
||||
## 9. Время получения котировки
|
||||
|
||||
Помимо биржевого времени события, канонический `Quote` содержит время фактического получения данных платформой:
|
||||
|
||||
```text
|
||||
received_at
|
||||
```
|
||||
|
||||
Разделение двух временных характеристик принципиально:
|
||||
|
||||
```text
|
||||
source_timestamp
|
||||
```
|
||||
|
||||
означает время, указанное источником данных;
|
||||
|
||||
```text
|
||||
received_at
|
||||
```
|
||||
|
||||
означает время, когда котировка была преобразована во внутреннюю модель платформы.
|
||||
|
||||
Это создаёт фундамент для последующего определения:
|
||||
|
||||
- возраста котировки;
|
||||
- сетевой задержки;
|
||||
- freshness;
|
||||
- stale data;
|
||||
- задержки между биржей и локальной системой.
|
||||
|
||||
---
|
||||
|
||||
## 10. Источник котировки
|
||||
|
||||
Канонический `Quote` получает идентификатор источника:
|
||||
|
||||
```text
|
||||
dzengi
|
||||
```
|
||||
|
||||
Это позволяет внутренней модели не зависеть от конкретного adapter-класса, сохраняя при этом происхождение рыночных данных.
|
||||
|
||||
Целевая модель допускает дальнейшую работу с несколькими источниками:
|
||||
|
||||
```text
|
||||
Dzengi
|
||||
Binance
|
||||
Coinbase
|
||||
другие источники
|
||||
```
|
||||
|
||||
При этом приоритетным источником для торговых решений остаётся биржа исполнения.
|
||||
|
||||
---
|
||||
|
||||
## 11. Quotes Handler
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/handlers/quotes_handler.py
|
||||
```
|
||||
|
||||
реализован специализированный обработчик quote-документа.
|
||||
|
||||
Его ответственность — оркестрировать существующие специализированные стадии обработки:
|
||||
|
||||
```text
|
||||
raw document
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Handler является единой точкой преобразования:
|
||||
|
||||
```text
|
||||
object → Quote
|
||||
```
|
||||
|
||||
Он не должен самостоятельно дублировать внутреннюю реализацию:
|
||||
|
||||
- schema validation;
|
||||
- parsing;
|
||||
- value validation;
|
||||
- mapping.
|
||||
|
||||
Вместо этого handler координирует специализированные компоненты.
|
||||
|
||||
---
|
||||
|
||||
## 12. Полная цепочка обработки
|
||||
|
||||
После Build 029 полный путь REST-документа выглядит следующим образом:
|
||||
|
||||
```text
|
||||
{
|
||||
"askPrice": "64159.55",
|
||||
"bidPrice": "64159.45",
|
||||
"closeTime": 1783887270312,
|
||||
"lastPrice": "64159.45",
|
||||
"symbol": "BTC/USD_LEVERAGE"
|
||||
}
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
Dzengi REST quote parser
|
||||
↓
|
||||
DzengiTicker24hrResponse
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
Dzengi quote mapper
|
||||
↓
|
||||
Quote(
|
||||
symbol=...,
|
||||
last_price=...,
|
||||
bid_price=...,
|
||||
ask_price=...,
|
||||
source_timestamp=...,
|
||||
received_at=...,
|
||||
source=...
|
||||
)
|
||||
```
|
||||
|
||||
Таким образом, верхние слои платформы больше не обязаны знать формат ответа Dzengi.
|
||||
|
||||
---
|
||||
|
||||
## 13. Архитектурные решения Build 029
|
||||
|
||||
### 13.1. Mapper изолирует специфику Dzengi
|
||||
|
||||
Только adapter layer знает о:
|
||||
|
||||
```text
|
||||
DzengiTicker24hrResponse
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
closeTime
|
||||
```
|
||||
|
||||
После mapping верхние слои работают исключительно с:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13.2. Handler не зависит от ExchangeService
|
||||
|
||||
Новый `QuotesHandler` не использует:
|
||||
|
||||
```text
|
||||
src.integrations.exchange.service.ExchangeService
|
||||
```
|
||||
|
||||
Направление зависимостей остаётся правильным:
|
||||
|
||||
```text
|
||||
external Dzengi payload
|
||||
↓
|
||||
Acquisition adapter
|
||||
↓
|
||||
Acquisition handler
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
Обратной зависимости новой подсистемы от legacy integration layer нет.
|
||||
|
||||
---
|
||||
|
||||
### 13.3. Handler не является Feed
|
||||
|
||||
`QuotesHandler` отвечает только за преобразование документа:
|
||||
|
||||
```text
|
||||
object → Quote
|
||||
```
|
||||
|
||||
Он не отвечает за получение документа от биржи.
|
||||
|
||||
Получение данных будет ответственностью:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/feeds/quotes_feed.py
|
||||
```
|
||||
|
||||
на следующем этапе миграции.
|
||||
|
||||
---
|
||||
|
||||
### 13.4. Handler не является Store
|
||||
|
||||
`QuotesHandler` не сохраняет котировки.
|
||||
|
||||
Хранение будет реализовано отдельно:
|
||||
|
||||
```text
|
||||
Build 032 — Канонический Quote Store
|
||||
```
|
||||
|
||||
Такое разделение предотвращает смешивание:
|
||||
|
||||
```text
|
||||
acquisition
|
||||
processing
|
||||
storage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13.5. Build не изменяет production runtime
|
||||
|
||||
В Build 029 не изменены:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Не переведены:
|
||||
|
||||
```text
|
||||
UI consumers
|
||||
execution consumers
|
||||
strategy consumers
|
||||
diagnostics consumers
|
||||
```
|
||||
|
||||
Работающий бот продолжает использовать прежний runtime-контур.
|
||||
|
||||
---
|
||||
|
||||
## 14. Что намеренно не реализовано
|
||||
|
||||
В Build 029 не входят:
|
||||
|
||||
```text
|
||||
Quotes Feed
|
||||
регистрация Quotes Feed
|
||||
подключение к Acquisition Service
|
||||
подключение нового REST Quotes Feed к ExchangeService facade
|
||||
Quote Store
|
||||
перенос MarketPriceCache на Quote Store
|
||||
WebSocket quote parsing
|
||||
WebSocket quote adapter
|
||||
перевод market runtime
|
||||
перевод read-only потребителей
|
||||
перевод UI-потребителей
|
||||
перевод execution-потребителей
|
||||
удаление TickerPrice
|
||||
удаление market snapshot dict layer
|
||||
удаление legacy quote parsing
|
||||
удаление MarketPriceCache
|
||||
```
|
||||
|
||||
Каждая из этих задач выполняется только в соответствующем последующем Build.
|
||||
|
||||
---
|
||||
|
||||
## 15. Проверки
|
||||
|
||||
Выполнена синтаксическая проверка:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/adapters/dzengi/mapper.py \
|
||||
src/market_data/acquisition/handlers/quotes_handler.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \
|
||||
tests/unit/market_data/acquisition/handlers/test_quotes_handler.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Успешно.
|
||||
```
|
||||
|
||||
Выполнены специализированные тесты Build 029:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \
|
||||
tests/unit/market_data/acquisition/handlers/test_quotes_handler.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
12 passed in 0.03s
|
||||
```
|
||||
|
||||
Выполнена полная регрессия проекта:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
457 passed in 0.26s
|
||||
```
|
||||
|
||||
Регрессий не обнаружено.
|
||||
|
||||
Количество тестов увеличилось:
|
||||
|
||||
```text
|
||||
После Build 028: 445 passed
|
||||
После Build 029: 457 passed
|
||||
```
|
||||
|
||||
Добавлено:
|
||||
|
||||
```text
|
||||
12 специализированных тестов
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Критерии завершения Build 029
|
||||
|
||||
Build 029 считается завершённым, поскольку выполнены все необходимые условия:
|
||||
|
||||
- [x] реализован специализированный Dzengi quote mapper;
|
||||
- [x] `DzengiTicker24hrResponse` преобразуется в канонический `Quote`;
|
||||
- [x] API-specific имена не выходят за пределы adapter layer;
|
||||
- [x] цены преобразуются в `Decimal`;
|
||||
- [x] не используется промежуточное преобразование цен через `float`;
|
||||
- [x] `closeTime` преобразуется в timezone-aware UTC `datetime`;
|
||||
- [x] фиксируется `received_at`;
|
||||
- [x] сохраняется источник котировки;
|
||||
- [x] реализован специализированный `QuotesHandler`;
|
||||
- [x] handler оркестрирует полный цикл обработки сырого документа;
|
||||
- [x] handler не дублирует ответственность parser;
|
||||
- [x] handler не дублирует ответственность validation;
|
||||
- [x] handler не дублирует ответственность mapper;
|
||||
- [x] новая реализация не зависит от `ExchangeService`;
|
||||
- [x] новая реализация не зависит от `MarketPriceCache`;
|
||||
- [x] production runtime не изменён;
|
||||
- [x] специализированные тесты проходят;
|
||||
- [x] полная регрессия проходит.
|
||||
|
||||
---
|
||||
|
||||
## 17. Итог
|
||||
|
||||
В результате Build 029 завершён слой преобразования REST-котировки Dzengi во внутреннюю каноническую модель платформы:
|
||||
|
||||
```text
|
||||
Dzengi REST payload
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
DzengiTicker24hrResponse
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
Также создан единый специализированный обработчик:
|
||||
|
||||
```text
|
||||
QuotesHandler
|
||||
```
|
||||
|
||||
который предоставляет операцию:
|
||||
|
||||
```text
|
||||
raw document → canonical Quote
|
||||
```
|
||||
|
||||
При этом сохранены ключевые архитектурные свойства миграции:
|
||||
|
||||
- новая реализация развивается параллельно legacy-контуру;
|
||||
- работающий бот не сломан;
|
||||
- `ExchangeService` не изменён;
|
||||
- `MarketPriceCache` не изменён;
|
||||
- runtime-потребители не изменены;
|
||||
- Dzengi-specific формат изолирован внутри adapter layer;
|
||||
- верхние слои получают каноническую модель `Quote`;
|
||||
- mapping и orchestration разделены по ответственности;
|
||||
- сохранена возможность безопасного поэтапного переключения системы.
|
||||
|
||||
**Build 029 завершён.**
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 030 — Quotes Feed и регистрация в Acquisition Service
|
||||
```
|
||||
630
docs/migrations/build_030.md
Normal file
630
docs/migrations/build_030.md
Normal file
@@ -0,0 +1,630 @@
|
||||
# Build 030 — Quotes Feed и регистрация в Acquisition Service
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Вертикаль:** Quotes Feed
|
||||
**Проект:** Dzentra
|
||||
**Тип изменения:** Архитектурная миграция без изменения поведения legacy runtime
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Цель Build 030 — собрать ранее реализованные компоненты Quotes Feed в завершённую прикладную цепочку получения канонической котировки и зарегистрировать эту цепочку в слое Acquisition Service.
|
||||
|
||||
Build должен обеспечить следующий поток данных:
|
||||
|
||||
```text
|
||||
Dzengi REST /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
На данном этапе новый Quotes Feed существует параллельно с legacy-контуром и ещё не подключается к `ExchangeService`, `MarketPriceCache`, market runtime, UI или Execution.
|
||||
|
||||
---
|
||||
|
||||
## 2. Предпосылки
|
||||
|
||||
К началу Build 030 были завершены предыдущие этапы:
|
||||
|
||||
```text
|
||||
Build 027 — Каноническая модель Quote и специализированные контракты
|
||||
Build 028 — Dzengi REST quote models, parser и validation
|
||||
Build 029 — Dzengi mapper и Quotes Handler
|
||||
```
|
||||
|
||||
В результате уже существовали:
|
||||
|
||||
- каноническая модель `Quote`;
|
||||
- контракт `QuoteDocumentSource`;
|
||||
- контракт `QuoteDocumentHandler`;
|
||||
- контракт `QuoteFeedProtocol`;
|
||||
- транспортная модель ответа Dzengi;
|
||||
- schema validation;
|
||||
- parser;
|
||||
- value validation;
|
||||
- mapper;
|
||||
- `DzengiQuoteDocumentHandler`;
|
||||
- специализированные исключения Quotes Feed.
|
||||
|
||||
Не хватало orchestration-слоя, связывающего эти компоненты в завершённый pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 3. Границы Build
|
||||
|
||||
В Build 030 изменены следующие production-файлы:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
src/market_data/acquisition/feeds/quotes_feed.py
|
||||
src/market_data/acquisition/registry.py
|
||||
src/market_data/acquisition/service.py
|
||||
```
|
||||
|
||||
Добавлен новый файл тестов:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py
|
||||
```
|
||||
|
||||
Расширены существующие тесты:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
|
||||
tests/unit/market_data/acquisition/test_registry.py
|
||||
tests/unit/market_data/acquisition/test_service.py
|
||||
```
|
||||
|
||||
Следующие компоненты намеренно не изменялись:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Также не изменялись:
|
||||
|
||||
- UI-потребители;
|
||||
- Execution-потребители;
|
||||
- торговые стратегии;
|
||||
- runtime-контур;
|
||||
- Quote Store;
|
||||
- legacy market snapshot dict layer.
|
||||
|
||||
Эти изменения относятся к следующим Build.
|
||||
|
||||
---
|
||||
|
||||
## 4. Реализованная архитектура
|
||||
|
||||
### 4.1. REST source
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
```
|
||||
|
||||
реализован специализированный источник:
|
||||
|
||||
```python
|
||||
DzengiQuoteDocumentSource
|
||||
```
|
||||
|
||||
Его ответственность ограничена получением сырого транспортного документа текущей котировки.
|
||||
|
||||
Целевая операция:
|
||||
|
||||
```python
|
||||
fetch_quote_document(symbol: str) -> object
|
||||
```
|
||||
|
||||
Источник выполняет запрос:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
```
|
||||
|
||||
с параметрами:
|
||||
|
||||
```python
|
||||
{
|
||||
"symbol": symbol,
|
||||
}
|
||||
```
|
||||
|
||||
REST source:
|
||||
|
||||
- принимает торговый символ;
|
||||
- передаёт его REST-клиенту без изменения;
|
||||
- получает декодированный транспортный документ;
|
||||
- возвращает исходный payload;
|
||||
- преобразует транспортные ошибки в специализированную ошибку Quotes Feed.
|
||||
|
||||
REST source не выполняет:
|
||||
|
||||
- schema validation;
|
||||
- parsing;
|
||||
- value validation;
|
||||
- mapping;
|
||||
- кэширование;
|
||||
- retry;
|
||||
- нормализацию торгового символа.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quotes Feed
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/feeds/quotes_feed.py
|
||||
```
|
||||
|
||||
реализован:
|
||||
|
||||
```python
|
||||
QuotesFeed
|
||||
```
|
||||
|
||||
Основная операция:
|
||||
|
||||
```python
|
||||
load_quote(symbol: str) -> Quote
|
||||
```
|
||||
|
||||
Внутренняя последовательность:
|
||||
|
||||
```text
|
||||
symbol
|
||||
↓
|
||||
QuoteDocumentSource.fetch_quote_document(symbol)
|
||||
↓
|
||||
raw document
|
||||
↓
|
||||
QuoteDocumentHandler.handle_quote_document(document)
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
`QuotesFeed` является orchestration-компонентом и не дублирует обязанности других слоёв.
|
||||
|
||||
Он не выполняет:
|
||||
|
||||
- транспортные запросы самостоятельно;
|
||||
- schema validation;
|
||||
- parsing;
|
||||
- value validation;
|
||||
- mapping;
|
||||
- нормализацию символа;
|
||||
- retry;
|
||||
- кэширование;
|
||||
- сохранение в Store;
|
||||
- обращение к `ExchangeService`.
|
||||
|
||||
Ошибки source и handler не переоборачиваются повторно.
|
||||
|
||||
---
|
||||
|
||||
## 6. Quote Feed Registry
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/registry.py
|
||||
```
|
||||
|
||||
добавлен отдельный реестр:
|
||||
|
||||
```python
|
||||
QuoteFeedRegistry
|
||||
```
|
||||
|
||||
Существующий:
|
||||
|
||||
```python
|
||||
InstrumentFeedRegistry
|
||||
```
|
||||
|
||||
сохранён без архитектурного объединения с Quotes Feed.
|
||||
|
||||
Это позволяет:
|
||||
|
||||
- не изменять стабильный Instrument Reference Data contour;
|
||||
- сохранить изоляцию вертикалей Acquisition;
|
||||
- минимизировать область регрессии;
|
||||
- избежать преждевременной универсализации registry.
|
||||
|
||||
`QuoteFeedRegistry` обеспечивает:
|
||||
|
||||
- регистрацию `QuoteFeedProtocol`;
|
||||
- получение зарегистрированного Feed по имени источника;
|
||||
- нормализацию внешних пробелов имени источника;
|
||||
- запрет пустого имени;
|
||||
- запрет повторной регистрации;
|
||||
- runtime-проверку соответствия `QuoteFeedProtocol`;
|
||||
- сохранение identity зарегистрированного объекта.
|
||||
|
||||
Ошибки registry представлены специализированным типом:
|
||||
|
||||
```python
|
||||
QuoteFeedRegistryError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Quote Acquisition Service
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/service.py
|
||||
```
|
||||
|
||||
добавлен отдельный прикладной сервис:
|
||||
|
||||
```python
|
||||
QuoteAcquisitionService
|
||||
```
|
||||
|
||||
Основная операция:
|
||||
|
||||
```python
|
||||
load_quote(
|
||||
source_name: str,
|
||||
symbol: str,
|
||||
) -> Quote
|
||||
```
|
||||
|
||||
Внутренняя последовательность:
|
||||
|
||||
```text
|
||||
source_name
|
||||
↓
|
||||
QuoteFeedRegistry.get(source_name)
|
||||
↓
|
||||
QuoteFeedProtocol
|
||||
↓
|
||||
load_quote(symbol)
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Сервис:
|
||||
|
||||
- выбирает Feed через registry;
|
||||
- передаёт `symbol` выбранному Feed без изменения;
|
||||
- возвращает канонический `Quote`;
|
||||
- не копирует полученную модель;
|
||||
- не выполняет retry;
|
||||
- не перехватывает и не переоборачивает ошибки registry или Feed.
|
||||
|
||||
Существующий:
|
||||
|
||||
```python
|
||||
InstrumentAcquisitionService
|
||||
```
|
||||
|
||||
не изменяет свою ответственность и продолжает обслуживать Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
## 8. Dependency Injection
|
||||
|
||||
В Build 030 сохранён уже применяемый в Instrument Reference Data подход явной сборки зависимостей.
|
||||
|
||||
Пример архитектурной сборки:
|
||||
|
||||
```python
|
||||
source = DzengiQuoteDocumentSource(...)
|
||||
handler = DzengiQuoteDocumentHandler(...)
|
||||
feed = QuotesFeed(
|
||||
source=source,
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
registry = QuoteFeedRegistry()
|
||||
registry.register("dzengi", feed)
|
||||
|
||||
service = QuoteAcquisitionService(
|
||||
registry=registry,
|
||||
)
|
||||
```
|
||||
|
||||
В Build намеренно не добавлены:
|
||||
|
||||
- глобальный singleton registry;
|
||||
- автоматическая регистрация при импорте;
|
||||
- скрытая сборка production pipeline внутри `QuoteAcquisitionService`;
|
||||
- глобальное mutable-состояние для Feed.
|
||||
|
||||
Такое решение сохраняет:
|
||||
|
||||
- dependency injection;
|
||||
- тестируемость;
|
||||
- явные зависимости;
|
||||
- изоляцию composition root от application service.
|
||||
|
||||
Фактическое подключение production pipeline к legacy facade отложено до Build 031.
|
||||
|
||||
---
|
||||
|
||||
## 9. Ответственности компонентов
|
||||
|
||||
| Компонент | Ответственность |
|
||||
|---|---|
|
||||
| `DzengiQuoteDocumentSource` | Получение сырого REST-документа котировки |
|
||||
| `DzengiQuoteDocumentHandler` | Полная обработка документа до канонической модели |
|
||||
| `QuotesFeed` | Оркестрация source → handler |
|
||||
| `QuoteFeedRegistry` | Регистрация и выбор Quotes Feed |
|
||||
| `QuoteAcquisitionService` | Прикладная точка получения `Quote` через выбранный Feed |
|
||||
| `Quote` | Каноническое внутреннее представление текущей котировки |
|
||||
|
||||
---
|
||||
|
||||
## 10. Полная цепочка обработки
|
||||
|
||||
После завершения Build 030 REST Quotes Feed имеет следующую структуру:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
raw object
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
validate_dzengi_quote_schema()
|
||||
↓
|
||||
parse_dzengi_quote_document()
|
||||
↓
|
||||
DzengiQuotePayload
|
||||
↓
|
||||
validate_dzengi_quote_values()
|
||||
↓
|
||||
map_dzengi_quote()
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
```
|
||||
|
||||
Таким образом, транспортный формат Dzengi полностью изолирован от внешних потребителей Acquisition.
|
||||
|
||||
---
|
||||
|
||||
## 11. Архитектурные ограничения
|
||||
|
||||
Build 030 намеренно не реализует следующие функции:
|
||||
|
||||
```text
|
||||
ExchangeService facade integration
|
||||
Quote Store
|
||||
MarketPriceCache migration
|
||||
WebSocket quote parsing
|
||||
market runtime migration
|
||||
read-only consumer migration
|
||||
UI consumer migration
|
||||
Execution consumer migration
|
||||
legacy TickerPrice removal
|
||||
legacy market snapshot dict removal
|
||||
MarketPriceCache removal
|
||||
```
|
||||
|
||||
Они относятся к следующим этапам:
|
||||
|
||||
```text
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Тестовое покрытие
|
||||
|
||||
Build 030 покрывает следующие сценарии.
|
||||
|
||||
### 12.1. REST source
|
||||
|
||||
Проверяется:
|
||||
|
||||
- использование endpoint `/api/v1/ticker/24hr`;
|
||||
- передача `symbol` в query parameters;
|
||||
- возврат исходного payload;
|
||||
- однократный вызов REST-клиента;
|
||||
- поддержка dependency injection REST-клиента;
|
||||
- создание стандартного REST-клиента при отсутствии injected client;
|
||||
- преобразование транспортной ошибки в `QuoteTransportError`;
|
||||
- сохранение исходной ошибки через `__cause__`.
|
||||
|
||||
### 12.2. Quotes Feed
|
||||
|
||||
Проверяется:
|
||||
|
||||
- соответствие `QuoteFeedProtocol`;
|
||||
- однократный вызов source;
|
||||
- передача `symbol` без изменения;
|
||||
- однократный вызов handler;
|
||||
- передача исходного документа handler без изменения;
|
||||
- возврат `Quote` без копирования;
|
||||
- отсутствие retry;
|
||||
- отсутствие повторного переоборачивания ошибок.
|
||||
|
||||
### 12.3. Quote Feed Registry
|
||||
|
||||
Проверяется:
|
||||
|
||||
- регистрация корректного Feed;
|
||||
- получение Feed по имени;
|
||||
- нормализация внешних пробелов имени;
|
||||
- запрет пустого имени;
|
||||
- запрет повторной регистрации;
|
||||
- проверка соответствия `QuoteFeedProtocol`;
|
||||
- сохранение identity объекта;
|
||||
- специализированные ошибки registry.
|
||||
|
||||
### 12.4. Quote Acquisition Service
|
||||
|
||||
Проверяется:
|
||||
|
||||
- передача `source_name` registry;
|
||||
- передача `symbol` Feed без изменения;
|
||||
- однократное обращение к registry;
|
||||
- однократный вызов Feed;
|
||||
- возврат `Quote` без копирования;
|
||||
- сохранение ошибок registry;
|
||||
- сохранение ошибок Feed;
|
||||
- отсутствие retry.
|
||||
|
||||
---
|
||||
|
||||
## 13. Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py \
|
||||
src/market_data/acquisition/feeds/quotes_feed.py \
|
||||
src/market_data/acquisition/registry.py \
|
||||
src/market_data/acquisition/service.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
|
||||
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \
|
||||
tests/unit/market_data/acquisition/test_registry.py \
|
||||
tests/unit/market_data/acquisition/test_service.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Успешно.
|
||||
Ошибок компиляции нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
|
||||
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \
|
||||
tests/unit/market_data/acquisition/test_registry.py \
|
||||
tests/unit/market_data/acquisition/test_service.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
89 passed in 0.06s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
498 passed in 0.25s
|
||||
```
|
||||
|
||||
Регрессий не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## 16. Результат Build
|
||||
|
||||
Build 030 завершён полностью.
|
||||
|
||||
Создана завершённая и протестированная вертикаль REST Quotes Feed:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Новая вертикаль пока работает независимо от legacy runtime, что обеспечивает безопасную поэтапную миграцию без изменения поведения работающего торгового бота.
|
||||
|
||||
---
|
||||
|
||||
## 17. Следующий этап
|
||||
|
||||
Следующий этап утверждённого плана:
|
||||
|
||||
```text
|
||||
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
|
||||
```
|
||||
|
||||
Его цель — переключить REST-получение текущей котировки внутри существующего `ExchangeService` на новый канонический Quotes Feed, сохранив текущие публичные интерфейсы и поведение legacy-потребителей.
|
||||
|
||||
Целевая переходная схема:
|
||||
|
||||
```text
|
||||
Legacy consumer
|
||||
↓
|
||||
ExchangeService facade
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
Dzengi /api/v1/ticker/24hr
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
legacy-compatible projection
|
||||
↓
|
||||
Legacy consumer
|
||||
```
|
||||
|
||||
До завершения последующих этапов `ExchangeService` остаётся совместимым фасадом между новой архитектурой Market Data Acquisition и существующими потребителями работающего бота.
|
||||
894
docs/migrations/build_031.md
Normal file
894
docs/migrations/build_031.md
Normal file
@@ -0,0 +1,894 @@
|
||||
# Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
|
||||
|
||||
## Статус
|
||||
|
||||
**Завершён.**
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Подключить новый канонический контур **Quotes Feed** как внутренний источник свежих REST-котировок для существующего `ExchangeService`, сохранив без изменений его публичные legacy-контракты и поведение существующих потребителей.
|
||||
|
||||
Основная архитектурная цель Build 031:
|
||||
|
||||
```text
|
||||
Dzengi GET /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
ExchangeService legacy facade
|
||||
↓
|
||||
существующие потребители
|
||||
```
|
||||
|
||||
После Build 031 `ExchangeService` больше не должен самостоятельно:
|
||||
|
||||
- выполнять прямой REST-запрос к `/api/v1/ticker/24hr`;
|
||||
- знать транспортные поля `lastPrice`, `bidPrice`, `askPrice`, `closeTime`;
|
||||
- разбирать сырой ответ ticker endpoint;
|
||||
- выполнять собственный parsing котировки Dzengi.
|
||||
|
||||
Эти обязанности переданы специализированной подсистеме `market_data/acquisition`.
|
||||
|
||||
---
|
||||
|
||||
## Исходное состояние
|
||||
|
||||
До Build 031 метод:
|
||||
|
||||
```python
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
```
|
||||
|
||||
самостоятельно выполнял полный legacy-процесс:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
↓
|
||||
ExchangeRestClient
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
ручное чтение lastPrice / bidPrice / askPrice / closeTime
|
||||
↓
|
||||
legacy dict snapshot
|
||||
```
|
||||
|
||||
В результате `ExchangeService` одновременно отвечал за:
|
||||
|
||||
- транспорт;
|
||||
- знание конкретного endpoint Dzengi;
|
||||
- знание транспортной схемы Dzengi;
|
||||
- parsing значений;
|
||||
- формирование внутреннего представления котировки;
|
||||
- формирование legacy snapshot;
|
||||
- обработку freshness.
|
||||
|
||||
Это нарушало архитектурное разделение ответственности.
|
||||
|
||||
К моменту начала Build 031 новый канонический Quotes Feed уже был реализован:
|
||||
|
||||
```text
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Задачей Build 031 стало подключение этого контура под существующий `ExchangeService` facade.
|
||||
|
||||
---
|
||||
|
||||
## Объём изменений
|
||||
|
||||
### Изменён production-файл
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
### Добавлен тестовый файл
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_quotes_facade.py
|
||||
```
|
||||
|
||||
### Не изменялись
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/mock_data.py
|
||||
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
src/market_data/acquisition/feeds/quotes_feed.py
|
||||
src/market_data/acquisition/handlers/quotes_handler.py
|
||||
src/market_data/acquisition/models/quote.py
|
||||
src/market_data/acquisition/registry.py
|
||||
src/market_data/acquisition/service.py
|
||||
src/market_data/acquisition/exceptions.py
|
||||
```
|
||||
|
||||
Новый Acquisition-контур уже содержал всю необходимую функциональность и не потребовал дополнительных изменений.
|
||||
|
||||
---
|
||||
|
||||
## Реализованная архитектура
|
||||
|
||||
После Build 031 получение свежей REST-котировки выполняется по следующей цепочке:
|
||||
|
||||
```text
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
legacy-compatible snapshot dict
|
||||
```
|
||||
|
||||
Таким образом, граница ответственности теперь выглядит следующим образом:
|
||||
|
||||
```text
|
||||
market_data/acquisition
|
||||
│
|
||||
│ отвечает за получение, проверку,
|
||||
│ parsing и mapping котировки
|
||||
↓
|
||||
canonical Quote
|
||||
│
|
||||
│ временная compatibility boundary
|
||||
↓
|
||||
ExchangeService facade
|
||||
│
|
||||
│ сохраняет старые публичные контракты
|
||||
↓
|
||||
legacy consumers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Подключение Quote Acquisition pipeline
|
||||
|
||||
В `ExchangeService` добавлен внутренний путь получения канонической котировки через уже реализованные компоненты Quotes Feed.
|
||||
|
||||
Используемая цепочка:
|
||||
|
||||
```text
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
`ExchangeService` теперь получает готовую каноническую модель:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
вместо сырого ответа Dzengi:
|
||||
|
||||
```python
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
Это устраняет зависимость facade от транспортной схемы `ticker/24hr`.
|
||||
|
||||
---
|
||||
|
||||
## Изменение get_fresh_market_snapshot()
|
||||
|
||||
До Build 031 метод самостоятельно выполнял:
|
||||
|
||||
```text
|
||||
создание ExchangeRestClient
|
||||
↓
|
||||
вызов /api/v1/ticker/24hr
|
||||
↓
|
||||
чтение lastPrice
|
||||
↓
|
||||
чтение bidPrice
|
||||
↓
|
||||
чтение askPrice
|
||||
↓
|
||||
чтение closeTime / eventTime
|
||||
↓
|
||||
преобразование значений
|
||||
↓
|
||||
формирование snapshot
|
||||
```
|
||||
|
||||
После Build 031 метод получает:
|
||||
|
||||
```python
|
||||
quote = self._load_quote_via_acquisition(
|
||||
validation.normalized_symbol,
|
||||
)
|
||||
```
|
||||
|
||||
После чего выполняет только временную legacy-проекцию:
|
||||
|
||||
```text
|
||||
Quote
|
||||
↓
|
||||
legacy-compatible dict snapshot
|
||||
```
|
||||
|
||||
Таким образом, `get_fresh_market_snapshot()` больше не является parser транспортного ответа Dzengi.
|
||||
|
||||
---
|
||||
|
||||
## Удалённый legacy parsing
|
||||
|
||||
Из REST quote-пути `ExchangeService` удалено прямое знание следующих транспортных полей:
|
||||
|
||||
```text
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
closeTime
|
||||
eventTime
|
||||
```
|
||||
|
||||
Также удалён прямой вызов:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
```
|
||||
|
||||
из `ExchangeService`.
|
||||
|
||||
Теперь endpoint и его транспортная схема принадлежат исключительно адаптеру:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/
|
||||
```
|
||||
|
||||
Это соответствует утверждённой архитектуре Acquisition.
|
||||
|
||||
---
|
||||
|
||||
## Legacy-compatible projection
|
||||
|
||||
Build 031 намеренно не удаляет legacy snapshot layer.
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
временно преобразуется обратно в:
|
||||
|
||||
```python
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
с сохранением прежней структуры:
|
||||
|
||||
```python
|
||||
{
|
||||
"symbol": ...,
|
||||
"last_price": ...,
|
||||
"bid_price": ...,
|
||||
"ask_price": ...,
|
||||
"updated_at": ...,
|
||||
"source": "fresh_rest",
|
||||
"age_seconds": ...,
|
||||
"is_fresh": ...,
|
||||
}
|
||||
```
|
||||
|
||||
Это необходимо для безопасной поэтапной миграции существующего работающего бота.
|
||||
|
||||
Удаление этого compatibility layer запланировано на:
|
||||
|
||||
```text
|
||||
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сохранение числового контракта
|
||||
|
||||
Каноническая модель `Quote` использует точные числовые значения, представленные через `Decimal`.
|
||||
|
||||
Legacy-потребители ожидают `float`.
|
||||
|
||||
Поэтому на временной границе совместимости выполняется преобразование:
|
||||
|
||||
```text
|
||||
Quote Decimal
|
||||
↓
|
||||
ExchangeService compatibility boundary
|
||||
↓
|
||||
legacy float
|
||||
```
|
||||
|
||||
То есть точность сохраняется внутри новой канонической подсистемы, а преобразование выполняется только при передаче данных старым потребителям.
|
||||
|
||||
Это временное решение до полного перевода потребителей на канонический `Quote`.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение symbol contract
|
||||
|
||||
Перед получением котировки сохраняется существующая проверка символа:
|
||||
|
||||
```python
|
||||
validation = self.validate_symbol(symbol_to_use)
|
||||
```
|
||||
|
||||
В новый Acquisition pipeline передаётся:
|
||||
|
||||
```python
|
||||
validation.normalized_symbol
|
||||
```
|
||||
|
||||
Таким образом:
|
||||
|
||||
- невалидный символ не передаётся в Quotes Feed;
|
||||
- используется канонически нормализованный символ;
|
||||
- существующее поведение `ExchangeService` сохраняется.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение source contract
|
||||
|
||||
Канонический `Quote` содержит источник Acquisition:
|
||||
|
||||
```text
|
||||
dzengi
|
||||
```
|
||||
|
||||
Однако существующий legacy snapshot использует:
|
||||
|
||||
```text
|
||||
fresh_rest
|
||||
```
|
||||
|
||||
В Build 031 сохранено прежнее значение:
|
||||
|
||||
```python
|
||||
"source": "fresh_rest"
|
||||
```
|
||||
|
||||
Это исключает непреднамеренное изменение поведения:
|
||||
|
||||
- UI;
|
||||
- журналирования;
|
||||
- диагностики;
|
||||
- runtime;
|
||||
- существующих потребителей, потенциально зависящих от значения `source`.
|
||||
|
||||
Переход на каноническую семантику источника должен выполняться отдельно при удалении legacy snapshot layer.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение timestamp contract
|
||||
|
||||
Канонический `Quote` содержит timezone-aware timestamp.
|
||||
|
||||
На legacy-границе сохраняется прежнее представление:
|
||||
|
||||
```text
|
||||
Quote.exchange_timestamp
|
||||
↓
|
||||
timestamp в миллисекундах
|
||||
↓
|
||||
существующие ExchangeService helpers
|
||||
↓
|
||||
updated_at
|
||||
age_seconds
|
||||
is_fresh
|
||||
```
|
||||
|
||||
Благодаря этому существующие потребители не получают изменения временной семантики.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение freshness contract
|
||||
|
||||
Сохранена существующая логика определения свежести REST-котировки.
|
||||
|
||||
Порог:
|
||||
|
||||
```text
|
||||
60 секунд
|
||||
```
|
||||
|
||||
Результат продолжает содержать:
|
||||
|
||||
```python
|
||||
"age_seconds": ...
|
||||
"is_fresh": ...
|
||||
```
|
||||
|
||||
Условие остаётся эквивалентным прежнему:
|
||||
|
||||
```python
|
||||
is_fresh = (
|
||||
age_seconds is not None
|
||||
and age_seconds <= 60
|
||||
)
|
||||
```
|
||||
|
||||
Build 031 не меняет политику freshness.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение публичных контрактов ExchangeService
|
||||
|
||||
После Build 031 сохранены без изменения следующие публичные методы:
|
||||
|
||||
```text
|
||||
get_price() -> TickerPrice
|
||||
|
||||
get_fresh_market_snapshot() -> dict[str, object]
|
||||
|
||||
refresh_price_cache() -> TickerPrice
|
||||
|
||||
refresh_market_snapshot_cache() -> dict[str, object]
|
||||
|
||||
get_market_snapshot() -> dict[str, object]
|
||||
|
||||
get_execution_snapshot() -> ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
Это позволяет существующим потребителям продолжать работу без изменений.
|
||||
|
||||
В частности, не потребовалось изменять:
|
||||
|
||||
```text
|
||||
src/telegram/ui/currency_ui.py
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/auto/execution_quality.py
|
||||
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
|
||||
src/trading/execution/pricing.py
|
||||
src/trading/diagnostics/snapshot.py
|
||||
src/trading/debug/execution.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Влияние на существующие методы ExchangeService
|
||||
|
||||
Методы:
|
||||
|
||||
```text
|
||||
get_price()
|
||||
get_market_snapshot()
|
||||
get_execution_snapshot()
|
||||
refresh_market_snapshot_cache()
|
||||
refresh_price_cache()
|
||||
_get_real_price()
|
||||
get_symbol_runtime_status()
|
||||
```
|
||||
|
||||
продолжают работать через существующие публичные и внутренние контракты.
|
||||
|
||||
Поскольку свежая REST-котировка теперь поступает через:
|
||||
|
||||
```text
|
||||
get_fresh_market_snapshot()
|
||||
↓
|
||||
Quote Acquisition pipeline
|
||||
```
|
||||
|
||||
существующие методы автоматически используют новый канонический REST Quotes Feed без прямого перевода каждого потребителя.
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
Новый Acquisition-контур использует специализированные ошибки quote-подсистемы.
|
||||
|
||||
Внешний контракт `ExchangeService` продолжает использовать:
|
||||
|
||||
```python
|
||||
ExchangeError
|
||||
```
|
||||
|
||||
Поэтому на границе facade сохраняется адаптация:
|
||||
|
||||
```text
|
||||
Acquisition error
|
||||
↓
|
||||
ExchangeService compatibility boundary
|
||||
↓
|
||||
ExchangeError
|
||||
```
|
||||
|
||||
При этом исходная ошибка сохраняется как:
|
||||
|
||||
```python
|
||||
__cause__
|
||||
```
|
||||
|
||||
Это обеспечивает одновременно:
|
||||
|
||||
- совместимость существующих потребителей;
|
||||
- сохранение исходного контекста ошибки;
|
||||
- возможность диагностики первопричины;
|
||||
- отсутствие утечки новой модели исключений в legacy-код раньше запланированного этапа миграции.
|
||||
|
||||
---
|
||||
|
||||
## Сохранение mock-режима
|
||||
|
||||
При отключённой реальной биржевой интеграции:
|
||||
|
||||
```python
|
||||
exchange_enabled = False
|
||||
```
|
||||
|
||||
новый Acquisition pipeline не вызывается.
|
||||
|
||||
Сохраняется прежний mock-контур:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
↓
|
||||
mock_ticker_price()
|
||||
↓
|
||||
legacy snapshot
|
||||
```
|
||||
|
||||
Build 031 не изменяет поведение mock-режима.
|
||||
|
||||
---
|
||||
|
||||
## Тестовое покрытие
|
||||
|
||||
Добавлен специализированный тестовый файл:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_quotes_facade.py
|
||||
```
|
||||
|
||||
Тесты проверяют границу между:
|
||||
|
||||
```text
|
||||
canonical Quote Acquisition
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
legacy ExchangeService facade
|
||||
```
|
||||
|
||||
Проверяемые свойства включают:
|
||||
|
||||
- использование нового Acquisition pipeline;
|
||||
- передачу нормализованного символа;
|
||||
- сохранение legacy snapshot contract;
|
||||
- преобразование канонических числовых значений в legacy-compatible значения;
|
||||
- сохранение `source="fresh_rest"`;
|
||||
- сохранение timestamp contract;
|
||||
- сохранение freshness contract;
|
||||
- адаптацию ошибок в `ExchangeError`;
|
||||
- сохранение исходной ошибки в `__cause__`;
|
||||
- отсутствие вызова нового Acquisition pipeline в mock-режиме;
|
||||
- отсутствие прямого ticker REST parsing в новом facade-пути.
|
||||
|
||||
---
|
||||
|
||||
## Регрессионная проверка runtime status
|
||||
|
||||
Дополнительно выполнен существующий набор тестов:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py
|
||||
```
|
||||
|
||||
Он подтверждает сохранение внешнего runtime-контракта после переключения внутреннего источника REST-котировки.
|
||||
|
||||
---
|
||||
|
||||
## Выполненные проверки
|
||||
|
||||
### Проверка синтаксиса
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/service.py \
|
||||
tests/unit/integrations/exchange/test_service_quotes_facade.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
### Специализированные и регрессионные тесты
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_quotes_facade.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
38 passed in 0.08s
|
||||
```
|
||||
|
||||
### Полная регрессия проекта
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
502 passed in 0.25s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рост тестового покрытия
|
||||
|
||||
После предыдущего этапа:
|
||||
|
||||
```text
|
||||
Build 030
|
||||
498 passed
|
||||
```
|
||||
|
||||
После завершения Build 031:
|
||||
|
||||
```text
|
||||
Build 031
|
||||
502 passed
|
||||
```
|
||||
|
||||
Добавлено:
|
||||
|
||||
```text
|
||||
4 новых теста
|
||||
```
|
||||
|
||||
Полная регрессия остаётся зелёной.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
### До Build 031
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
↓
|
||||
ExchangeRestClient
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
ручной parsing полей Dzengi
|
||||
↓
|
||||
legacy dict snapshot
|
||||
↓
|
||||
потребители
|
||||
```
|
||||
|
||||
### После Build 031
|
||||
|
||||
```text
|
||||
ExchangeService legacy facade
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
legacy-compatible dict projection
|
||||
↓
|
||||
существующие потребители
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные гарантии после Build 031
|
||||
|
||||
После завершения этапа выполняются следующие гарантии:
|
||||
|
||||
1. `ExchangeService` больше не выполняет прямой REST-запрос к `/api/v1/ticker/24hr`.
|
||||
|
||||
2. `ExchangeService` больше не знает транспортные поля:
|
||||
|
||||
```text
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
closeTime
|
||||
eventTime
|
||||
```
|
||||
|
||||
3. REST-котировка проходит через канонический Quotes Feed.
|
||||
|
||||
4. Внутренним результатом Acquisition является:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
5. Legacy snapshot создаётся только как временная compatibility projection.
|
||||
|
||||
6. Существующие публичные контракты `ExchangeService` сохранены.
|
||||
|
||||
7. `MarketPriceCache` пока не изменён.
|
||||
|
||||
8. WebSocket-контур пока не изменён.
|
||||
|
||||
9. UI-потребители пока не переведены напрямую на `Quote`.
|
||||
|
||||
10. Execution-потребители пока не переведены напрямую на `Quote`.
|
||||
|
||||
11. Полная регрессия проекта проходит успешно:
|
||||
|
||||
```text
|
||||
502 passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что намеренно не входит в Build 031
|
||||
|
||||
Build 031 не реализует:
|
||||
|
||||
```text
|
||||
Quote Store
|
||||
перенос MarketPriceCache
|
||||
удаление MarketPriceCache
|
||||
изменение MarketPriceSnapshot
|
||||
WebSocket quote parsing
|
||||
WebSocket quote adapter
|
||||
перевод market runtime на Quotes Feed
|
||||
прямой перевод UI на Quote
|
||||
прямой перевод execution на Quote
|
||||
удаление TickerPrice
|
||||
удаление ExecutionPriceSnapshot
|
||||
удаление legacy market snapshot dict layer
|
||||
удаление legacy quote compatibility layer
|
||||
```
|
||||
|
||||
Эти изменения выполняются последующими Build по утверждённому плану.
|
||||
|
||||
---
|
||||
|
||||
## Следующий этап
|
||||
|
||||
```text
|
||||
Build 032 — Канонический Quote Store
|
||||
```
|
||||
|
||||
Его задача — создать канонический слой хранения текущих котировок, который станет основой для последующего переноса существующего:
|
||||
|
||||
```text
|
||||
MarketPriceCache
|
||||
```
|
||||
|
||||
на новую архитектуру.
|
||||
|
||||
Последовательность дальнейшей миграции:
|
||||
|
||||
```text
|
||||
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 031 завершён полностью.**
|
||||
|
||||
Новый канонический REST Quotes Feed стал внутренним источником свежих котировок для `ExchangeService`, при этом существующий работающий бот сохранил прежние публичные контракты.
|
||||
|
||||
Ключевой результат:
|
||||
|
||||
```text
|
||||
Было:
|
||||
|
||||
ExchangeService
|
||||
↓
|
||||
прямой REST ticker/24hr
|
||||
↓
|
||||
ручной parsing
|
||||
↓
|
||||
legacy snapshot
|
||||
```
|
||||
|
||||
```text
|
||||
Стало:
|
||||
|
||||
ExchangeService facade
|
||||
↓
|
||||
canonical Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
временная legacy projection
|
||||
↓
|
||||
существующие потребители
|
||||
```
|
||||
|
||||
Это создаёт безопасную архитектурную основу для следующего этапа:
|
||||
|
||||
```text
|
||||
Build 032 — Канонический Quote Store
|
||||
```
|
||||
1033
docs/migrations/build_032.md
Normal file
1033
docs/migrations/build_032.md
Normal file
File diff suppressed because it is too large
Load Diff
864
docs/migrations/build_033.md
Normal file
864
docs/migrations/build_033.md
Normal file
@@ -0,0 +1,864 @@
|
||||
# Build 033 — Перенос MarketPriceCache на Quote Store
|
||||
|
||||
**Engineering Build Record**
|
||||
|
||||
---
|
||||
|
||||
## Контроль документа
|
||||
|
||||
| Свойство | Значение |
|
||||
|---|---|
|
||||
| Документ | Build 033 — Перенос MarketPriceCache на Quote Store |
|
||||
| Тип документа | Engineering Build Record |
|
||||
| Статус | **Completed** |
|
||||
| Подсистема | Market Data / Storage / Legacy Exchange Integration |
|
||||
| Проект | Dzentra |
|
||||
| Язык | Русский |
|
||||
| Предыдущий этап | Build 032 — Канонический Quote Store |
|
||||
| Следующий этап | Build 034 — Dzengi WebSocket quote parsing и адаптер |
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение Build
|
||||
|
||||
Цель Build 033 — перевести legacy-компонент `MarketPriceCache` с собственного внутреннего хранилища котировок на канонический `Quote Store`, сохранив полную обратную совместимость с существующими потребителями.
|
||||
|
||||
До Build 033 `MarketPriceCache` самостоятельно владел runtime-состоянием котировок:
|
||||
|
||||
```python
|
||||
_prices: dict[tuple[str, str], MarketPriceSnapshot] = {}
|
||||
```
|
||||
|
||||
Это создавало отдельный контур хранения рыночных цен параллельно с введённым в Build 032 каноническим `Quote Store`.
|
||||
|
||||
После Build 033 единственным владельцем состояния котировок, доступных через `MarketPriceCache`, становится канонический `Quote Store`.
|
||||
|
||||
Целевая переходная архитектура:
|
||||
|
||||
```text
|
||||
Legacy consumers
|
||||
│
|
||||
▼
|
||||
MarketPriceCache
|
||||
compatibility facade
|
||||
│
|
||||
▼
|
||||
Canonical Quote
|
||||
│
|
||||
▼
|
||||
InMemoryQuoteStore
|
||||
```
|
||||
|
||||
Сам `MarketPriceCache` сохраняется временно как compatibility facade до его окончательного удаления на Build 039.
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектурный контекст
|
||||
|
||||
Build 033 является частью последовательного перехода Quotes Feed на новую архитектуру Market Data Acquisition:
|
||||
|
||||
```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 033 не переводит непосредственных потребителей `MarketPriceCache` на новые API. Эта миграция выполняется последующими Build.
|
||||
|
||||
Задача текущего этапа — устранить независимое legacy-хранилище котировок без нарушения работы существующего бота.
|
||||
|
||||
---
|
||||
|
||||
## 3. Исходное состояние
|
||||
|
||||
До Build 033 класс:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
содержал собственное class-level хранилище:
|
||||
|
||||
```python
|
||||
class MarketPriceCache:
|
||||
_prices: dict[tuple[str, str], MarketPriceSnapshot] = {}
|
||||
```
|
||||
|
||||
Ключ записи формировался из:
|
||||
|
||||
```text
|
||||
(runtime_key, symbol)
|
||||
```
|
||||
|
||||
`MarketPriceCache` самостоятельно выполнял:
|
||||
|
||||
- запись текущей цены;
|
||||
- хранение `bid_price`;
|
||||
- хранение `ask_price`;
|
||||
- хранение `updated_at`;
|
||||
- хранение фактического источника данных;
|
||||
- изоляцию по `runtime_key`;
|
||||
- вычисление возраста snapshot;
|
||||
- очистку записей по символу и runtime.
|
||||
|
||||
При этом после Build 032 уже существовал канонический:
|
||||
|
||||
```text
|
||||
InMemoryQuoteStore
|
||||
```
|
||||
|
||||
работающий с моделью:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
Таким образом, существовали два отдельных механизма хранения котировок:
|
||||
|
||||
```text
|
||||
MarketPriceCache
|
||||
│
|
||||
└── собственный dict[tuple[str, str], MarketPriceSnapshot]
|
||||
|
||||
Quote Store
|
||||
│
|
||||
└── каноническое хранилище Quote
|
||||
```
|
||||
|
||||
Build 033 устранил это дублирование для контура `MarketPriceCache`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Выполненные изменения
|
||||
|
||||
### 4.1. Изменённый исходный файл
|
||||
|
||||
Изменён:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
### 4.2. Добавленный тестовый файл
|
||||
|
||||
Добавлен:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_market_cache.py
|
||||
```
|
||||
|
||||
### 4.3. Файлы, не потребовавшие изменений
|
||||
|
||||
В рамках Build 033 не изменялись:
|
||||
|
||||
```text
|
||||
src/storage/quote_store.py
|
||||
src/storage/exceptions.py
|
||||
src/market_data/acquisition/models/quote.py
|
||||
tests/unit/storage/test_quote_store.py
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Это подтверждает сохранение существующих публичных контрактов и минимальный scope миграции.
|
||||
|
||||
---
|
||||
|
||||
## 5. Новая роль MarketPriceCache
|
||||
|
||||
После Build 033 `MarketPriceCache` больше не является самостоятельным владельцем runtime-состояния котировок.
|
||||
|
||||
Его новая роль:
|
||||
|
||||
```text
|
||||
Legacy compatibility facade
|
||||
```
|
||||
|
||||
Он обеспечивает совместимость между существующими legacy-потребителями и канонической моделью хранения котировок.
|
||||
|
||||
Логика записи:
|
||||
|
||||
```text
|
||||
Legacy caller
|
||||
│
|
||||
▼
|
||||
MarketPriceCache.set_price(...)
|
||||
│
|
||||
▼
|
||||
Canonical Quote
|
||||
│
|
||||
▼
|
||||
QuoteStoreProtocol.set(...)
|
||||
```
|
||||
|
||||
Логика чтения:
|
||||
|
||||
```text
|
||||
Legacy caller
|
||||
│
|
||||
▼
|
||||
MarketPriceCache.get_price(...)
|
||||
│
|
||||
▼
|
||||
QuoteStoreProtocol.get(...)
|
||||
│
|
||||
▼
|
||||
Canonical Quote
|
||||
│
|
||||
▼
|
||||
MarketPriceSnapshot
|
||||
│
|
||||
▼
|
||||
Legacy caller
|
||||
```
|
||||
|
||||
Таким образом, `MarketPriceSnapshot` остаётся только временной compatibility model.
|
||||
|
||||
---
|
||||
|
||||
## 6. Устранение собственного хранилища MarketPriceCache
|
||||
|
||||
До Build 033:
|
||||
|
||||
```python
|
||||
_prices: dict[tuple[str, str], MarketPriceSnapshot] = {}
|
||||
```
|
||||
|
||||
После Build 033 `MarketPriceCache` использует канонический контракт:
|
||||
|
||||
```text
|
||||
QuoteStoreProtocol
|
||||
```
|
||||
|
||||
и реализацию:
|
||||
|
||||
```text
|
||||
InMemoryQuoteStore
|
||||
```
|
||||
|
||||
Собственное независимое хранилище `_prices` устранено.
|
||||
|
||||
Это является главным архитектурным результатом Build 033.
|
||||
|
||||
---
|
||||
|
||||
## 7. Преобразование legacy-входа в канонический Quote
|
||||
|
||||
Публичный legacy-контракт записи сохранён:
|
||||
|
||||
```python
|
||||
MarketPriceCache.set_price(
|
||||
symbol=...,
|
||||
price=...,
|
||||
bid_price=...,
|
||||
ask_price=...,
|
||||
updated_at=...,
|
||||
source=...,
|
||||
runtime_key=...,
|
||||
)
|
||||
```
|
||||
|
||||
Внутри compatibility facade эти данные преобразуются в каноническую модель:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
Основное соответствие полей:
|
||||
|
||||
| Legacy `MarketPriceCache` | Канонический `Quote` |
|
||||
|---|---|
|
||||
| `symbol` | `symbol` |
|
||||
| `price` | `last_price` |
|
||||
| `bid_price` | `bid_price` |
|
||||
| `ask_price` | `ask_price` |
|
||||
| `updated_at` | каноническое timestamp-представление |
|
||||
| `source` | `source` |
|
||||
| время получения | `received_at` |
|
||||
|
||||
На legacy-границе сохраняется использование `float`.
|
||||
|
||||
Внутри канонической модели используются точные числовые значения `Decimal`.
|
||||
|
||||
Таким образом, преобразование имеет вид:
|
||||
|
||||
```text
|
||||
Legacy float values
|
||||
│
|
||||
▼
|
||||
MarketPriceCache
|
||||
│
|
||||
▼
|
||||
Decimal values
|
||||
│
|
||||
▼
|
||||
Canonical Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Чтение через MarketPriceSnapshot
|
||||
|
||||
Существующие потребители ожидают от:
|
||||
|
||||
```python
|
||||
MarketPriceCache.get_price(...)
|
||||
```
|
||||
|
||||
объект:
|
||||
|
||||
```text
|
||||
MarketPriceSnapshot
|
||||
```
|
||||
|
||||
Поэтому Build 033 не удаляет эту модель.
|
||||
|
||||
При чтении выполняется обратное compatibility-преобразование:
|
||||
|
||||
```text
|
||||
QuoteStore
|
||||
│
|
||||
▼
|
||||
Quote
|
||||
│
|
||||
▼
|
||||
MarketPriceSnapshot
|
||||
```
|
||||
|
||||
Сохраняются legacy-поля:
|
||||
|
||||
```text
|
||||
symbol
|
||||
price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
runtime_key
|
||||
```
|
||||
|
||||
Также сохранены методы:
|
||||
|
||||
```python
|
||||
age_seconds()
|
||||
has_bid_ask()
|
||||
```
|
||||
|
||||
Благодаря этому существующие потребители не потребовали изменений.
|
||||
|
||||
---
|
||||
|
||||
## 9. Сохранение семантики свежести
|
||||
|
||||
Legacy-потребители используют:
|
||||
|
||||
```python
|
||||
cached_price.age_seconds()
|
||||
```
|
||||
|
||||
для определения возраста котировки.
|
||||
|
||||
Build 033 сохраняет этот публичный контракт.
|
||||
|
||||
Возраст snapshot определяется на основе канонической информации о времени получения котировки.
|
||||
|
||||
Таким образом, freshness-семантика больше не требует отдельного независимого хранилища состояния внутри `MarketPriceCache`.
|
||||
|
||||
Существующие вызовы:
|
||||
|
||||
```python
|
||||
cached_price.age_seconds()
|
||||
```
|
||||
|
||||
продолжают работать без изменений.
|
||||
|
||||
---
|
||||
|
||||
## 10. Сохранение семантики bid/ask
|
||||
|
||||
Legacy-модель предоставляет:
|
||||
|
||||
```python
|
||||
has_bid_ask()
|
||||
```
|
||||
|
||||
Этот контракт сохранён.
|
||||
|
||||
Он продолжает использоваться существующими execution-потребителями для проверки наличия корректных положительных значений:
|
||||
|
||||
```text
|
||||
bid_price
|
||||
ask_price
|
||||
```
|
||||
|
||||
Build 033 не требует изменения существующих потребителей этой проверки.
|
||||
|
||||
---
|
||||
|
||||
## 11. Изоляция runtime_key
|
||||
|
||||
Сохранена существующая изоляция котировок по:
|
||||
|
||||
```text
|
||||
runtime_key
|
||||
```
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
auto
|
||||
debug_auto
|
||||
default
|
||||
```
|
||||
|
||||
Котировки одного инструмента в разных runtime остаются независимыми.
|
||||
|
||||
Концептуальный ключ хранения:
|
||||
|
||||
```text
|
||||
source_name
|
||||
+
|
||||
runtime_key
|
||||
+
|
||||
symbol
|
||||
```
|
||||
|
||||
Это позволяет одновременно хранить:
|
||||
|
||||
```text
|
||||
BTC/USD_LEVERAGE + auto
|
||||
BTC/USD_LEVERAGE + debug_auto
|
||||
BTC/USD_LEVERAGE + default
|
||||
```
|
||||
|
||||
как независимые runtime-записи.
|
||||
|
||||
---
|
||||
|
||||
## 12. Нормализация runtime_key и symbol
|
||||
|
||||
Сохранено существующее поведение нормализации.
|
||||
|
||||
Символ нормализуется в uppercase:
|
||||
|
||||
```text
|
||||
btc/usd_leverage
|
||||
↓
|
||||
BTC/USD_LEVERAGE
|
||||
```
|
||||
|
||||
`runtime_key` нормализуется в lowercase:
|
||||
|
||||
```text
|
||||
AUTO
|
||||
↓
|
||||
auto
|
||||
```
|
||||
|
||||
Это сохраняет прежнюю семантику `MarketPriceCache`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Разделение source_name и Quote.source
|
||||
|
||||
Build 033 сохраняет архитектурное различие между:
|
||||
|
||||
```text
|
||||
source_name
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
Quote.source
|
||||
```
|
||||
|
||||
`source_name` определяет namespace хранения.
|
||||
|
||||
`Quote.source` определяет фактическое происхождение котировки.
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
Storage namespace:
|
||||
legacy-market-price-cache
|
||||
|
||||
Actual quote source:
|
||||
ws_depth:auto
|
||||
```
|
||||
|
||||
или:
|
||||
|
||||
```text
|
||||
Storage namespace:
|
||||
legacy-market-price-cache
|
||||
|
||||
Actual quote source:
|
||||
market-polling
|
||||
```
|
||||
|
||||
Это предотвращает смешивание:
|
||||
|
||||
- идентичности storage namespace;
|
||||
- provenance рыночных данных.
|
||||
|
||||
---
|
||||
|
||||
## 14. Сохранение семантики clear()
|
||||
|
||||
Полностью сохранены существующие варианты очистки.
|
||||
|
||||
### Полная очистка facade namespace
|
||||
|
||||
```python
|
||||
MarketPriceCache.clear()
|
||||
```
|
||||
|
||||
Очищает все записи, принадлежащие `MarketPriceCache`.
|
||||
|
||||
### Очистка символа во всех runtime
|
||||
|
||||
```python
|
||||
MarketPriceCache.clear("BTC/USD_LEVERAGE")
|
||||
```
|
||||
|
||||
Очищает указанный символ во всех runtime внутри namespace facade.
|
||||
|
||||
### Очистка runtime по всем символам
|
||||
|
||||
```python
|
||||
MarketPriceCache.clear(runtime_key="auto")
|
||||
```
|
||||
|
||||
Очищает все символы указанного runtime.
|
||||
|
||||
### Точечная очистка
|
||||
|
||||
```python
|
||||
MarketPriceCache.clear(
|
||||
"BTC/USD_LEVERAGE",
|
||||
runtime_key="auto",
|
||||
)
|
||||
```
|
||||
|
||||
Очищает только конкретную запись.
|
||||
|
||||
---
|
||||
|
||||
## 15. Изоляция от других владельцев Quote Store
|
||||
|
||||
Критически важное требование Build 033:
|
||||
|
||||
```text
|
||||
MarketPriceCache.clear()
|
||||
```
|
||||
|
||||
не должен удалять котировки, записанные другими владельцами или источниками в канонический `Quote Store`.
|
||||
|
||||
Поэтому операции facade ограничиваются собственным storage namespace.
|
||||
|
||||
Архитектурно:
|
||||
|
||||
```text
|
||||
Quote Store
|
||||
├── legacy-market-price-cache
|
||||
│ ├── auto
|
||||
│ ├── debug_auto
|
||||
│ └── default
|
||||
│
|
||||
└── other-source
|
||||
└── ...
|
||||
```
|
||||
|
||||
Очистка:
|
||||
|
||||
```python
|
||||
MarketPriceCache.clear()
|
||||
```
|
||||
|
||||
затрагивает только:
|
||||
|
||||
```text
|
||||
legacy-market-price-cache
|
||||
```
|
||||
|
||||
и не изменяет данные других namespace.
|
||||
|
||||
---
|
||||
|
||||
## 16. Обратная совместимость
|
||||
|
||||
Build 033 не изменил публичные вызовы:
|
||||
|
||||
```python
|
||||
MarketPriceCache.set_price(...)
|
||||
MarketPriceCache.get_price(...)
|
||||
MarketPriceCache.clear(...)
|
||||
```
|
||||
|
||||
Не изменены существующие production-потребители:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Также сохранены legacy-контракты:
|
||||
|
||||
```python
|
||||
MarketPriceSnapshot.age_seconds()
|
||||
MarketPriceSnapshot.has_bid_ask()
|
||||
```
|
||||
|
||||
Это позволило выполнить архитектурную миграцию без изменения поведения работающего бота.
|
||||
|
||||
---
|
||||
|
||||
## 17. Тестовое покрытие
|
||||
|
||||
Добавлен специализированный тестовый файл:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_market_cache.py
|
||||
```
|
||||
|
||||
Тестами проверяются:
|
||||
|
||||
- соответствие `MarketPriceCache` каноническому `QuoteStoreProtocol`;
|
||||
- запись канонического `Quote`;
|
||||
- чтение через legacy `MarketPriceSnapshot`;
|
||||
- сохранение `symbol`;
|
||||
- сохранение `price`;
|
||||
- сохранение `bid_price`;
|
||||
- сохранение `ask_price`;
|
||||
- сохранение `source`;
|
||||
- сохранение `runtime_key`;
|
||||
- нормализация символа;
|
||||
- нормализация `runtime_key`;
|
||||
- изоляция разных runtime;
|
||||
- изоляция разных символов;
|
||||
- замена предыдущей котировки новой;
|
||||
- полная очистка facade namespace;
|
||||
- очистка по символу;
|
||||
- очистка по runtime;
|
||||
- точечная очистка;
|
||||
- вычисление возраста snapshot;
|
||||
- legacy-проверка `has_bid_ask()`;
|
||||
- преобразование timestamp;
|
||||
- защита внешних записей другого `source_name` от очистки через `MarketPriceCache`.
|
||||
|
||||
---
|
||||
|
||||
## 18. Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/market_cache.py \
|
||||
tests/unit/integrations/exchange/test_market_cache.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
SUCCESS
|
||||
```
|
||||
|
||||
Ошибок компиляции нет.
|
||||
|
||||
---
|
||||
|
||||
## 19. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/storage/test_quote_store.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
76 passed in 0.04s
|
||||
```
|
||||
|
||||
Все специализированные тесты успешно пройдены.
|
||||
|
||||
---
|
||||
|
||||
## 20. Регрессионная проверка потребителей
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_quotes_facade.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
47 passed in 0.13s
|
||||
```
|
||||
|
||||
Регрессионный контур существующих потребителей полностью сохранён.
|
||||
|
||||
---
|
||||
|
||||
## 21. Полная регрессионная проверка проекта
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
578 passed in 0.28s
|
||||
```
|
||||
|
||||
Все тесты проекта успешно пройдены.
|
||||
|
||||
Регрессий не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## 22. Архитектурный результат
|
||||
|
||||
До Build 033:
|
||||
|
||||
```text
|
||||
Legacy consumers
|
||||
│
|
||||
▼
|
||||
MarketPriceCache
|
||||
│
|
||||
▼
|
||||
Private _prices dict
|
||||
│
|
||||
▼
|
||||
MarketPriceSnapshot
|
||||
```
|
||||
|
||||
Параллельно существовал:
|
||||
|
||||
```text
|
||||
Canonical Quote
|
||||
│
|
||||
▼
|
||||
Quote Store
|
||||
```
|
||||
|
||||
После Build 033:
|
||||
|
||||
```text
|
||||
Legacy consumers
|
||||
│
|
||||
▼
|
||||
MarketPriceCache
|
||||
compatibility facade
|
||||
│
|
||||
▼
|
||||
Canonical Quote
|
||||
│
|
||||
▼
|
||||
Quote Store
|
||||
```
|
||||
|
||||
При чтении:
|
||||
|
||||
```text
|
||||
Quote Store
|
||||
│
|
||||
▼
|
||||
Canonical Quote
|
||||
│
|
||||
▼
|
||||
MarketPriceSnapshot
|
||||
compatibility model
|
||||
│
|
||||
▼
|
||||
Legacy consumer
|
||||
```
|
||||
|
||||
Таким образом, независимое legacy-хранилище котировок устранено.
|
||||
|
||||
---
|
||||
|
||||
## 23. Что намеренно не входит в Build 033
|
||||
|
||||
Build 033 не выполняет:
|
||||
|
||||
- удаление `MarketPriceCache`;
|
||||
- удаление `MarketPriceSnapshot`;
|
||||
- перевод WebSocket parsing на новый Dzengi quote adapter;
|
||||
- перевод `MarketDataRunner` на `Quotes Feed`;
|
||||
- перевод UI-потребителей на канонический `Quote`;
|
||||
- перевод execution-потребителей на канонический `Quote`;
|
||||
- удаление `TickerPrice`;
|
||||
- удаление legacy market snapshot dict layer;
|
||||
- удаление legacy quote parsing.
|
||||
|
||||
Эти изменения выполняются последующими этапами утверждённого плана.
|
||||
|
||||
---
|
||||
|
||||
## 24. Условия завершения
|
||||
|
||||
Build 033 считается завершённым, поскольку выполнены все обязательные условия:
|
||||
|
||||
- [x] `MarketPriceCache` больше не владеет собственным `_prices` dict.
|
||||
- [x] Канонический `Quote Store` используется для хранения котировок facade.
|
||||
- [x] `set_price()` преобразует legacy-вход в канонический `Quote`.
|
||||
- [x] `get_price()` возвращает совместимый `MarketPriceSnapshot`.
|
||||
- [x] Сохранён контракт `age_seconds()`.
|
||||
- [x] Сохранён контракт `has_bid_ask()`.
|
||||
- [x] Сохранена изоляция по `runtime_key`.
|
||||
- [x] Сохранена нормализация символа.
|
||||
- [x] Сохранена семантика `clear()`.
|
||||
- [x] Очистка facade не затрагивает другие storage namespace.
|
||||
- [x] Production-потребители не потребовали изменений.
|
||||
- [x] Специализированные тесты успешно пройдены.
|
||||
- [x] Регрессионные тесты потребителей успешно пройдены.
|
||||
- [x] Полный набор тестов проекта успешно пройден.
|
||||
- [x] Обратная совместимость работающего бота сохранена.
|
||||
|
||||
---
|
||||
|
||||
## 25. Статус Build
|
||||
|
||||
**Build 033 — Completed.**
|
||||
|
||||
Канонический `Quote Store` теперь является владельцем состояния котировок, доступных через legacy `MarketPriceCache`.
|
||||
|
||||
`MarketPriceCache` сохранён только как временный compatibility facade для существующих потребителей.
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 034 — Dzengi WebSocket quote parsing и адаптер
|
||||
```
|
||||
594
docs/migrations/build_034.md
Normal file
594
docs/migrations/build_034.md
Normal file
@@ -0,0 +1,594 @@
|
||||
# Build 034 — Dzengi WebSocket quote parsing и адаптер
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data
|
||||
**Контур:** Market Data Acquisition / Quotes Feed
|
||||
**Проект:** Dzentra
|
||||
**Язык документации:** Русский
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Цель Build 034 — создать специализированный контур обработки WebSocket-сообщений котировок Dzengi и преобразования их в каноническую модель `Quote`.
|
||||
|
||||
Build должен был изолировать знание транспортных форматов Dzengi WebSocket от канонического слоя Market Data и подготовить архитектурную основу для последующего перевода market runtime на новый Quotes Feed.
|
||||
|
||||
В рамках Build реализована цепочка:
|
||||
|
||||
```text
|
||||
Dzengi WebSocket message
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
DzengiWebSocketQuoteResponse
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектурный результат
|
||||
|
||||
После Build 034 обработка WebSocket-котировок Dzengi получила специализированный адаптерный контур внутри:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/
|
||||
```
|
||||
|
||||
Транспортные особенности Dzengi WebSocket больше не должны распространяться на каноническую модель `Quote` и будущих потребителей Quotes Feed.
|
||||
|
||||
Архитектурная граница имеет следующий вид:
|
||||
|
||||
```text
|
||||
Dzengi-specific transport formats
|
||||
↓
|
||||
adapters/dzengi
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
Quotes Feed / Quote Store / consumers
|
||||
```
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/quote.py
|
||||
```
|
||||
|
||||
остаётся независимой от:
|
||||
|
||||
```text
|
||||
payload
|
||||
Payload
|
||||
symbolName
|
||||
bid
|
||||
ask
|
||||
ofr
|
||||
bids
|
||||
asks
|
||||
price
|
||||
p
|
||||
bidPrice
|
||||
askPrice
|
||||
```
|
||||
|
||||
Эти имена являются особенностями внешнего транспорта Dzengi и обрабатываются внутри адаптерного слоя.
|
||||
|
||||
---
|
||||
|
||||
## 3. Изменённые файлы
|
||||
|
||||
В рамках Build 034 изменены следующие файлы:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/models.py
|
||||
src/market_data/acquisition/adapters/dzengi/parser.py
|
||||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||||
src/market_data/acquisition/validation/schema.py
|
||||
src/market_data/acquisition/validation/values.py
|
||||
```
|
||||
|
||||
Добавлен специализированный WebSocket-адаптер:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/websocket.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Добавленные тесты
|
||||
|
||||
Добавлены следующие специализированные тестовые файлы:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py
|
||||
tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Поддерживаемые WebSocket-форматы
|
||||
|
||||
Новый адаптерный контур поддерживает транспортные варианты, существовавшие в legacy-реализации Dzengi WebSocket.
|
||||
|
||||
### 5.1. Оболочки сообщения
|
||||
|
||||
Поддерживаются:
|
||||
|
||||
```text
|
||||
payload
|
||||
Payload
|
||||
```
|
||||
|
||||
а также вложенная двойная оболочка.
|
||||
|
||||
Примеры допустимой структуры:
|
||||
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"symbolName": "BTC/USD_LEVERAGE",
|
||||
"bid": "64159.45",
|
||||
"ask": "64159.55"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```json
|
||||
{
|
||||
"Payload": {
|
||||
"Payload": {
|
||||
"symbolName": "BTC/USD_LEVERAGE",
|
||||
"bids": [
|
||||
["64159.45", "1.0"]
|
||||
],
|
||||
"asks": [
|
||||
["64159.55", "1.0"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Поддерживаемые поля символа
|
||||
|
||||
Адаптер поддерживает следующие транспортные имена символа:
|
||||
|
||||
```text
|
||||
symbolName
|
||||
symbol
|
||||
```
|
||||
|
||||
После обработки внешнее представление преобразуется в каноническое поле:
|
||||
|
||||
```text
|
||||
Quote.symbol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Поддерживаемые представления bid и ask
|
||||
|
||||
Поддерживаются прямые поля:
|
||||
|
||||
```text
|
||||
bid
|
||||
ask
|
||||
```
|
||||
|
||||
вариант Dzengi:
|
||||
|
||||
```text
|
||||
bid
|
||||
ofr
|
||||
```
|
||||
|
||||
а также depth-представление:
|
||||
|
||||
```text
|
||||
bids
|
||||
asks
|
||||
```
|
||||
|
||||
Для элементов depth поддерживаются представления в виде:
|
||||
|
||||
```text
|
||||
list
|
||||
dict
|
||||
```
|
||||
|
||||
Поддерживаемые имена поля цены внутри depth-элементов:
|
||||
|
||||
```text
|
||||
price
|
||||
p
|
||||
bidPrice
|
||||
askPrice
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Семантика last_price для depth-сообщений
|
||||
|
||||
Для WebSocket depth-сообщений, содержащих лучшие цены bid и ask, сохранена legacy-семантика:
|
||||
|
||||
```text
|
||||
last_price = midpoint(best_bid, best_ask)
|
||||
```
|
||||
|
||||
То есть каноническое значение `Quote.last_price` определяется как середина между лучшей ценой покупки и лучшей ценой продажи.
|
||||
|
||||
Это решение сохраняет обратную совместимость с существующим поведением market runtime до его последующего архитектурного перевода.
|
||||
|
||||
---
|
||||
|
||||
## 9. Обработка timestamp
|
||||
|
||||
WebSocket timestamp является необязательным.
|
||||
|
||||
Если транспортное сообщение содержит допустимый timestamp биржи, он преобразуется в:
|
||||
|
||||
```text
|
||||
Quote.exchange_timestamp
|
||||
```
|
||||
|
||||
Если timestamp отсутствует, каноническая модель допускает:
|
||||
|
||||
```text
|
||||
exchange_timestamp = None
|
||||
```
|
||||
|
||||
Время фактического получения и обработки котировки фиксируется отдельно:
|
||||
|
||||
```text
|
||||
Quote.received_at
|
||||
```
|
||||
|
||||
Таким образом, сохраняется разделение двух временных характеристик:
|
||||
|
||||
```text
|
||||
exchange_timestamp
|
||||
время события по данным биржи
|
||||
|
||||
received_at
|
||||
время получения котировки системой Dzentra
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Schema validation
|
||||
|
||||
Schema validation отвечает исключительно за структурную корректность WebSocket-документа.
|
||||
|
||||
На этом этапе проверяется возможность извлечения необходимых частей сообщения без переноса бизнес-логики в транспортный слой.
|
||||
|
||||
Schema validation не должна:
|
||||
|
||||
```text
|
||||
создавать canonical Quote
|
||||
выполнять mapping
|
||||
управлять runtime
|
||||
записывать данные в Quote Store
|
||||
обращаться к MarketPriceCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Parser
|
||||
|
||||
Parser преобразует структурно проверенный WebSocket-документ в специализированную raw-модель Dzengi:
|
||||
|
||||
```text
|
||||
DzengiWebSocketQuoteResponse
|
||||
```
|
||||
|
||||
Parser сохраняет границу между:
|
||||
|
||||
```text
|
||||
сырой внешний документ
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
типизированное транспортное представление Dzengi
|
||||
```
|
||||
|
||||
Parser не создаёт канонический `Quote`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Value validation
|
||||
|
||||
Value validation проверяет семантическую допустимость извлечённых значений.
|
||||
|
||||
В частности, контур должен обеспечивать корректность значений, необходимых для построения канонической котировки:
|
||||
|
||||
```text
|
||||
symbol
|
||||
bid price
|
||||
ask price
|
||||
timestamp, если присутствует
|
||||
```
|
||||
|
||||
Проверка значений выполняется до mapping в каноническую модель.
|
||||
|
||||
---
|
||||
|
||||
## 13. Mapper
|
||||
|
||||
Mapper преобразует проверенную raw-модель Dzengi WebSocket в:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
На этой границе происходит переход:
|
||||
|
||||
```text
|
||||
Dzengi-specific representation
|
||||
↓
|
||||
canonical Dzentra representation
|
||||
```
|
||||
|
||||
После mapping потребитель не должен зависеть от исходного формата WebSocket-сообщения.
|
||||
|
||||
---
|
||||
|
||||
## 14. DzengiWebSocketQuoteAdapter
|
||||
|
||||
Специализированный адаптер инкапсулирует полный конвейер обработки одного WebSocket-сообщения:
|
||||
|
||||
```text
|
||||
raw document
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parsing
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapping
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Результатом успешной обработки является канонический объект:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
Адаптер не отвечает за:
|
||||
|
||||
```text
|
||||
поддержание WebSocket-соединения
|
||||
reconnect
|
||||
runtime lifecycle
|
||||
регистрацию market runtime
|
||||
запись в Quote Store
|
||||
legacy MarketPriceCache facade
|
||||
```
|
||||
|
||||
Эти обязанности принадлежат другим архитектурным слоям.
|
||||
|
||||
---
|
||||
|
||||
## 15. Что намеренно не изменялось
|
||||
|
||||
В Build 034 не изменялись runtime-файлы:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/ws_client.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Также Build 034 не выполнял переключение:
|
||||
|
||||
```text
|
||||
market runtime → Quotes Feed
|
||||
```
|
||||
|
||||
и не удалял legacy-механизмы.
|
||||
|
||||
Это принципиальная граница Build.
|
||||
|
||||
Build 034 создаёт новый специализированный адаптерный контур, но не переключает на него существующий runtime.
|
||||
|
||||
Перевод runtime предусмотрен следующим этапом:
|
||||
|
||||
```text
|
||||
Build 035 — Перевод market runtime на Quotes Feed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Обратная совместимость
|
||||
|
||||
В Build 034 сохранены существующие транспортные варианты legacy WebSocket-контура:
|
||||
|
||||
```text
|
||||
payload / Payload
|
||||
двойная оболочка
|
||||
symbolName / symbol
|
||||
bid + ask
|
||||
bid + ofr
|
||||
bids + asks
|
||||
depth item list
|
||||
depth item dict
|
||||
price / p / bidPrice / askPrice
|
||||
необязательный timestamp
|
||||
```
|
||||
|
||||
Для depth-сообщений сохранено существующее правило:
|
||||
|
||||
```text
|
||||
last_price = midpoint(best_bid, best_ask)
|
||||
```
|
||||
|
||||
Таким образом, Build не требует одномоментного удаления legacy runtime и подготавливает безопасный переход к новой архитектуре.
|
||||
|
||||
---
|
||||
|
||||
## 17. Проверка компиляции
|
||||
|
||||
Выполнена проверка:
|
||||
|
||||
```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/adapters/dzengi/mapper.py \
|
||||
src/market_data/acquisition/adapters/dzengi/websocket.py \
|
||||
src/market_data/acquisition/validation/schema.py \
|
||||
src/market_data/acquisition/validation/values.py \
|
||||
tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py \
|
||||
tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py \
|
||||
tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
24 passed in 0.04s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
602 passed in 0.30s
|
||||
```
|
||||
|
||||
Количество тестов до Build 034:
|
||||
|
||||
```text
|
||||
578 passed
|
||||
```
|
||||
|
||||
Количество тестов после Build 034:
|
||||
|
||||
```text
|
||||
602 passed
|
||||
```
|
||||
|
||||
Добавлено:
|
||||
|
||||
```text
|
||||
24 теста
|
||||
```
|
||||
|
||||
Полная регрессия подтверждает отсутствие обнаруженных регрессий в существующем коде проекта.
|
||||
|
||||
---
|
||||
|
||||
## 20. Итог Build
|
||||
|
||||
Build 034 завершён полностью.
|
||||
|
||||
Реализованы:
|
||||
|
||||
```text
|
||||
специализированная WebSocket raw-модель Dzengi
|
||||
WebSocket schema validation
|
||||
WebSocket parser
|
||||
WebSocket value validation
|
||||
WebSocket mapper
|
||||
DzengiWebSocketQuoteAdapter
|
||||
преобразование WebSocket-сообщения в canonical Quote
|
||||
поддержка legacy-вариантов формата Dzengi
|
||||
24 специализированных теста
|
||||
```
|
||||
|
||||
Не выполнялись:
|
||||
|
||||
```text
|
||||
переключение market runtime
|
||||
изменение ws_client.py
|
||||
изменение market_stream.py
|
||||
изменение market_data_runner.py
|
||||
удаление legacy WebSocket parsing
|
||||
удаление MarketPriceCache
|
||||
```
|
||||
|
||||
Архитектурный результат:
|
||||
|
||||
```text
|
||||
Dzengi WebSocket transport
|
||||
↓
|
||||
Dzengi-specific validation / parsing / mapping
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. Следующий Build
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 035 — Перевод market runtime на Quotes Feed
|
||||
```
|
||||
|
||||
Его задача — подключить существующий market runtime к новому каноническому контуру котировок, используя созданные ранее:
|
||||
|
||||
```text
|
||||
Quote
|
||||
Quote Store
|
||||
Quotes Feed
|
||||
Dzengi REST Quotes Feed
|
||||
Dzengi WebSocket quote adapter
|
||||
```
|
||||
|
||||
При этом переход должен выполняться без преждевременного удаления legacy-механизмов и с сохранением работоспособности существующего бота до завершения последующих этапов миграции.
|
||||
898
docs/migrations/build_035.md
Normal file
898
docs/migrations/build_035.md
Normal file
@@ -0,0 +1,898 @@
|
||||
# Build 035 — Перевод market runtime на Quotes Feed
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data
|
||||
**Контур:** Market Data Acquisition / Quotes Feed / Market Runtime
|
||||
**Проект:** Dzentra
|
||||
**Язык документации:** Русский
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Цель Build 035 — перевести существующий WebSocket market runtime с самостоятельного legacy parsing рыночных сообщений на канонический контур обработки котировок, созданный в предыдущих Build.
|
||||
|
||||
До Build 035 runtime самостоятельно извлекал цены из WebSocket-сообщений Dzengi и передавал примитивные значения в legacy facade:
|
||||
|
||||
```text
|
||||
Dzengi WebSocket message
|
||||
↓
|
||||
legacy runtime parsing
|
||||
↓
|
||||
float price / bid / ask
|
||||
↓
|
||||
MarketPriceCache.set_price()
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
После Build 035 рабочий runtime-путь использует специализированный WebSocket-адаптер и каноническую модель `Quote`:
|
||||
|
||||
```text
|
||||
Dzengi WebSocket message
|
||||
↓
|
||||
ExchangeWebSocketClient
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
Таким образом, WebSocket runtime больше не выполняет собственное преобразование транспортного формата Dzengi в набор примитивных ценовых значений.
|
||||
|
||||
---
|
||||
|
||||
## 2. Предпосылки
|
||||
|
||||
Build 035 опирается на результаты предыдущих этапов миграции.
|
||||
|
||||
### Build 027
|
||||
|
||||
Создана каноническая модель:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
### Build 028
|
||||
|
||||
Созданы:
|
||||
|
||||
```text
|
||||
REST quote schema validation
|
||||
REST quote parser
|
||||
REST quote value validation
|
||||
```
|
||||
|
||||
### Build 029
|
||||
|
||||
Созданы:
|
||||
|
||||
```text
|
||||
REST quote mapper
|
||||
DzengiQuoteDocumentHandler
|
||||
```
|
||||
|
||||
### Build 030
|
||||
|
||||
Создан полный REST Quotes Feed:
|
||||
|
||||
```text
|
||||
Dzengi REST ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
### Build 031
|
||||
|
||||
Новый REST Quotes Feed подключён под legacy `ExchangeService` facade.
|
||||
|
||||
### Build 032
|
||||
|
||||
Создан канонический:
|
||||
|
||||
```text
|
||||
Quote Store
|
||||
```
|
||||
|
||||
### Build 033
|
||||
|
||||
`MarketPriceCache` переведён на использование `Quote Store` как внутреннего хранилища.
|
||||
|
||||
### Build 034
|
||||
|
||||
Создан специализированный контур обработки WebSocket-котировок:
|
||||
|
||||
```text
|
||||
Dzengi WebSocket message
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
canonical Quote
|
||||
```
|
||||
|
||||
Build 035 подключает этот контур к существующему market runtime.
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектурная проблема до Build 035
|
||||
|
||||
До Build 035 существовало несколько независимых путей обработки котировок.
|
||||
|
||||
REST-контур уже использовал каноническую модель:
|
||||
|
||||
```text
|
||||
REST ticker/24hr
|
||||
↓
|
||||
Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
Но WebSocket runtime продолжал самостоятельно разбирать транспортные сообщения.
|
||||
|
||||
### `market_stream.py`
|
||||
|
||||
Рабочий путь имел вид:
|
||||
|
||||
```text
|
||||
WebSocket message
|
||||
↓
|
||||
_payload_from_message()
|
||||
↓
|
||||
_extract_market_event()
|
||||
↓
|
||||
float price / bid / ask
|
||||
↓
|
||||
MarketPriceCache.set_price()
|
||||
```
|
||||
|
||||
### `market_data_runner.py`
|
||||
|
||||
Рабочий путь имел вид:
|
||||
|
||||
```text
|
||||
WebSocket message
|
||||
↓
|
||||
_extract_depth_payload()
|
||||
↓
|
||||
_extract_best_price()
|
||||
↓
|
||||
float midpoint
|
||||
↓
|
||||
MarketPriceCache.set_price()
|
||||
```
|
||||
|
||||
Таким образом, логика понимания формата Dzengi WebSocket существовала одновременно:
|
||||
|
||||
```text
|
||||
в новом DzengiWebSocketQuoteAdapter
|
||||
в market_stream.py
|
||||
в market_data_runner.py
|
||||
```
|
||||
|
||||
Это нарушало архитектурную границу:
|
||||
|
||||
```text
|
||||
transport-specific parsing
|
||||
↓
|
||||
только adapter layer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектурный результат
|
||||
|
||||
После Build 035 оба WebSocket runtime-пути используют единый канонический адаптер:
|
||||
|
||||
```text
|
||||
ExchangeWebSocketClient
|
||||
↓
|
||||
decoded WebSocket message
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache compatibility facade
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
Runtime больше не должен самостоятельно знать:
|
||||
|
||||
```text
|
||||
как устроены payload / Payload
|
||||
как извлекается symbolName
|
||||
как извлекаются bids / asks
|
||||
какие варианты depth item поддерживает Dzengi
|
||||
как вычисляется canonical last_price
|
||||
как преобразуется timestamp
|
||||
```
|
||||
|
||||
Эта ответственность принадлежит:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/websocket.py
|
||||
```
|
||||
|
||||
и связанному с ним контуру:
|
||||
|
||||
```text
|
||||
schema validation
|
||||
parser
|
||||
value validation
|
||||
mapper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменённые production-файлы
|
||||
|
||||
В рамках Build 035 изменены:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Изменённые тестовые файлы
|
||||
|
||||
Расширены существующие тесты:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_market_cache.py
|
||||
tests/unit/integrations/exchange/test_market_stream.py
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Новые тестовые файлы не создавались.
|
||||
|
||||
---
|
||||
|
||||
## 7. Расширение MarketPriceCache
|
||||
|
||||
В `MarketPriceCache` добавлен новый метод:
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def set_quote(
|
||||
cls,
|
||||
quote: Quote,
|
||||
*,
|
||||
runtime_key: str = "default",
|
||||
) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
Его задача — принять уже готовый канонический объект:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
и записать его в существующее внутреннее хранилище через compatibility facade.
|
||||
|
||||
Цепочка:
|
||||
|
||||
```text
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Сохранение канонического Quote без повторного mapping
|
||||
|
||||
До Build 035 при наличии уже готового `Quote` потенциально мог возникнуть лишний цикл:
|
||||
|
||||
```text
|
||||
Quote
|
||||
↓
|
||||
float values
|
||||
↓
|
||||
MarketPriceCache.set_price()
|
||||
↓
|
||||
создание нового Quote
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
После Build 035 используется прямой путь:
|
||||
|
||||
```text
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
При этом не требуется:
|
||||
|
||||
```text
|
||||
преобразовывать Decimal в float
|
||||
повторно создавать Quote
|
||||
повторно вычислять received_at
|
||||
повторно преобразовывать exchange_timestamp
|
||||
изменять source
|
||||
```
|
||||
|
||||
Таким образом, сохраняется исходный канонический объект.
|
||||
|
||||
---
|
||||
|
||||
## 9. Сохранение legacy set_price()
|
||||
|
||||
Существующий публичный метод:
|
||||
|
||||
```text
|
||||
MarketPriceCache.set_price()
|
||||
```
|
||||
|
||||
не удалён.
|
||||
|
||||
Это необходимо для сохранения обратной совместимости существующего бота и legacy-потребителей.
|
||||
|
||||
После Build 035 его архитектурная роль:
|
||||
|
||||
```text
|
||||
legacy primitive values
|
||||
↓
|
||||
MarketPriceCache.set_price()
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
Таким образом, `set_price()` остаётся compatibility entry point, а непосредственная запись готового канонического объекта выполняется через:
|
||||
|
||||
```text
|
||||
set_quote()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Перевод market_stream.py
|
||||
|
||||
Рабочий WebSocket-путь `market_stream.py` переведён на:
|
||||
|
||||
```text
|
||||
ExchangeWebSocketClient.stream_depth()
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter.map_message()
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
```
|
||||
|
||||
Новый runtime-путь больше не использует legacy-функцию:
|
||||
|
||||
```text
|
||||
_extract_market_event()
|
||||
```
|
||||
|
||||
для обработки рабочих WebSocket-сообщений.
|
||||
|
||||
---
|
||||
|
||||
## 11. Проверка символа в market_stream.py
|
||||
|
||||
После получения канонического `Quote` выполняется проверка соответствия символа ожидаемому инструменту.
|
||||
|
||||
Концептуально:
|
||||
|
||||
```text
|
||||
requested symbol
|
||||
↕
|
||||
Quote.symbol
|
||||
```
|
||||
|
||||
Если сообщение относится к другому инструменту, оно не должно записываться в runtime namespace.
|
||||
|
||||
Это предотвращает сохранение чужой котировки в контексте текущего WebSocket-потока.
|
||||
|
||||
---
|
||||
|
||||
## 12. Обработка невалидных сообщений в market_stream.py
|
||||
|
||||
Ошибка обработки отдельного WebSocket-сообщения не должна немедленно завершать весь market stream.
|
||||
|
||||
Ошибки канонического Acquisition-контура отдельного сообщения обрабатываются внутри цикла:
|
||||
|
||||
```text
|
||||
invalid WebSocket message
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
MarketDataAcquisitionError
|
||||
↓
|
||||
сообщение пропускается
|
||||
↓
|
||||
stream продолжает работу
|
||||
```
|
||||
|
||||
При этом сетевые, transport и connection errors не маскируются этим механизмом и продолжают обрабатываться существующим reconnect-контуром.
|
||||
|
||||
---
|
||||
|
||||
## 13. Перевод MarketDataRunner
|
||||
|
||||
Основной WebSocket runtime в:
|
||||
|
||||
```text
|
||||
MarketDataRunner._run_websocket()
|
||||
```
|
||||
|
||||
переведён с самостоятельного извлечения:
|
||||
|
||||
```text
|
||||
best_bid
|
||||
best_ask
|
||||
```
|
||||
|
||||
на канонический путь:
|
||||
|
||||
```text
|
||||
raw WebSocket payload
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter.map_message()
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
После успешного mapping готовый объект записывается:
|
||||
|
||||
```text
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote(
|
||||
runtime_key=context.runtime_key
|
||||
)
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Runtime isolation
|
||||
|
||||
Сохраняется существующая изоляция runtime-контекстов через:
|
||||
|
||||
```text
|
||||
runtime_key
|
||||
```
|
||||
|
||||
Примеры существующих runtime:
|
||||
|
||||
```text
|
||||
auto
|
||||
debug_auto
|
||||
default
|
||||
```
|
||||
|
||||
При записи через:
|
||||
|
||||
```text
|
||||
MarketPriceCache.set_quote()
|
||||
```
|
||||
|
||||
используется соответствующий:
|
||||
|
||||
```text
|
||||
context.runtime_key
|
||||
```
|
||||
|
||||
Таким образом, котировки разных runtime не смешиваются.
|
||||
|
||||
---
|
||||
|
||||
## 15. Bid и ask после перехода на Quote
|
||||
|
||||
До Build 035 `MarketDataRunner` самостоятельно извлекал:
|
||||
|
||||
```text
|
||||
best_bid
|
||||
best_ask
|
||||
```
|
||||
|
||||
из сырого WebSocket payload.
|
||||
|
||||
После Build 035 эти значения берутся из уже проверенного канонического объекта:
|
||||
|
||||
```text
|
||||
Quote.bid_price
|
||||
Quote.ask_price
|
||||
```
|
||||
|
||||
Для legacy logging boundary при необходимости допускается преобразование:
|
||||
|
||||
```text
|
||||
Decimal → float
|
||||
```
|
||||
|
||||
Но внутренний канонический объект остаётся основанным на:
|
||||
|
||||
```text
|
||||
Decimal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Last price для depth-сообщений
|
||||
|
||||
Согласно контракту Build 034 для WebSocket depth-сообщений:
|
||||
|
||||
```text
|
||||
last_price = midpoint(best_bid, best_ask)
|
||||
```
|
||||
|
||||
После Build 035 runtime больше не должен самостоятельно повторять этот расчёт.
|
||||
|
||||
Он получает готовое значение:
|
||||
|
||||
```text
|
||||
Quote.last_price
|
||||
```
|
||||
|
||||
из `DzengiWebSocketQuoteAdapter`.
|
||||
|
||||
Таким образом, правило определения `last_price` имеет одну каноническую реализацию.
|
||||
|
||||
---
|
||||
|
||||
## 17. Сохранение invalid payload semantics
|
||||
|
||||
До Build 035 `MarketDataRunner` поддерживал счётчик последовательных невалидных WebSocket-сообщений.
|
||||
|
||||
Существующая семантика сохранена:
|
||||
|
||||
```text
|
||||
invalid message
|
||||
↓
|
||||
invalid_payload_count += 1
|
||||
```
|
||||
|
||||
Успешный `Quote`:
|
||||
|
||||
```text
|
||||
valid Quote
|
||||
↓
|
||||
invalid_payload_count = 0
|
||||
```
|
||||
|
||||
После пяти последовательных невалидных сообщений:
|
||||
|
||||
```text
|
||||
5 consecutive invalid messages
|
||||
↓
|
||||
RuntimeError
|
||||
↓
|
||||
существующий fallback-контур
|
||||
```
|
||||
|
||||
Таким образом, Build 035 не изменяет существующую политику деградации runtime.
|
||||
|
||||
---
|
||||
|
||||
## 18. REST fallback
|
||||
|
||||
REST fallback не потребовал изменения.
|
||||
|
||||
К моменту Build 035 REST-путь уже использует новый Quotes Feed:
|
||||
|
||||
```text
|
||||
Dzengi REST ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache facade
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
После Build 035 WebSocket и REST-пути сходятся на одной канонической модели:
|
||||
|
||||
```text
|
||||
WebSocket
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
Quote Store
|
||||
↑
|
||||
Quote
|
||||
↑
|
||||
REST Quotes Feed
|
||||
↑
|
||||
REST
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Что намеренно не изменялось
|
||||
|
||||
В Build 035 не изменялись:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/ws_client.py
|
||||
src/market_data/acquisition/adapters/dzengi/websocket.py
|
||||
src/market_data/acquisition/models/quote.py
|
||||
src/storage/quote_store.py
|
||||
```
|
||||
|
||||
Причина:
|
||||
|
||||
- `ws_client.py` уже имеет достаточный transport-only контракт;
|
||||
- WebSocket-адаптер завершён в Build 034;
|
||||
- каноническая модель `Quote` не требует расширения;
|
||||
- `Quote Store` уже предоставляет необходимый контракт хранения.
|
||||
|
||||
---
|
||||
|
||||
## 20. Legacy parsing helpers
|
||||
|
||||
После перевода рабочего runtime-пути некоторые legacy helper-функции больше не являются частью основного пути обработки котировок.
|
||||
|
||||
В частности:
|
||||
|
||||
```text
|
||||
market_stream.py:
|
||||
_extract_market_event()
|
||||
|
||||
market_data_runner.py:
|
||||
_extract_best_price()
|
||||
```
|
||||
|
||||
Они не удалены в Build 035.
|
||||
|
||||
Это намеренное решение.
|
||||
|
||||
Окончательная очистка legacy parsing относится к последующему этапу:
|
||||
|
||||
```text
|
||||
Build 039 — Удаление legacy quote parsing и MarketPriceCache
|
||||
```
|
||||
|
||||
Build 035 меняет рабочий runtime-путь, но не выполняет преждевременную очистку compatibility layer.
|
||||
|
||||
---
|
||||
|
||||
## 21. Обратная совместимость
|
||||
|
||||
Build 035 сохраняет работоспособность существующего бота.
|
||||
|
||||
Не удалены:
|
||||
|
||||
```text
|
||||
MarketPriceCache
|
||||
MarketPriceCache.set_price()
|
||||
legacy read APIs
|
||||
runtime_key isolation
|
||||
REST fallback
|
||||
существующие runtime lifecycle contracts
|
||||
```
|
||||
|
||||
Это соответствует принятому принципу миграции Dzentra:
|
||||
|
||||
```text
|
||||
новый контур создаётся
|
||||
↓
|
||||
существующие потребители постепенно переключаются
|
||||
↓
|
||||
legacy удаляется только после завершения миграции
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22. Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/market_cache.py \
|
||||
src/integrations/exchange/market_stream.py \
|
||||
src/integrations/exchange/market_data_runner.py \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 23. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
33 passed in 0.13s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 24. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
608 passed in 0.29s
|
||||
```
|
||||
|
||||
Количество тестов до Build 035:
|
||||
|
||||
```text
|
||||
602 passed
|
||||
```
|
||||
|
||||
Количество тестов после Build 035:
|
||||
|
||||
```text
|
||||
608 passed
|
||||
```
|
||||
|
||||
Добавлено:
|
||||
|
||||
```text
|
||||
6 тестов
|
||||
```
|
||||
|
||||
Полная регрессия подтверждает отсутствие обнаруженных регрессий в существующем коде проекта.
|
||||
|
||||
---
|
||||
|
||||
## 25. Итог Build
|
||||
|
||||
Build 035 завершён полностью.
|
||||
|
||||
Реализованы:
|
||||
|
||||
```text
|
||||
подключение DzengiWebSocketQuoteAdapter к market_stream
|
||||
подключение DzengiWebSocketQuoteAdapter к MarketDataRunner
|
||||
передача canonical Quote непосредственно в compatibility facade
|
||||
добавление MarketPriceCache.set_quote()
|
||||
сохранение legacy MarketPriceCache.set_price()
|
||||
сохранение runtime_key isolation
|
||||
сохранение invalid payload semantics
|
||||
сохранение REST fallback
|
||||
сохранение обратной совместимости
|
||||
```
|
||||
|
||||
Подтверждён новый рабочий WebSocket-путь:
|
||||
|
||||
```text
|
||||
ExchangeWebSocketClient
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 26. Архитектурное состояние после Build 035
|
||||
|
||||
После завершения Build 035 Dzentra имеет два канонических пути получения текущей котировки.
|
||||
|
||||
### WebSocket
|
||||
|
||||
```text
|
||||
Dzengi WebSocket
|
||||
↓
|
||||
ExchangeWebSocketClient
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache compatibility facade
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
### REST
|
||||
|
||||
```text
|
||||
Dzengi REST ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache compatibility facade
|
||||
↓
|
||||
Quote Store
|
||||
```
|
||||
|
||||
Таким образом, оба transport-пути приводят данные к единой канонической модели:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
до передачи их потребителям.
|
||||
|
||||
---
|
||||
|
||||
## 27. Следующий Build
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 036 — Перевод read-only и UI-потребителей
|
||||
```
|
||||
|
||||
Его задача — определить и перевести read-only и UI-потребителей текущей рыночной котировки с legacy-представлений на канонический `Quote` и новый контур хранения там, где это архитектурно обосновано.
|
||||
|
||||
Build 036 должен выполняться без изменения execution semantics и без преждевременного удаления legacy compatibility layer.
|
||||
|
||||
Execution-потребители остаются отдельным последующим этапом миграции.
|
||||
739
docs/migrations/build_036.md
Normal file
739
docs/migrations/build_036.md
Normal file
@@ -0,0 +1,739 @@
|
||||
# Build 036 — Перевод read-only и UI-потребителей
|
||||
|
||||
**Статус:** Завершён
|
||||
**Результат:** Успешно
|
||||
**Полная регрессия:** `614 passed`
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение Build
|
||||
|
||||
Цель Build 036 — перевести read-only и UI-потребителей рыночной котировки с legacy-представлений:
|
||||
|
||||
- `TickerPrice`;
|
||||
- `dict[str, object]` из `get_market_snapshot()`;
|
||||
|
||||
на каноническую внутреннюю модель:
|
||||
|
||||
`Quote`
|
||||
|
||||
Build продолжает миграцию подсистемы рыночных данных на целевую архитектуру:
|
||||
|
||||
Market Data
|
||||
↓
|
||||
Market Intelligence
|
||||
↓
|
||||
Decision
|
||||
↓
|
||||
Execution
|
||||
↓
|
||||
Exchange
|
||||
|
||||
В рамках Build 036 изменяется исключительно read-only контур.
|
||||
|
||||
Execution pricing, торговые стратегии, signal runtime и execution quality не переводятся и не изменяются.
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходное состояние
|
||||
|
||||
До Build 036 read-only и UI-потребители получали текущую рыночную котировку через несколько legacy-интерфейсов.
|
||||
|
||||
Основные варианты:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService.get_price()
|
||||
↓
|
||||
TickerPrice
|
||||
|
||||
или:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService.get_market_snapshot()
|
||||
↓
|
||||
dict[str, object]
|
||||
|
||||
При этом после предыдущих Build каноническая модель `Quote` уже существовала и использовалась внутри новой инфраструктуры:
|
||||
|
||||
REST Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
|
||||
WebSocket quote adapter
|
||||
↓
|
||||
Quote
|
||||
|
||||
MarketPriceCache
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
Quote
|
||||
|
||||
Таким образом, read-only потребители продолжали зависеть от compatibility-представлений, несмотря на наличие канонической модели котировки.
|
||||
|
||||
---
|
||||
|
||||
## 3. Целевое состояние
|
||||
|
||||
После Build 036 read-only и UI-потребители получают канонический объект:
|
||||
|
||||
`Quote`
|
||||
|
||||
через публичный facade:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Целевая цепочка чтения:
|
||||
|
||||
read-only / UI consumer
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
При отсутствии свежей котировки используется REST fallback:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектурный принцип
|
||||
|
||||
Read-only и UI-потребители не обращаются напрямую к:
|
||||
|
||||
`Quote Store`
|
||||
|
||||
Доступ выполняется через:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Это позволяет сохранить единый facade, отвечающий за:
|
||||
|
||||
- выбор default symbol;
|
||||
- нормализацию и валидацию символа;
|
||||
- нормализацию `runtime_key`;
|
||||
- поддержку mock mode;
|
||||
- чтение канонической котировки из cache/store;
|
||||
- проверку свежести;
|
||||
- REST fallback;
|
||||
- преобразование внутренних ошибок в `ExchangeError`.
|
||||
|
||||
Целевая граница:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
MarketPriceCache
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
UI не должен самостоятельно знать:
|
||||
|
||||
- структуру ключей Quote Store;
|
||||
- `source_name`;
|
||||
- правила `runtime_key`;
|
||||
- freshness policy;
|
||||
- правила REST fallback;
|
||||
- внутреннюю обработку ошибок Acquisition Layer.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменённые production-файлы
|
||||
|
||||
В рамках Build 036 изменены:
|
||||
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/service.py
|
||||
|
||||
src/telegram/ui/currency_ui.py
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
|
||||
src/trading/diagnostics/snapshot.py
|
||||
|
||||
---
|
||||
|
||||
## 6. Изменённые и добавленные тесты
|
||||
|
||||
Изменены:
|
||||
|
||||
tests/unit/integrations/exchange/test_market_cache.py
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
|
||||
Добавлен:
|
||||
|
||||
tests/unit/integrations/exchange/test_service_quote.py
|
||||
|
||||
После Build 036 общее количество тестов увеличилось:
|
||||
|
||||
после Build 035: 608 passed
|
||||
после Build 036: 614 passed
|
||||
|
||||
Добавлено:
|
||||
|
||||
6 тестов
|
||||
|
||||
---
|
||||
|
||||
## 7. Изменения в MarketPriceCache
|
||||
|
||||
Файл:
|
||||
|
||||
`src/integrations/exchange/market_cache.py`
|
||||
|
||||
Добавлен канонический read API:
|
||||
|
||||
`MarketPriceCache.get_quote()`
|
||||
|
||||
Его назначение — вернуть непосредственно канонический объект `Quote`, сохранённый в Quote Store.
|
||||
|
||||
Цепочка:
|
||||
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
QuoteStore.get()
|
||||
↓
|
||||
Quote | None
|
||||
|
||||
Метод:
|
||||
|
||||
- нормализует `symbol`;
|
||||
- нормализует `runtime_key`;
|
||||
- читает котировку из собственного namespace Quote Store;
|
||||
- возвращает канонический `Quote`;
|
||||
- не создаёт `MarketPriceSnapshot`;
|
||||
- не выполняет преобразование `Decimal` в `float`;
|
||||
- сохраняет канонический объект котировки.
|
||||
|
||||
Legacy API:
|
||||
|
||||
`MarketPriceCache.get_price()`
|
||||
|
||||
сохранён для compatibility-потребителей, которые ещё не переведены на `Quote`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Изменения в ExchangeService
|
||||
|
||||
Файл:
|
||||
|
||||
`src/integrations/exchange/service.py`
|
||||
|
||||
Добавлен публичный канонический read API:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Метод сохраняет обязанности facade и отвечает за:
|
||||
|
||||
1. выбор default symbol;
|
||||
2. поддержку mock mode;
|
||||
3. валидацию символа;
|
||||
4. нормализацию `runtime_key`;
|
||||
5. чтение канонической котировки из MarketPriceCache;
|
||||
6. проверку свежести;
|
||||
7. REST fallback при cache miss или stale quote;
|
||||
8. сохранение свежей котировки в Quote Store через MarketPriceCache;
|
||||
9. возврат канонического `Quote`.
|
||||
|
||||
Целевая цепочка:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
validate_symbol()
|
||||
↓
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
freshness check
|
||||
↓
|
||||
Quote
|
||||
|
||||
При отсутствии свежей котировки:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
---
|
||||
|
||||
## 9. Политика свежести
|
||||
|
||||
Для read-only и UI-потребителей сохранена существующая политика свежести рыночной котировки.
|
||||
|
||||
Возраст котировки определяется по:
|
||||
|
||||
`Quote.received_at`
|
||||
|
||||
Если сохранённая котировка достаточно свежая, возвращается существующий канонический объект.
|
||||
|
||||
Если котировка отсутствует или устарела, выполняется REST fallback через новый Quotes Feed.
|
||||
|
||||
Таким образом:
|
||||
|
||||
fresh cached Quote
|
||||
↓
|
||||
вернуть Quote без REST-запроса
|
||||
|
||||
stale cached Quote
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
сохранить новый Quote
|
||||
↓
|
||||
вернуть новый Quote
|
||||
|
||||
---
|
||||
|
||||
## 10. REST fallback
|
||||
|
||||
REST fallback использует новую каноническую цепочку Acquisition Layer:
|
||||
|
||||
Dzengi REST API
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parsing
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapping
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
Полученный объект сохраняется:
|
||||
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
Не используется лишний цикл преобразований:
|
||||
|
||||
Quote
|
||||
↓
|
||||
dict
|
||||
↓
|
||||
float
|
||||
↓
|
||||
новый Quote
|
||||
|
||||
Канонический объект остаётся `Quote` на всём новом пути.
|
||||
|
||||
---
|
||||
|
||||
## 11. Mock mode
|
||||
|
||||
`ExchangeService.get_quote()` сохраняет поддержку режима:
|
||||
|
||||
`exchange_enabled = False`
|
||||
|
||||
В этом режиме потребителю также возвращается канонический:
|
||||
|
||||
`Quote`
|
||||
|
||||
Mock-котировка содержит:
|
||||
|
||||
- `symbol`;
|
||||
- `last_price`;
|
||||
- `bid_price`;
|
||||
- `ask_price`;
|
||||
- `exchange_timestamp`;
|
||||
- `received_at`;
|
||||
- `source`.
|
||||
|
||||
Таким образом, потребители `get_quote()` не должны знать, работает приложение с реальной биржей или в mock mode.
|
||||
|
||||
---
|
||||
|
||||
## 12. Обработка ошибок
|
||||
|
||||
Новый canonical read API сохраняет существующую границу ошибок ExchangeService.
|
||||
|
||||
Ошибки Acquisition Layer:
|
||||
|
||||
- не передаются напрямую UI-потребителям;
|
||||
- логируются в контексте `ticker/24hr`;
|
||||
- преобразуются во внешний `ExchangeError`;
|
||||
- сохраняют исходную ошибку через `__cause__`.
|
||||
|
||||
Граница остаётся следующей:
|
||||
|
||||
Acquisition error
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
ExchangeError
|
||||
↓
|
||||
UI / diagnostics consumer
|
||||
|
||||
---
|
||||
|
||||
## 13. Перевод currency_ui.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/telegram/ui/currency_ui.py`
|
||||
|
||||
До Build 036 использовался legacy API:
|
||||
|
||||
`ExchangeService.get_price()`
|
||||
|
||||
Возвращаемая модель:
|
||||
|
||||
`TickerPrice`
|
||||
|
||||
Для расчёта использовалось:
|
||||
|
||||
`ticker.price`
|
||||
|
||||
После Build 036 используется:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
и каноническое поле:
|
||||
|
||||
`quote.last_price`
|
||||
|
||||
Целевая цепочка:
|
||||
|
||||
currency_ui
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
Quote.last_price
|
||||
|
||||
Локальная логика расчёта стоимости баланса, обработка `ExchangeError` и кэширование рассчитанных цен сохранены.
|
||||
|
||||
---
|
||||
|
||||
## 14. Перевод auto/ui.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/telegram/handlers/auto/ui.py`
|
||||
|
||||
До Build 036 UI получал legacy market snapshot:
|
||||
|
||||
`ExchangeService.get_market_snapshot()`
|
||||
|
||||
и работал с:
|
||||
|
||||
`dict[str, object]`
|
||||
|
||||
Основные поля:
|
||||
|
||||
- `last_price`;
|
||||
- `bid_price`;
|
||||
- `ask_price`.
|
||||
|
||||
После Build 036 UI получает:
|
||||
|
||||
`Quote`
|
||||
|
||||
через:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Используются канонические поля:
|
||||
|
||||
- `quote.last_price`;
|
||||
- `quote.bid_price`;
|
||||
- `quote.ask_price`.
|
||||
|
||||
В результате UI больше не зависит от legacy market snapshot dict для получения текущей рыночной котировки.
|
||||
|
||||
---
|
||||
|
||||
## 15. Перевод debug_auto/ui.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/telegram/handlers/debug_auto/ui.py`
|
||||
|
||||
В debug UI разделены две разные сущности:
|
||||
|
||||
1. текущая рыночная котировка;
|
||||
2. execution snapshot.
|
||||
|
||||
После Build 036 market-секция использует:
|
||||
|
||||
`Quote`
|
||||
|
||||
и читает:
|
||||
|
||||
- `last_price`;
|
||||
- `bid_price`;
|
||||
- `ask_price`;
|
||||
- `source`;
|
||||
- `received_at`;
|
||||
- `exchange_timestamp`.
|
||||
|
||||
Execution-секция продолжает использовать:
|
||||
|
||||
`ExecutionPriceSnapshot`
|
||||
|
||||
Это намеренное разделение.
|
||||
|
||||
Build 036 не изменяет execution semantics.
|
||||
|
||||
Целевая схема:
|
||||
|
||||
Debug UI
|
||||
├── Market section
|
||||
│ ↓
|
||||
│ Quote
|
||||
│
|
||||
└── Execution section
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
|
||||
---
|
||||
|
||||
## 16. Перевод trading/diagnostics/snapshot.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/trading/diagnostics/snapshot.py`
|
||||
|
||||
До Build 036 диагностика использовала:
|
||||
|
||||
`ExchangeService.get_market_snapshot()`
|
||||
|
||||
После Build 036 используется:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Для выбора диагностической цены сохраняется существующая семантика:
|
||||
|
||||
BUY
|
||||
↓
|
||||
quote.ask_price
|
||||
|
||||
SELL
|
||||
↓
|
||||
quote.bid_price
|
||||
|
||||
другое состояние
|
||||
↓
|
||||
quote.last_price
|
||||
|
||||
Build не изменяет торговые решения и используется только для read-only диагностики.
|
||||
|
||||
---
|
||||
|
||||
## 17. Сохранённые legacy API
|
||||
|
||||
Build 036 не удаляет:
|
||||
|
||||
- `ExchangeService.get_price()`;
|
||||
- `ExchangeService.get_market_snapshot()`;
|
||||
- `ExchangeService.get_execution_snapshot()`;
|
||||
- `ExchangeService.get_fresh_market_snapshot()`;
|
||||
- `MarketPriceCache.get_price()`.
|
||||
|
||||
Они сохраняются для ещё не переведённых compatibility-потребителей.
|
||||
|
||||
Удаление legacy API возможно только после полного перевода всех зависимых компонентов и отдельной проверки использования.
|
||||
|
||||
---
|
||||
|
||||
## 18. Что намеренно не изменялось
|
||||
|
||||
Build 036 не затрагивает:
|
||||
|
||||
src/trading/execution/pricing.py
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/auto/execution_quality.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
src/trading/debug/execution.py
|
||||
|
||||
Эти компоненты относятся к:
|
||||
|
||||
- execution pricing;
|
||||
- signal runtime;
|
||||
- execution quality;
|
||||
- strategy decisions;
|
||||
- debug execution semantics.
|
||||
|
||||
Их перевод должен выполняться отдельно.
|
||||
|
||||
---
|
||||
|
||||
## 19. Что не входит в Build 036
|
||||
|
||||
В рамках Build 036 не выполнялись:
|
||||
|
||||
- перевод execution pricing;
|
||||
- перевод signal runtime;
|
||||
- перевод execution quality;
|
||||
- перевод торговых стратегий;
|
||||
- удаление `TickerPrice`;
|
||||
- удаление `get_price()`;
|
||||
- удаление `get_market_snapshot()`;
|
||||
- удаление legacy market snapshot dict layer;
|
||||
- удаление `ExecutionPriceSnapshot`;
|
||||
- удаление `MarketPriceCache`;
|
||||
- удаление legacy parsing helpers.
|
||||
|
||||
---
|
||||
|
||||
## 20. Проверка синтаксиса
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/market_cache.py \
|
||||
src/integrations/exchange/service.py \
|
||||
src/telegram/ui/currency_ui.py \
|
||||
src/telegram/handlers/auto/ui.py \
|
||||
src/telegram/handlers/debug_auto/ui.py \
|
||||
src/trading/diagnostics/snapshot.py \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_service_quote.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
|
||||
Результат:
|
||||
|
||||
`Успешно`
|
||||
|
||||
Ошибок синтаксиса не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## 21. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_service_quote.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py \
|
||||
-q
|
||||
|
||||
Результат:
|
||||
|
||||
`44 passed in 0.29s`
|
||||
|
||||
---
|
||||
|
||||
## 22. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
`python -m pytest -q`
|
||||
|
||||
Результат:
|
||||
|
||||
`614 passed in 0.27s`
|
||||
|
||||
Полная тестовая регрессия проекта успешно пройдена.
|
||||
|
||||
---
|
||||
|
||||
## 23. Итоговая архитектура после Build 036
|
||||
|
||||
После завершения Build 036 read-only и UI-контур использует следующую архитектуру:
|
||||
|
||||
┌──────────────────────────────┐
|
||||
│ currency_ui │
|
||||
│ auto UI │
|
||||
│ debug UI market section │
|
||||
│ trading diagnostics │
|
||||
└──────────────┬───────────────┘
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
При cache miss или stale quote:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
---
|
||||
|
||||
## 24. Результат Build
|
||||
|
||||
Build 036 успешно завершён.
|
||||
|
||||
Достигнуты следующие результаты:
|
||||
|
||||
- добавлен канонический read API `MarketPriceCache.get_quote()`;
|
||||
- добавлен публичный facade `ExchangeService.get_quote()`;
|
||||
- сохранены symbol validation, mock mode, freshness policy и REST fallback;
|
||||
- `currency_ui.py` переведён с `TickerPrice` на `Quote`;
|
||||
- `auto/ui.py` переведён с legacy market snapshot dict на `Quote`;
|
||||
- market-секция debug UI переведена на `Quote`;
|
||||
- execution-секция debug UI оставлена на `ExecutionPriceSnapshot`;
|
||||
- trading diagnostics переведена на `Quote`;
|
||||
- execution-контур не изменён;
|
||||
- legacy API сохранены для последующей миграции;
|
||||
- специализированные тесты успешно пройдены;
|
||||
- полная регрессия успешно пройдена.
|
||||
|
||||
Итог:
|
||||
|
||||
До Build 036:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
TickerPrice / market snapshot dict
|
||||
|
||||
После Build 036:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
---
|
||||
|
||||
## 25. Следующий этап
|
||||
|
||||
Следующий этап миграции:
|
||||
|
||||
**Build 037 — Перевод execution-потребителей**
|
||||
|
||||
Его задача — отдельно проанализировать и перевести execution-sensitive потребителей канонической рыночной котировки без изменения торговой семантики и без преждевременного удаления legacy compatibility API.
|
||||
608
docs/migrations/build_037.md
Normal file
608
docs/migrations/build_037.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# Build 037 — Перевод execution-потребителей на canonical Quote
|
||||
|
||||
**Статус:** завершён
|
||||
**Тип изменения:** миграция / архитектурный рефакторинг
|
||||
**Подсистема:** Market Data / Exchange Integration / Trading Execution
|
||||
**Дата завершения:** 13 июля 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Цель Build 037 — перевести execution-потребителей с legacy-представлений рыночной котировки на каноническую модель `Quote`, сохранив существующее поведение торговой системы и обратную совместимость переходного периода.
|
||||
|
||||
Build продолжает миграционную последовательность:
|
||||
|
||||
```text
|
||||
Build 031 — Quotes Feed
|
||||
↓
|
||||
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 037 execution-контур получает рыночную котировку через канонический `Quote`, а специализированный execution boundary предоставляет типизированное представление `ExecutionPriceSnapshot`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектурный принцип
|
||||
|
||||
В рамках Build 037 зафиксировано следующее направление зависимости:
|
||||
|
||||
```text
|
||||
Dzengi REST / WebSocket
|
||||
↓
|
||||
Market Data Acquisition
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
↓
|
||||
Execution consumers
|
||||
```
|
||||
|
||||
Execution-потребители не должны самостоятельно:
|
||||
|
||||
- разбирать сырой payload биржи;
|
||||
- знать формат Dzengi REST или WebSocket;
|
||||
- обращаться напрямую к `Quote Store`;
|
||||
- зависеть от legacy `MarketPriceSnapshot`;
|
||||
- использовать dict-based market snapshot там, где необходим типизированный execution-контракт;
|
||||
- самостоятельно определять источник котировки.
|
||||
|
||||
Канонический объект рыночной котировки:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
Типизированное представление для execution-контура:
|
||||
|
||||
```python
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Область изменений
|
||||
|
||||
В рамках Build 037 изменены следующие production-файлы:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/trading/auto/execution_quality.py
|
||||
src/trading/debug/execution.py
|
||||
```
|
||||
|
||||
Добавлены специализированные тесты:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_execution_quote.py
|
||||
tests/unit/trading/auto/test_execution_quality.py
|
||||
tests/unit/trading/debug/test_execution.py
|
||||
```
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/execution/pricing.py
|
||||
```
|
||||
|
||||
был проанализирован, но не потребовал production-изменений, поскольку уже использовал типизированный boundary:
|
||||
|
||||
```python
|
||||
ExchangeService().get_execution_snapshot(symbol)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения в ExchangeService
|
||||
|
||||
### 4.1. Execution snapshot теперь строится из canonical Quote
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
get_execution_snapshot()
|
||||
```
|
||||
|
||||
переведён на получение канонической котировки через:
|
||||
|
||||
```python
|
||||
get_quote()
|
||||
```
|
||||
|
||||
Таким образом, execution boundary больше не зависит от legacy `MarketPriceSnapshot`.
|
||||
|
||||
Целевая цепочка:
|
||||
|
||||
```text
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
ExchangeService.get_execution_snapshot()
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2. Сохранён типизированный execution-контракт
|
||||
|
||||
Execution-потребители получают:
|
||||
|
||||
```python
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
с полями:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
is_fresh
|
||||
age_seconds
|
||||
```
|
||||
|
||||
Это позволяет execution-слою работать с явным типизированным контрактом вместо произвольного словаря.
|
||||
|
||||
---
|
||||
|
||||
### 4.3. Сохранена семантика freshness
|
||||
|
||||
При построении `ExecutionPriceSnapshot` сохраняется информация о возрасте котировки:
|
||||
|
||||
```text
|
||||
age_seconds
|
||||
```
|
||||
|
||||
и состоянии актуальности:
|
||||
|
||||
```text
|
||||
is_fresh
|
||||
```
|
||||
|
||||
Execution-контур продолжает использовать существующие ограничения максимального возраста котировки.
|
||||
|
||||
В частности:
|
||||
|
||||
```python
|
||||
_max_execution_snapshot_age_seconds = 5
|
||||
```
|
||||
|
||||
остаётся execution-policy и не переносится в слой Market Data.
|
||||
|
||||
Это соответствует разделению ответственности:
|
||||
|
||||
```text
|
||||
Market Data
|
||||
предоставляет Quote и объективный возраст данных
|
||||
|
||||
Execution
|
||||
определяет, допустим ли этот возраст для исполнения сделки
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения в execution quality
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/auto/execution_quality.py
|
||||
```
|
||||
|
||||
переведён с legacy dict-based market snapshot на типизированный:
|
||||
|
||||
```python
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
Ранее execution quality зависел от:
|
||||
|
||||
```python
|
||||
get_market_snapshot()
|
||||
```
|
||||
|
||||
и извлекал значения через:
|
||||
|
||||
```python
|
||||
snapshot.get("bid_price")
|
||||
snapshot.get("ask_price")
|
||||
snapshot.get("last_price")
|
||||
snapshot.get("age_seconds")
|
||||
snapshot.get("is_fresh")
|
||||
```
|
||||
|
||||
После Build 037 используется типизированный execution boundary:
|
||||
|
||||
```python
|
||||
get_execution_snapshot()
|
||||
```
|
||||
|
||||
и атрибуты:
|
||||
|
||||
```python
|
||||
snapshot.bid_price
|
||||
snapshot.ask_price
|
||||
snapshot.last_price
|
||||
snapshot.age_seconds
|
||||
snapshot.is_fresh
|
||||
```
|
||||
|
||||
Это устраняет зависимость execution quality от legacy dict-based представления котировки.
|
||||
|
||||
---
|
||||
|
||||
## 6. Устранение legacy fallback через get_price()
|
||||
|
||||
В execution quality существовал fallback через:
|
||||
|
||||
```python
|
||||
ExchangeService().get_price(...)
|
||||
```
|
||||
|
||||
В рамках Build 037 этот путь устранён из execution-потребителя.
|
||||
|
||||
Fallback теперь также проходит через типизированный execution boundary:
|
||||
|
||||
```python
|
||||
ExchangeService().get_execution_snapshot(...)
|
||||
```
|
||||
|
||||
Таким образом, основной и fallback-пути используют единый контракт данных.
|
||||
|
||||
Целевая схема:
|
||||
|
||||
```text
|
||||
canonical Quote
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
├── основной execution path
|
||||
└── fallback execution path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Изменения debug execution
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/debug/execution.py
|
||||
```
|
||||
|
||||
переведён с:
|
||||
|
||||
```python
|
||||
get_fresh_market_snapshot()
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```python
|
||||
get_execution_snapshot()
|
||||
```
|
||||
|
||||
Debug execution теперь использует тот же типизированный execution boundary, что и production execution.
|
||||
|
||||
Это устраняет архитектурное расхождение:
|
||||
|
||||
```text
|
||||
production execution → ExecutionPriceSnapshot
|
||||
debug execution → legacy dict snapshot
|
||||
```
|
||||
|
||||
и заменяет его единым подходом:
|
||||
|
||||
```text
|
||||
production execution → ExecutionPriceSnapshot
|
||||
debug execution → ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Side-aware execution pricing
|
||||
|
||||
Build 037 сохраняет существующую семантику выбора цены исполнения.
|
||||
|
||||
Для входа в LONG:
|
||||
|
||||
```text
|
||||
ask_price
|
||||
↓ fallback
|
||||
last_price
|
||||
```
|
||||
|
||||
Для входа в SHORT:
|
||||
|
||||
```text
|
||||
bid_price
|
||||
↓ fallback
|
||||
last_price
|
||||
```
|
||||
|
||||
Для выхода из LONG:
|
||||
|
||||
```text
|
||||
bid_price
|
||||
↓ fallback
|
||||
last_price
|
||||
```
|
||||
|
||||
Для выхода из SHORT:
|
||||
|
||||
```text
|
||||
ask_price
|
||||
↓ fallback
|
||||
last_price
|
||||
```
|
||||
|
||||
Для общего получения текущей рыночной цены:
|
||||
|
||||
```text
|
||||
last_price
|
||||
```
|
||||
|
||||
Это поведение не изменялось в рамках Build 037.
|
||||
|
||||
---
|
||||
|
||||
## 9. Что намеренно не изменялось
|
||||
|
||||
Build 037 ограничен execution-потребителями.
|
||||
|
||||
В рамках данного Build намеренно не переводились:
|
||||
|
||||
```text
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
```
|
||||
|
||||
Эти файлы относятся к следующему этапу:
|
||||
|
||||
```text
|
||||
Build 038 — Перевод strategy и runtime-потребителей на canonical Quote
|
||||
```
|
||||
|
||||
Также не выполнялись:
|
||||
|
||||
- удаление legacy `get_market_snapshot()`;
|
||||
- удаление legacy `get_fresh_market_snapshot()`;
|
||||
- удаление `MarketPriceCache`;
|
||||
- глобальная перестройка `ExchangeService`;
|
||||
- изменение утверждённой структуры каталогов;
|
||||
- изменение торговой стратегии;
|
||||
- изменение execution thresholds;
|
||||
- изменение логики открытия, закрытия или flip позиции.
|
||||
|
||||
---
|
||||
|
||||
## 10. Обратная совместимость
|
||||
|
||||
Build 037 выполнен как безопасный миграционный этап.
|
||||
|
||||
Legacy API не удалялись, поскольку некоторые потребители ещё используют переходные методы.
|
||||
|
||||
Сохраняются:
|
||||
|
||||
```text
|
||||
get_market_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
get_price()
|
||||
MarketPriceCache
|
||||
```
|
||||
|
||||
Их удаление допустимо только после полного перевода всех production-потребителей и отдельного контрольного `grep`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Тестовое покрытие
|
||||
|
||||
Для Build 037 добавлены специализированные unit-тесты:
|
||||
|
||||
```text
|
||||
tests/unit/integrations/exchange/test_service_execution_quote.py
|
||||
tests/unit/trading/auto/test_execution_quality.py
|
||||
tests/unit/trading/debug/test_execution.py
|
||||
```
|
||||
|
||||
Тестами подтверждено:
|
||||
|
||||
- `get_execution_snapshot()` использует canonical `Quote`;
|
||||
- execution service не зависит от legacy market snapshot;
|
||||
- execution quality использует `ExecutionPriceSnapshot`;
|
||||
- legacy `get_market_snapshot()` не используется новым execution quality path;
|
||||
- debug execution использует `get_execution_snapshot()`;
|
||||
- legacy `get_fresh_market_snapshot()` не используется новым debug execution path;
|
||||
- сохраняется корректная передача `last_price`;
|
||||
- сохраняется корректная передача `bid_price`;
|
||||
- сохраняется корректная передача `ask_price`;
|
||||
- сохраняется информация о возрасте котировки;
|
||||
- сохраняется freshness semantics.
|
||||
|
||||
---
|
||||
|
||||
## 12. Результаты проверки
|
||||
|
||||
Проверка синтаксиса:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/service.py \
|
||||
src/trading/auto/execution_quality.py \
|
||||
src/trading/debug/execution.py \
|
||||
tests/unit/integrations/exchange/test_service_execution_quote.py \
|
||||
tests/unit/trading/auto/test_execution_quality.py \
|
||||
tests/unit/trading/debug/test_execution.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
Специализированные тесты:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_execution_quote.py \
|
||||
tests/unit/trading/auto/test_execution_quality.py \
|
||||
tests/unit/trading/debug/test_execution.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
4 passed in 0.08s
|
||||
```
|
||||
|
||||
Полная регрессия:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
618 passed in 0.28s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Изменение количества тестов
|
||||
|
||||
До Build 037:
|
||||
|
||||
```text
|
||||
614 passed
|
||||
```
|
||||
|
||||
После Build 037:
|
||||
|
||||
```text
|
||||
618 passed
|
||||
```
|
||||
|
||||
Добавлено:
|
||||
|
||||
```text
|
||||
4 теста
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Итоговая архитектура после Build 037
|
||||
|
||||
После завершения Build 037 execution-контур выглядит следующим образом:
|
||||
|
||||
```text
|
||||
Dzengi REST / WebSocket
|
||||
↓
|
||||
Market Data Acquisition
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
ExchangeService.get_execution_snapshot()
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
├── trading/execution/pricing.py
|
||||
├── trading/auto/execution_quality.py
|
||||
└── trading/debug/execution.py
|
||||
```
|
||||
|
||||
Legacy market snapshot больше не является обязательным источником данных для переведённых execution-потребителей.
|
||||
|
||||
---
|
||||
|
||||
## 15. Критерии завершения Build 037
|
||||
|
||||
Build 037 считается завершённым, поскольку выполнены все обязательные условия:
|
||||
|
||||
- [x] `get_execution_snapshot()` строится из canonical `Quote`;
|
||||
- [x] execution quality переведён на `ExecutionPriceSnapshot`;
|
||||
- [x] debug execution переведён на `ExecutionPriceSnapshot`;
|
||||
- [x] legacy `get_price()` устранён из изменённого execution fallback path;
|
||||
- [x] `pricing.py` проверен и уже использует typed execution boundary;
|
||||
- [x] side-aware pricing сохранён;
|
||||
- [x] freshness semantics сохранена;
|
||||
- [x] legacy API не удалены преждевременно;
|
||||
- [x] специализированные тесты проходят;
|
||||
- [x] полная регрессия проходит;
|
||||
- [x] существующая торговая логика не изменена.
|
||||
|
||||
---
|
||||
|
||||
## 16. Следующий этап
|
||||
|
||||
Следующий этап миграции:
|
||||
|
||||
```text
|
||||
Build 038 — Перевод strategy и runtime-потребителей на canonical Quote
|
||||
```
|
||||
|
||||
Основные кандидаты следующего Build:
|
||||
|
||||
```text
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
```
|
||||
|
||||
Цель Build 038:
|
||||
|
||||
```text
|
||||
убрать зависимость strategy и runtime-потребителей
|
||||
от legacy dict-based market snapshot и перевести их
|
||||
на canonical Quote с сохранением существующей торговой семантики.
|
||||
```
|
||||
|
||||
После Build 038 должен быть выполнен новый контрольный `grep` для определения оставшихся production-зависимостей от:
|
||||
|
||||
```text
|
||||
get_market_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
get_price()
|
||||
MarketPriceCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. Статус
|
||||
|
||||
```text
|
||||
Build 037 — ЗАВЕРШЁН
|
||||
```
|
||||
|
||||
Следующий Build:
|
||||
|
||||
```text
|
||||
Build 038 — Перевод strategy и runtime-потребителей на canonical Quote
|
||||
```
|
||||
567
docs/migrations/build_038.md
Normal file
567
docs/migrations/build_038.md
Normal file
@@ -0,0 +1,567 @@
|
||||
# Build 038 — Перевод strategy и runtime-потребителей на canonical Quote
|
||||
|
||||
**Dzentra Market Data Migration**
|
||||
|
||||
---
|
||||
|
||||
## Контроль документа
|
||||
|
||||
| Свойство | Значение |
|
||||
|---|---|
|
||||
| Документ | Build 038 — Перевод strategy и runtime-потребителей на canonical Quote |
|
||||
| Тип документа | Build Record |
|
||||
| Версия | 1.0 |
|
||||
| Статус | **Completed** |
|
||||
| Подсистема | Market Data Acquisition |
|
||||
| Проект | Dzentra |
|
||||
| Язык | Русский |
|
||||
| Предыдущий этап | Build 037 — Перевод execution-потребителей |
|
||||
| Результат полной регрессии | **626 passed** |
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение Build 038
|
||||
|
||||
Build 038 завершает перевод выбранных strategy- и runtime-потребителей с legacy snapshot API на каноническую модель рыночной котировки:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
Цель этапа — исключить использование словарных market snapshot в следующих потребителях:
|
||||
|
||||
```text
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
```
|
||||
|
||||
и перевести их на единый типизированный источник текущей рыночной котировки:
|
||||
|
||||
```text
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Контекст предыдущих Build
|
||||
|
||||
Build 038 является продолжением последовательной миграции Market Data:
|
||||
|
||||
```text
|
||||
Build 031
|
||||
↓
|
||||
Canonical Quote model
|
||||
|
||||
Build 032
|
||||
↓
|
||||
Quote acquisition pipeline
|
||||
|
||||
Build 033
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
Build 034
|
||||
↓
|
||||
Dzengi WebSocket quote parsing и adapter
|
||||
|
||||
Build 035
|
||||
↓
|
||||
Market runtime переведён на Quotes Feed
|
||||
|
||||
Build 036
|
||||
↓
|
||||
Read-only и UI-потребители переведены на canonical Quote
|
||||
|
||||
Build 037
|
||||
↓
|
||||
Execution-потребители переведены на typed execution snapshot
|
||||
|
||||
Build 038
|
||||
↓
|
||||
Strategy и runtime-потребители переведены на canonical Quote
|
||||
```
|
||||
|
||||
В результате Build 038 каноническая модель `Quote` становится непосредственным источником текущей рыночной котировки для выбранных runtime- и strategy-компонентов.
|
||||
|
||||
---
|
||||
|
||||
## 3. Каноническая модель Quote
|
||||
|
||||
Используется модель:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Quote:
|
||||
symbol: str
|
||||
|
||||
last_price: Decimal
|
||||
bid_price: Decimal
|
||||
ask_price: Decimal
|
||||
|
||||
exchange_timestamp: datetime | None
|
||||
received_at: datetime
|
||||
|
||||
source: str
|
||||
```
|
||||
|
||||
Модель расположена в:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/quote.py
|
||||
```
|
||||
|
||||
Основные свойства модели:
|
||||
|
||||
- независимость от конкретного поставщика рыночных данных;
|
||||
- типизированные цены через `Decimal`;
|
||||
- отсутствие словарного доступа к ценовым полям;
|
||||
- наличие времени биржи;
|
||||
- наличие времени получения данных;
|
||||
- явное указание источника.
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменённые runtime-потребители
|
||||
|
||||
### 4.1. Auto Signal Runtime
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/auto/signal_runtime.py
|
||||
```
|
||||
|
||||
Legacy-путь:
|
||||
|
||||
```text
|
||||
ExchangeService.get_market_snapshot()
|
||||
↓
|
||||
dict[str, object]
|
||||
↓
|
||||
snapshot.get("bid_price")
|
||||
snapshot.get("ask_price")
|
||||
snapshot.get("last_price")
|
||||
```
|
||||
|
||||
Новый путь:
|
||||
|
||||
```text
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
quote.bid_price
|
||||
quote.ask_price
|
||||
quote.last_price
|
||||
```
|
||||
|
||||
В результате runtime больше не зависит от словарного представления текущей рыночной котировки.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменённые strategy-потребители
|
||||
|
||||
### 5.1. Trend Strategy
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/strategies/trend.py
|
||||
```
|
||||
|
||||
Стратегия переведена с legacy market snapshot на canonical `Quote`.
|
||||
|
||||
Новый источник:
|
||||
|
||||
```text
|
||||
ExchangeService.get_quote(
|
||||
symbol,
|
||||
runtime_key="auto",
|
||||
)
|
||||
```
|
||||
|
||||
Ценовые данные теперь читаются непосредственно из типизированной модели:
|
||||
|
||||
```text
|
||||
quote.last_price
|
||||
quote.bid_price
|
||||
quote.ask_price
|
||||
```
|
||||
|
||||
Сохранена существующая логика выбора цены анализа:
|
||||
|
||||
```text
|
||||
1. midpoint между bid и ask;
|
||||
2. last_price;
|
||||
3. 0.0 при отсутствии пригодной цены.
|
||||
```
|
||||
|
||||
Midpoint остаётся предпочтительной ценой анализа:
|
||||
|
||||
```text
|
||||
(bid + ask) / 2
|
||||
```
|
||||
|
||||
Это позволяет уменьшить зависимость анализа от случайного последнего исполнения сделки.
|
||||
|
||||
---
|
||||
|
||||
### 5.2. Scalp Strategy
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/strategies/scalp.py
|
||||
```
|
||||
|
||||
Стратегия также переведена на:
|
||||
|
||||
```text
|
||||
ExchangeService.get_quote(
|
||||
symbol,
|
||||
runtime_key="auto",
|
||||
)
|
||||
```
|
||||
|
||||
Ценовые поля теперь получаются через:
|
||||
|
||||
```text
|
||||
quote.last_price
|
||||
quote.bid_price
|
||||
quote.ask_price
|
||||
```
|
||||
|
||||
Сохранены:
|
||||
|
||||
- существующая логика определения analysis price;
|
||||
- midpoint между bid и ask;
|
||||
- fallback на `last_price`;
|
||||
- safe fallback;
|
||||
- существующие strategy payload;
|
||||
- существующие торговые пороги;
|
||||
- существующая логика принятия решений.
|
||||
|
||||
---
|
||||
|
||||
## 6. Обработка Decimal
|
||||
|
||||
Canonical `Quote` использует:
|
||||
|
||||
```text
|
||||
Decimal
|
||||
```
|
||||
|
||||
для полей:
|
||||
|
||||
```text
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
```
|
||||
|
||||
Существующие strategy helper-функции `_safe_float()` ранее принимали:
|
||||
|
||||
```text
|
||||
NumericLike | None
|
||||
```
|
||||
|
||||
где `Decimal` не входил в контракт `NumericLike`.
|
||||
|
||||
Это приводило к ошибкам статической типизации Pylance:
|
||||
|
||||
```text
|
||||
Аргумент типа "Decimal" нельзя присвоить параметру "value"
|
||||
типа "NumericLike | None"
|
||||
```
|
||||
|
||||
В рамках Build 038 контракт локальных strategy helper-функций расширен до:
|
||||
|
||||
```python
|
||||
value: NumericLike | Decimal | None
|
||||
```
|
||||
|
||||
Глобальный тип:
|
||||
|
||||
```text
|
||||
NumericLike
|
||||
```
|
||||
|
||||
не изменялся.
|
||||
|
||||
Это сохраняет локальность изменения и не расширяет общий типовой контракт проекта без необходимости.
|
||||
|
||||
Архитектурная граница имеет вид:
|
||||
|
||||
```text
|
||||
canonical Quote
|
||||
↓
|
||||
Decimal
|
||||
↓
|
||||
strategy helper
|
||||
↓
|
||||
float
|
||||
↓
|
||||
существующие strategy calculations и payload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Что намеренно не изменялось
|
||||
|
||||
Build 038 не изменяет:
|
||||
|
||||
- структуру canonical `Quote`;
|
||||
- `QuoteStore`;
|
||||
- acquisition pipeline;
|
||||
- WebSocket adapter;
|
||||
- REST adapter;
|
||||
- market runtime producer;
|
||||
- execution pricing semantics;
|
||||
- торговые пороги;
|
||||
- логику открытия позиции;
|
||||
- логику закрытия позиции;
|
||||
- flip-логику;
|
||||
- risk management;
|
||||
- journal payload contracts;
|
||||
- strategy payload keys;
|
||||
- Telegram UI;
|
||||
- структуру каталогов проекта.
|
||||
|
||||
Build 038 является локальным этапом миграции потребителей, а не изменением торговой логики.
|
||||
|
||||
---
|
||||
|
||||
## 8. Контроль legacy-зависимостей
|
||||
|
||||
После выполнения Build 038 выполнен контрольный поиск:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "get_market_snapshot\(|get_fresh_market_snapshot\(|get_execution_snapshot\(|get_price\(|snapshot\.get\(|quote\.get\(|runtime_key" \
|
||||
src/trading/auto/signal_runtime.py \
|
||||
src/trading/strategies/trend.py \
|
||||
src/trading/strategies/scalp.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
src/trading/auto/signal_runtime.py:881: runtime_key="auto",
|
||||
src/trading/strategies/trend.py:244: runtime_key="auto",
|
||||
src/trading/strategies/scalp.py:64: runtime_key="auto",
|
||||
```
|
||||
|
||||
Legacy-вызовы не обнаружены.
|
||||
|
||||
В проверенных файлах отсутствуют рабочие зависимости от:
|
||||
|
||||
```text
|
||||
get_market_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
get_execution_snapshot()
|
||||
get_price()
|
||||
snapshot.get(...)
|
||||
quote.get(...)
|
||||
```
|
||||
|
||||
Оставшиеся:
|
||||
|
||||
```text
|
||||
runtime_key="auto"
|
||||
```
|
||||
|
||||
являются ожидаемой частью вызова `get_quote()` и не представляют legacy-зависимость.
|
||||
|
||||
---
|
||||
|
||||
## 9. Проверка синтаксиса
|
||||
|
||||
Выполнена проверка:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/trading/auto/signal_runtime.py \
|
||||
src/trading/strategies/trend.py \
|
||||
src/trading/strategies/scalp.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
Ошибки синтаксиса отсутствуют.
|
||||
|
||||
---
|
||||
|
||||
## 10. Целевые тесты
|
||||
|
||||
После исправления типизации `Decimal` выполнены целевые тесты:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/trading/strategies/test_trend_quote.py \
|
||||
tests/unit/trading/strategies/test_scalp_quote.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
...... [100%]
|
||||
|
||||
6 passed in 0.09s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Полная регрессия
|
||||
|
||||
Выполнена полная проверка проекта:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
626 passed in 0.31s
|
||||
```
|
||||
|
||||
Регрессий не обнаружено.
|
||||
|
||||
Для сравнения:
|
||||
|
||||
```text
|
||||
После Build 037: 618 passed
|
||||
После Build 038: 626 passed
|
||||
```
|
||||
|
||||
Количество тестов увеличено на:
|
||||
|
||||
```text
|
||||
8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Итоговая архитектура после Build 038
|
||||
|
||||
После завершения Build 038 путь текущей рыночной котировки для мигрированных strategy- и runtime-потребителей выглядит следующим образом:
|
||||
|
||||
```text
|
||||
Dzengi WebSocket
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
├── Auto Signal Runtime
|
||||
├── Trend Strategy
|
||||
└── Scalp Strategy
|
||||
```
|
||||
|
||||
Для execution-контура сохраняется специализированная типизированная граница, введённая в Build 037:
|
||||
|
||||
```text
|
||||
canonical Quote
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
↓
|
||||
Execution consumers
|
||||
```
|
||||
|
||||
Таким образом, после Build 038 разделены два типа потребления:
|
||||
|
||||
```text
|
||||
Market / Strategy / Runtime
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
Execution
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Архитектурный результат
|
||||
|
||||
Build 038 устраняет ещё один слой legacy market snapshot API из рабочего торгового контура.
|
||||
|
||||
До Build 038:
|
||||
|
||||
```text
|
||||
Market Data
|
||||
↓
|
||||
legacy dict snapshot
|
||||
↓
|
||||
snapshot.get(...)
|
||||
↓
|
||||
runtime / strategies
|
||||
```
|
||||
|
||||
После Build 038:
|
||||
|
||||
```text
|
||||
Market Data
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
typed attributes
|
||||
↓
|
||||
runtime / strategies
|
||||
```
|
||||
|
||||
Это обеспечивает:
|
||||
|
||||
- единый канонический контракт текущей котировки;
|
||||
- статическую типизацию;
|
||||
- отказ от строковых ключей для доступа к ценам;
|
||||
- явную работу с `Decimal`;
|
||||
- уменьшение зависимости trading layer от legacy exchange representations;
|
||||
- подготовку к дальнейшему удалению legacy snapshot API.
|
||||
|
||||
---
|
||||
|
||||
## 14. Критерии завершения
|
||||
|
||||
Build 038 считается завершённым, поскольку выполнены все критерии:
|
||||
|
||||
- [x] `signal_runtime.py` переведён на canonical `Quote`;
|
||||
- [x] `trend.py` переведён на canonical `Quote`;
|
||||
- [x] `scalp.py` переведён на canonical `Quote`;
|
||||
- [x] legacy `get_market_snapshot()` удалён из мигрированных путей;
|
||||
- [x] словарный доступ `snapshot.get(...)` к текущей котировке удалён;
|
||||
- [x] типизация `Decimal` обработана явно;
|
||||
- [x] глобальный `NumericLike` не изменён;
|
||||
- [x] существующая торговая логика сохранена;
|
||||
- [x] strategy payload contracts сохранены;
|
||||
- [x] `py_compile` проходит успешно;
|
||||
- [x] целевые тесты проходят успешно;
|
||||
- [x] полная регрессия проходит успешно;
|
||||
- [x] итоговый результат — **626 passed**.
|
||||
|
||||
---
|
||||
|
||||
## 15. Статус
|
||||
|
||||
```text
|
||||
Build 038: COMPLETED
|
||||
```
|
||||
|
||||
Build 038 завершён и зафиксирован.
|
||||
|
||||
Система готова к следующему этапу миграции.
|
||||
246
docs/migrations/greps.txt
Normal file
246
docs/migrations/greps.txt
Normal file
@@ -0,0 +1,246 @@
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot % >....
|
||||
elif isinstance(data.get("payload"), dict):
|
||||
payload = data["payload"]
|
||||
if isinstance(payload.get("symbols"), list):
|
||||
symbols = payload["symbols"]
|
||||
|
||||
print("Количество symbols:", len(symbols) if symbols is not None else None)
|
||||
|
||||
if symbols:
|
||||
dict_items = [item for item in symbols if isinstance(item, dict)]
|
||||
|
||||
all_keys = sorted(
|
||||
{
|
||||
key
|
||||
for item in dict_items
|
||||
for key in item
|
||||
}
|
||||
)
|
||||
|
||||
print("Все ключи symbol items:")
|
||||
for key in all_keys:
|
||||
print(f" {key}")
|
||||
|
||||
print("\nПервый symbol item:")
|
||||
print(json.dumps(dict_items[0], ensure_ascii=False, indent=2))
|
||||
PY
|
||||
Корневой тип: dict
|
||||
Корневые ключи: ['exchangeFilters', 'rateLimits', 'serverTime', 'symbols', 'timezone']
|
||||
Количество symbols: 51
|
||||
Все ключи symbol items:
|
||||
assetType
|
||||
baseAsset
|
||||
baseAssetPrecision
|
||||
country
|
||||
filters
|
||||
industry
|
||||
longRate
|
||||
marketModes
|
||||
marketType
|
||||
maxSLGap
|
||||
maxTPGap
|
||||
minSLGap
|
||||
minTPGap
|
||||
name
|
||||
orderTypes
|
||||
quoteAsset
|
||||
quoteAssetId
|
||||
quotePrecision
|
||||
sector
|
||||
shortRate
|
||||
status
|
||||
swapChargeInterval
|
||||
symbol
|
||||
tickSize
|
||||
tickValue
|
||||
tradingFee
|
||||
tradingHours
|
||||
|
||||
Первый symbol item:
|
||||
{
|
||||
"assetType": "CRYPTOCURRENCY",
|
||||
"baseAsset": "ETH",
|
||||
"baseAssetPrecision": 3,
|
||||
"country": "",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "LOT_SIZE",
|
||||
"maxQty": "1000",
|
||||
"minQty": "0.001",
|
||||
"stepSize": "0.001"
|
||||
},
|
||||
{
|
||||
"filterType": "MIN_NOTIONAL",
|
||||
"minNotional": "2"
|
||||
}
|
||||
],
|
||||
"industry": "",
|
||||
"longRate": -0.01,
|
||||
"marketModes": [
|
||||
"REGULAR"
|
||||
],
|
||||
"marketType": "LEVERAGE",
|
||||
"maxSLGap": 50.0,
|
||||
"maxTPGap": 50.0,
|
||||
"minSLGap": 0,
|
||||
"minTPGap": 0,
|
||||
"name": "ETH/EUR",
|
||||
"orderTypes": [
|
||||
"LIMIT",
|
||||
"MARKET",
|
||||
"STOP"
|
||||
],
|
||||
"quoteAsset": "EUR",
|
||||
"quoteAssetId": "EUR_LEVERAGE",
|
||||
"quotePrecision": 3,
|
||||
"sector": "",
|
||||
"shortRate": 0.01,
|
||||
"status": "TRADING",
|
||||
"swapChargeInterval": 480,
|
||||
"symbol": "ETH/EUR_LEVERAGE",
|
||||
"tickSize": 0.01,
|
||||
"tickValue": 18.3415,
|
||||
"tradingFee": 0.06,
|
||||
"tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -"
|
||||
}
|
||||
|
||||
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot % >....
|
||||
filter_keys: dict[str, set[str]] = {}
|
||||
|
||||
for item in symbols:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
filters = item.get("filters")
|
||||
if not isinstance(filters, list):
|
||||
continue
|
||||
|
||||
for entry in filters:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
|
||||
filter_type = str(entry.get("filterType") or "<missing>")
|
||||
filter_types[filter_type] += 1
|
||||
filter_keys.setdefault(filter_type, set()).update(
|
||||
str(key) for key in entry
|
||||
)
|
||||
|
||||
print("Типы filters:")
|
||||
for filter_type, count in sorted(filter_types.items()):
|
||||
print(f"{filter_type}: {count}")
|
||||
print(" keys:", sorted(filter_keys[filter_type]))
|
||||
PY
|
||||
Типы filters:
|
||||
LOT_SIZE: 51
|
||||
keys: ['filterType', 'maxQty', 'minQty', 'stepSize']
|
||||
MIN_NOTIONAL: 39
|
||||
keys: ['filterType', 'minNotional']
|
||||
|
||||
|
||||
python - <<'PY'
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(
|
||||
"app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json"
|
||||
)
|
||||
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
if isinstance(data, dict) and isinstance(data.get("symbols"), list):
|
||||
symbols = data["symbols"]
|
||||
elif (
|
||||
isinstance(data, dict)
|
||||
and isinstance(data.get("payload"), dict)
|
||||
and isinstance(data["payload"].get("symbols"), list)
|
||||
):
|
||||
symbols = data["payload"]["symbols"]
|
||||
else:
|
||||
raise SystemExit("symbols не найдены")
|
||||
|
||||
fields = [
|
||||
"symbol",
|
||||
"name",
|
||||
"status",
|
||||
"baseAsset",
|
||||
"quoteAsset",
|
||||
"marketModes",
|
||||
"marketType",
|
||||
"tickSize",
|
||||
"stepSize",
|
||||
"minQty",
|
||||
"minNotional",
|
||||
"filters",
|
||||
]
|
||||
|
||||
for field in fields:
|
||||
present = 0
|
||||
non_empty = 0
|
||||
types = Counter()
|
||||
|
||||
for item in symbols:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
if field in item:
|
||||
present += 1
|
||||
value = item[field]
|
||||
types[type(value).__name__] += 1
|
||||
|
||||
if value not in (None, "", [], {}):
|
||||
non_empty += 1
|
||||
|
||||
print(
|
||||
f"{field}: present={present}, "
|
||||
f"non_empty={non_empty}, "
|
||||
f"types={dict(types)}"
|
||||
)
|
||||
PY
|
||||
symbol: present=51, non_empty=51, types={'str': 51}
|
||||
name: present=51, non_empty=51, types={'str': 51}
|
||||
status: present=51, non_empty=51, types={'str': 51}
|
||||
baseAsset: present=51, non_empty=51, types={'str': 51}
|
||||
quoteAsset: present=51, non_empty=51, types={'str': 51}
|
||||
marketModes: present=51, non_empty=51, types={'list': 51}
|
||||
marketType: present=51, non_empty=51, types={'str': 51}
|
||||
tickSize: present=51, non_empty=51, types={'float': 48, 'int': 3}
|
||||
stepSize: present=0, non_empty=0, types={}
|
||||
minQty: present=0, non_empty=0, types={}
|
||||
minNotional: present=0, non_empty=0, types={}
|
||||
filters: present=51, non_empty=51, types={'list': 51}
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot % ;2B
|
||||
|
||||
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot % ;2Bgrep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "from src\.telegram\.handlers\.market import|import src\.telegram\.handlers\.market|include_router\(.*market|market\.router|handlers\.market" \
|
||||
app/src app/tests tests 2>/dev/null
|
||||
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot % grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "include_router|include_routers" \
|
||||
app/src \
|
||||
| grep -Ei "market|router"
|
||||
app/src/telegram/routers.py:16: dispatcher.include_router(start_router)
|
||||
app/src/telegram/routers.py:17: dispatcher.include_router(home_router)
|
||||
app/src/telegram/routers.py:18: dispatcher.include_router(portfolio_router)
|
||||
app/src/telegram/routers.py:19: dispatcher.include_router(auto_router)
|
||||
app/src/telegram/routers.py:20: dispatcher.include_router(journal_router)
|
||||
app/src/telegram/routers.py:21: dispatcher.include_router(debug_auto_router)
|
||||
app/src/telegram/routers.py:22: dispatcher.include_router(debug_router)
|
||||
app/src/telegram/routers.py:23: dispatcher.include_router(system_router)
|
||||
app/src/telegram/handlers/auto/__init__.py:8:router.include_router(main_router)
|
||||
app/src/telegram/handlers/auto/__init__.py:9:router.include_router(risk_router)
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot %
|
||||
|
||||
((.venv) ) segeba@mbpbsg dzentra_bot % grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "from src\.telegram\.ui\.currency_ui import|import src\.telegram\.ui\.currency_ui" \
|
||||
app/src app/tests tests 2>/dev/null
|
||||
app/src/telegram/handlers/market.py:28:from src.telegram.ui.currency_ui import format_usd_amount
|
||||
app/src/telegram/handlers/portfolio.py:26:from src.telegram.ui.currency_ui import (
|
||||
201
docs/migrations/instrument_reference_data_migration.md
Normal file
201
docs/migrations/instrument_reference_data_migration.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Dzentra --- Instrument Reference Data Migration Plan
|
||||
|
||||
> Статус: Утверждённый базовый план миграции
|
||||
|
||||
## Общая стратегия
|
||||
|
||||
Миграция выполняется постепенно без нарушения работы существующего бота.
|
||||
|
||||
Основные принципы:
|
||||
|
||||
- не переписывать подсистему целиком;
|
||||
- не удалять legacy-код до полного перевода потребителей;
|
||||
- сохранять публичные интерфейсы `ExchangeService`;
|
||||
- не создавать параллельную бизнес-логику без плана удаления;
|
||||
- разделять Acquisition, Validation, Storage и Processing;
|
||||
- выполнять миграцию небольшими проверяемыми Build.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## План Build
|
||||
|
||||
Build Цель Совместимость
|
||||
------- ---------------------------------------------------- ------------------------
|
||||
001 Внутренняя модель Instrument Reference Data Без изменения runtime
|
||||
002 Raw-модели ответа Dzengi Полная
|
||||
003 Структурная валидация exchangeInfo Полная
|
||||
004 Parser exchangeInfo Полная
|
||||
005 Value validation Полная
|
||||
006 Mapper Dzengi → Instrument Полная
|
||||
007 Protocol и Exceptions Полная
|
||||
008 Dzengi REST Adapter Полная
|
||||
009 Instrument Handler Полная
|
||||
010 Instrument Feed Полная
|
||||
011 Registry Полная
|
||||
012 Acquisition Service Полная
|
||||
013 Проверка эквивалентности старой и новой реализации Полная
|
||||
014 Compatibility mapper Instrument → ExchangeSymbol Полная
|
||||
015 Переключение get_exchange_symbols() Полная
|
||||
016 Перевод normalize_symbol()/symbol_candidates() Полная
|
||||
017 Переключение validate_symbol() Полная
|
||||
018 Переключение get_symbol_runtime_status() Полная
|
||||
019 Подготовка переноса кэша в Storage Полная
|
||||
020 Перенос кэша Полная
|
||||
021 Перевод первой группы потребителей Полная
|
||||
022 Перевод валидации и runtime-статусов на канонический Instrumen
|
||||
023 Перевод рыночных runtime-потребителей на канонический Instrument
|
||||
024 Удаление неиспользуемого legacy Market Handler
|
||||
025 Удаление ExchangeSymbol compatibility layer и завершение миграции
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Зависимости
|
||||
|
||||
``` text
|
||||
001
|
||||
↓
|
||||
002
|
||||
↓
|
||||
003
|
||||
↓
|
||||
004
|
||||
↓
|
||||
005
|
||||
↓
|
||||
006
|
||||
↓
|
||||
007
|
||||
↓
|
||||
008
|
||||
↓
|
||||
009
|
||||
↓
|
||||
010
|
||||
↓
|
||||
011
|
||||
↓
|
||||
012
|
||||
↓
|
||||
013
|
||||
↓
|
||||
014
|
||||
↓
|
||||
015
|
||||
├──→016→017
|
||||
└──→018
|
||||
↓
|
||||
019→020
|
||||
↓
|
||||
021→022+
|
||||
↓
|
||||
023
|
||||
↓
|
||||
024
|
||||
↓
|
||||
025
|
||||
```
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Точки обратной совместимости
|
||||
|
||||
До завершения миграции должны сохраняться:
|
||||
|
||||
- `ExchangeService.get_exchange_symbols()`
|
||||
- `ExchangeService.validate_symbol()`
|
||||
- `ExchangeService.get_symbol_runtime_status()`
|
||||
|
||||
Также сохраняются:
|
||||
|
||||
- сигнатуры методов;
|
||||
- старые импорты;
|
||||
- формат ошибок;
|
||||
- Telegram UI;
|
||||
- автоторговля;
|
||||
- существующее поведение runtime;
|
||||
- совместимость `ExchangeSymbol` через временный compatibility mapper.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Классификация изменений
|
||||
|
||||
Тип Значение
|
||||
-------------------------------------- ----------------------------------
|
||||
Исправление ошибки Исправляет неверную работу
|
||||
Обязательное архитектурное изменение Необходимо для новой архитектуры
|
||||
Улучшение надёжности Не меняет наблюдаемое поведение
|
||||
Изменение поведения Требует отдельного согласования
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Улучшения допускаются
|
||||
|
||||
Допускается:
|
||||
|
||||
- усиление типизации;
|
||||
- безопасная обработка неполных данных;
|
||||
- исправление ошибок parser;
|
||||
- корректная обработка filters;
|
||||
- улучшение надёжности REST;
|
||||
- отделение транспортной логики от предметной;
|
||||
- тестируемые контракты.
|
||||
|
||||
Недопустимо без согласования:
|
||||
|
||||
- изменение поведения;
|
||||
- изменение TTL кэша;
|
||||
- изменение правил валидации;
|
||||
- изменение логики UI.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Build 001
|
||||
|
||||
### Цель
|
||||
|
||||
Создать независимую внутреннюю модель Instrument Reference Data.
|
||||
|
||||
На этом этапе:
|
||||
|
||||
- не переносится parser;
|
||||
- не создаётся REST adapter;
|
||||
- не меняется ExchangeService;
|
||||
- не меняется runtime;
|
||||
- не переносится кэш.
|
||||
|
||||
### Для начала Build 001 необходимо получить
|
||||
|
||||
Обязательно:
|
||||
|
||||
1. модель `ExchangeSymbol`;
|
||||
2. `SymbolValidationResult`;
|
||||
3. `normalize_symbol()`;
|
||||
4. `symbol_candidates()`;
|
||||
5. `validate_symbol()`;
|
||||
6. `integrations/exchange/service.py`;
|
||||
7. текущий parser `exchangeInfo`;
|
||||
8. текущий mapper (если существует);
|
||||
9. модели ответа `exchangeInfo`;
|
||||
10. REST-клиент `exchangeInfo`;
|
||||
11. обработку filters/status/marketModes.
|
||||
|
||||
Также нужны результаты `grep` по использованию этих сущностей и, если
|
||||
существуют, соответствующие тесты.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Обязательные правила
|
||||
|
||||
После каждого Build:
|
||||
|
||||
1. анализ;
|
||||
2. внесение изменений только текущего этапа;
|
||||
3. обновление тестов;
|
||||
4. проверка импортов;
|
||||
5. проверка синтаксиса;
|
||||
6. запуск тестов;
|
||||
7. запуск приложения;
|
||||
8. проверка обратной совместимости.
|
||||
|
||||
Только после успешного завершения Build допускается переход к следующему
|
||||
этапу.
|
||||
70
docs/migrations/Вывод grep по дополнительным полям.txt
Normal file
70
docs/migrations/Вывод grep по дополнительным полям.txt
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user