build 039: complete Quotes Feed migration foundation

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

View 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 по-прежнему изменяться не будет.

View 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 останется полностью независимым от бизнес-логики.

View 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-поведение работающего бота.

View 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 001003 стало безопасно реализовать 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

File diff suppressed because it is too large Load Diff

1071
docs/migrations/build_006.md Normal file

File diff suppressed because it is too large Load Diff

View 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 008012 предполагаемая полная цепочка будет выглядеть так:
```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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View 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
```

View 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 001012
создана новая независимая 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()
```

View 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
```

View 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
```

View 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

View 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
```

View 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-совместимости**.

View 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` без нарушения работоспособности существующего бота.

View 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

File diff suppressed because it is too large Load Diff

View 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:** ✅ Завершён.

View 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:** ✅ Завершён.

View 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:** ✅ Завершён.

View 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 027040.
---
## 16. Статус завершения
**Build 026 завершён полностью.**
Дополнительных изменений кода в рамках Build 026 не требуется.
Следующий этап:
```text
Build 027 — Каноническая модель Quote и специализированные контракты
```

View 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
```

View File

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

View 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 027028.
После его завершения сформирована цепочка:
```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
```

View 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 и существующими потребителями работающего бота.

View 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

File diff suppressed because it is too large Load Diff

View 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 и адаптер
```

View 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-механизмов и с сохранением работоспособности существующего бота до завершения последующих этапов миграции.

View 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-потребители остаются отдельным последующим этапом миграции.

View 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.

View 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
```

View 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
View 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 (

View 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 допускается переход к следующему
этапу.

File diff suppressed because one or more lines are too long