Files
dzentra_bot/docs/migrations/build_012.md

1556 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 012 — Instrument Acquisition Service
**Статус:** Завершён
**Подсистема:** `market_data/acquisition`
**Область:** Instrument Reference Data
**Тип изменения:** Изолированное добавление application-level сервиса получения справочника инструментов без подключения к production runtime
**Результат полного набора тестов:** `180 passed`
---
## 1. Цель Build 012
Цель Build 012 — реализовать application-level сервис, который получает Instrument Feed из Registry и запускает загрузку справочника инструментов.
Реализован класс:
```text
InstrumentAcquisitionService
```
Его архитектурная граница:
```text
source_name
InstrumentAcquisitionService
InstrumentFeedRegistry
InstrumentFeedProtocol
load_instruments()
tuple[Instrument, ...]
```
Build 012 завершает orchestration-слой новой изолированной acquisition pipeline.
На этом этапе сервис:
```text
получает Feed из Registry;
вызывает Feed;
возвращает результат Feed без изменения;
сохраняет специализированные ошибки без wrapping.
```
Build 012 не подключается к существующему `ExchangeService`, не меняет legacy runtime и не переключает production-потребителей на новую реализацию.
---
## 2. Почему Build 012 выполняется именно сейчас
До начала Build 012 были завершены:
```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 — Protocol и Exceptions
Build 008 — Dzengi REST Adapter
Build 009 — Instrument Handler
Build 010 — Instrument Feed
Build 011 — Instrument Feed Registry
```
После Build 011 уже существовала цепочка:
```text
source_name
InstrumentFeedRegistry
InstrumentFeedProtocol
```
И отдельно существовал полный Feed pipeline:
```text
InstrumentFeed
InstrumentDocumentSource
DzengiInstrumentDocumentSource
Dzengi REST API
raw document
InstrumentDocumentHandler
DzengiInstrumentDocumentHandler
schema validation
parser
value validation
mapper
tuple[Instrument, ...]
```
Однако отсутствовала application-level точка входа, соединяющая Registry с запуском Feed.
Build 012 создаёт эту точку:
```text
InstrumentAcquisitionService
```
---
## 3. Архитектурная ответственность Acquisition Service
Единственная предметная ответственность сервиса:
```text
получить source_name
получить Feed через Registry
вызвать load_instruments()
вернуть tuple[Instrument, ...]
```
Минимальная логика сервиса:
```python
feed = self._registry.get(source_name)
return feed.load_instruments()
```
Сервис не содержит собственной transport-, parsing-, validation-, mapping- или storage-логики.
---
## 4. Изменённые файлы
В рамках Build 012 реализован:
```text
app/src/market_data/acquisition/service.py
```
Создан тестовый файл:
```text
app/tests/unit/market_data/acquisition/test_service.py
```
Другие production-файлы не изменялись.
В частности, не изменялись:
```text
app/src/market_data/acquisition/exceptions.py
app/src/market_data/acquisition/protocol.py
app/src/market_data/acquisition/registry.py
app/src/market_data/acquisition/feeds/instrument_feed.py
app/src/market_data/acquisition/adapters/dzengi/rest.py
app/src/market_data/acquisition/handlers/instrument_handler.py
app/src/integrations/exchange/*
app/src/telegram/*
app/src/trading/*
```
Re-export в `__init__.py` не добавлялся.
---
## 5. Реализованный InstrumentAcquisitionService
В файле:
```text
app/src/market_data/acquisition/service.py
```
реализован класс:
```python
class InstrumentAcquisitionService:
...
```
Его публичный контракт:
```python
def load_instruments(
self,
source_name: str,
) -> tuple[Instrument, ...]:
...
```
Полная схема:
```text
InstrumentAcquisitionService.load_instruments(source_name)
InstrumentFeedRegistry.get(source_name)
InstrumentFeedProtocol
InstrumentFeedProtocol.load_instruments()
tuple[Instrument, ...]
```
---
## 6. Явная dependency injection Registry
Registry передаётся сервису через конструктор:
```python
def __init__(
self,
*,
registry: InstrumentFeedRegistry,
) -> None:
self._registry = registry
```
Правильная композиция:
```python
registry = InstrumentFeedRegistry()
registry.register(
"dzengi",
feed,
)
service = InstrumentAcquisitionService(
registry=registry,
)
```
После этого:
```python
instruments = service.load_instruments("dzengi")
```
Сервис не создаёт Registry самостоятельно.
---
## 7. Почему Service не создаёт Registry
Внутри `InstrumentAcquisitionService` отсутствует:
```python
self._registry = InstrumentFeedRegistry()
```
Это принципиальное архитектурное решение.
Если бы Service самостоятельно создавал Registry:
```text
Service
создаёт Registry
Registry изначально пуст
требуется скрытая регистрация Feed
Service начинает знать о конкретных источниках
```
Вместо этого используется:
```text
готовый Registry
передаётся Service
```
Это обеспечивает:
```text
явные зависимости;
контролируемую composition;
независимость от конкретного источника;
простое тестирование;
отсутствие скрытой инициализации.
```
---
## 8. Независимость от Dzengi
`InstrumentAcquisitionService` не импортирует и не создаёт:
```text
DzengiInstrumentDocumentSource
DzengiInstrumentDocumentHandler
DzengiExchangeInfoResponse
ExchangeRestClient
InstrumentFeed
Dzengi parser
Dzengi mapper
```
Сервис зависит только от:
```text
Instrument
InstrumentFeedRegistry
```
Архитектурная схема:
```text
InstrumentAcquisitionService
InstrumentFeedRegistry
InstrumentFeedProtocol
```
Конкретный источник определяется снаружи через регистрацию Feed.
---
## 9. Передача source_name без изменения
Service не выполняет над `source_name`:
```text
strip()
lower()
casefold()
replace()
alias resolution
automatic source mapping
```
Он непосредственно передаёт полученное значение Registry:
```python
feed = self._registry.get(source_name)
```
Например:
```text
" dzengi "
```
передаётся в Registry именно как:
```text
" dzengi "
```
Нормализация внешних пробелов является ответственностью:
```text
InstrumentFeedRegistry
```
Это исключает дублирование правил между Service и Registry.
---
## 10. Получение Feed через Registry
Service не хранит Feed напрямую.
Отсутствует:
```python
self._feed = feed
```
Вместо этого для каждого вызова:
```python
service.load_instruments(source_name)
```
выполняется:
```python
feed = self._registry.get(source_name)
```
Таким образом:
```text
source_name
Registry
соответствующий Feed
```
Service не определяет самостоятельно, какой Feed использовать.
---
## 11. Однократный вызов Registry
Для одного вызова:
```python
service.load_instruments("dzengi")
```
метод:
```python
registry.get("dzengi")
```
вызывается ровно один раз.
Отсутствуют:
```text
повторный lookup;
предварительная проверка наличия;
двойной get();
fallback lookup.
```
Это подтверждено unit-тестом.
---
## 12. Однократный вызов Feed
После успешного получения Feed выполняется:
```python
feed.load_instruments()
```
ровно один раз.
Цепочка:
```text
Service.load_instruments()
Registry.get()
Feed.load_instruments()
return result
```
Отсутствуют:
```text
retry;
повторный вызов после ошибки;
предварительный вызов;
дополнительная проверочная загрузка.
```
---
## 13. Возврат результата без копирования
Service непосредственно возвращает:
```python
return feed.load_instruments()
```
Он не выполняет:
```python
tuple(feed.load_instruments())
```
или:
```python
result = feed.load_instruments()
return tuple(result)
```
Поэтому сохраняется identity результата:
```python
result is instruments
```
равно:
```text
True
```
Это подтверждено unit-тестами.
---
## 14. Сохранение порядка инструментов
Service не выполняет:
```text
sorting;
filtering;
deduplication;
grouping;
reordering.
```
Если Feed возвращает:
```text
BTC/USD_LEVERAGE
ETH/USD_LEVERAGE
XRP/USD_LEVERAGE
```
Service возвращает инструменты в том же порядке:
```text
BTC/USD_LEVERAGE
ETH/USD_LEVERAGE
XRP/USD_LEVERAGE
```
Порядок результата Feed сохраняется.
---
## 15. Поведение при пустом результате
Если Feed возвращает:
```python
()
```
Service также возвращает:
```python
()
```
без ошибки.
Service не интерпретирует пустой результат как:
```text
transport error;
schema error;
value error;
mapping error;
отсутствие источника.
```
На Build 012 отсутствует утверждённое правило, согласно которому пустой `tuple` должен считаться ошибкой.
---
## 16. Сохранение специализированных ошибок
В `InstrumentAcquisitionService` отсутствует общий `try/except`, который заменял бы исходные ошибки новой общей ошибкой.
Без изменения могут пройти:
```text
InstrumentFeedRegistryError
InstrumentReferenceTransportError
InstrumentReferenceSchemaError
InstrumentReferenceParseError
InstrumentReferenceValueError
InstrumentReferenceMappingError
```
Схема:
```text
Registry error
Service
та же Registry error
```
или:
```text
Feed error
Service
та же Feed error
```
---
## 17. Сохранение identity ошибки
Тесты подтверждают не только тип ошибки, но и сохранение исходного объекта.
Если Feed выбрасывает:
```python
original_error = InstrumentReferenceTransportError(
"Network error."
)
```
то Service передаёт именно этот объект:
```python
exc_info.value is original_error
```
равно:
```text
True
```
Ошибка не:
```text
копируется;
оборачивается;
заменяется;
переводится в другой тип.
```
---
## 18. Ошибка отсутствующего Feed
Если Registry не содержит источник:
```text
"dzengi"
```
вызов:
```python
service.load_instruments("dzengi")
```
приводит к:
```text
InstrumentFeedRegistryError
```
Service не выполняет:
```text
fallback;
создание Feed;
регистрацию Feed;
использование default source;
возврат пустого tuple.
```
Ошибка Registry проходит наружу без wrapping.
---
## 19. Feed не вызывается при ошибке Registry
Последовательность:
```text
Service.load_instruments()
Registry.get()
InstrumentFeedRegistryError
```
останавливается на ошибке Registry.
Метод:
```text
feed.load_instruments()
```
не вызывается.
Это подтверждено unit-тестом.
---
## 20. Отсутствие retry после ошибки Feed
Если Feed выбрасывает:
```text
InstrumentReferenceTransportError
```
Service не повторяет вызов.
Схема:
```text
feed.load_instruments()
InstrumentReferenceTransportError
ошибка немедленно выходит из Service
```
Счётчик вызовов Feed остаётся:
```text
1
```
Таким образом, Build 012 не вводит скрытую retry policy.
---
## 21. Почему retry отсутствует
Retry является отдельной operational policy.
Для его корректной реализации необходимо отдельно определить:
```text
какие ошибки являются retryable;
максимальное число попыток;
интервалы между попытками;
backoff;
jitter;
timeout budget;
логирование повторных попыток;
поведение при исчерпании попыток.
```
Build 012 не должен неявно принимать эти архитектурные решения.
Поэтому:
```text
один вызов Service
один вызов Feed
```
---
## 22. Почему не создан новый Service exception
В Build 012 не добавлен:
```text
InstrumentAcquisitionServiceError
```
Уже существуют специализированные ошибки:
```text
InstrumentFeedRegistryError
InstrumentReferenceTransportError
InstrumentReferenceSchemaError
InstrumentReferenceParseError
InstrumentReferenceValueError
InstrumentReferenceMappingError
```
Создание общей ошибки:
```text
InstrumentAcquisitionServiceError
```
и wrapping всех причин в неё ухудшило бы диагностируемость.
Поэтому сохраняется точная причина отказа.
---
## 23. Что Service не хранит
Внутри `InstrumentAcquisitionService` отсутствуют:
```text
tuple[Instrument, ...];
последний успешный справочник;
предыдущий snapshot;
timestamp;
TTL;
cache age;
последняя ошибка;
индекс инструментов;
legacy ExchangeSymbol.
```
Service хранит только зависимость:
```text
InstrumentFeedRegistry
```
---
## 24. Что Service не делает
Build 012 сознательно не выполняет:
```text
REST-запросы напрямую;
получение exchangeInfo напрямую;
schema validation;
parsing;
value validation;
mapping;
создание Registry;
создание Feed;
создание Source;
создание Handler;
автоматическую регистрацию Feed;
retry;
backoff;
кэширование;
хранение Instrument;
сравнение snapshot;
фильтрацию инструментов;
сортировку инструментов;
дедупликацию инструментов;
нормализацию торговых символов;
преобразование Instrument в ExchangeSymbol;
изменение ExchangeService;
изменение AutoTrade;
изменение Telegram UI;
изменение trading runtime;
формирование пользовательских ошибок;
логирование событий.
```
Единственная orchestration-ответственность:
```text
Registry lookup
Feed invocation
```
---
## 25. Production composition не входит в Build 012
Build 012 не создаёт автоматически полную production-композицию:
```text
DzengiInstrumentDocumentSource
+
DzengiInstrumentDocumentHandler
InstrumentFeed
InstrumentFeedRegistry
InstrumentAcquisitionService
```
На текущем этапе новая pipeline остаётся изолированной.
Не создаётся:
```text
global Registry;
global Service;
singleton Feed;
application startup wiring;
dependency container;
ExchangeService integration.
```
Это предотвращает преждевременное изменение production runtime.
---
## 26. Реализованные тестовые сценарии
Создан файл:
```text
app/tests/unit/market_data/acquisition/test_service.py
```
Фактически выполнено:
```text
13 tests
```
Проверены следующие сценарии:
1. загрузка инструментов из зарегистрированного Feed;
2. передача `source_name` Registry без изменения;
3. однократный вызов Registry;
4. однократный вызов Feed;
5. возврат результата без копирования;
6. сохранение порядка инструментов;
7. возврат пустого tuple без ошибки;
8. сохранение Registry error без wrapping;
9. отсутствие вызова Feed при ошибке Registry;
10. сохранение transport error без wrapping;
11. сохранение value error без wrapping;
12. сохранение mapping error без wrapping;
13. отсутствие retry после ошибки Feed.
Часть сценариев реализована через параметризацию.
Фактическое количество выполненных тестовых случаев:
```text
13
```
---
## 27. Проверка получения инструментов
Тест подтверждает:
```python
registry = InstrumentFeedRegistry()
instruments = (
_instrument(),
)
feed = StubInstrumentFeed(
instruments=instruments,
)
registry.register(
"dzengi",
feed,
)
service = InstrumentAcquisitionService(
registry=registry,
)
result = service.load_instruments("dzengi")
assert result is instruments
```
Это доказывает:
```text
Service получил Feed через Registry;
Feed был вызван;
результат Feed возвращён;
identity результата сохранена.
```
---
## 28. Проверка передачи source_name без изменения
Используется тестовый Registry, который записывает полученные имена.
Вызов:
```python
service.load_instruments(" dzengi ")
```
приводит к передаче в Registry именно:
```text
" dzengi "
```
Тест подтверждает:
```python
assert registry.requested_source_names == [
" dzengi ",
]
```
Service не дублирует нормализацию Registry.
---
## 29. Проверка отсутствия retry
Тестовый Feed содержит счётчик:
```python
self.load_call_count = 0
```
При вызове:
```python
feed.load_instruments()
```
счётчик увеличивается.
Feed настроен на выбрасывание:
```text
InstrumentReferenceTransportError
```
После вызова Service подтверждено:
```python
assert feed.load_call_count == 1
```
Это доказывает отсутствие скрытой повторной попытки.
---
## 30. Выполненные проверки
### Проверка 1 — unit-тесты Acquisition Service
Команда:
```bash
python -m pytest \
tests/unit/market_data/acquisition/test_service.py \
-q
```
Результат:
```text
............. [100%]
13 passed in 0.01s
```
Статус:
```text
PASSED
```
---
### Проверка 2 — Python compilation
Команда:
```bash
python -m py_compile \
src/market_data/acquisition/service.py \
tests/unit/market_data/acquisition/test_service.py
```
Результат:
```text
Команда завершилась без ошибок и без вывода.
```
Статус:
```text
PASSED
```
---
### Проверка 3 — полный набор тестов проекта
Команда:
```bash
python -m pytest -q
```
Результат:
```text
.................................................................................................................................................................................... [100%]
180 passed in 0.09s
```
Статус:
```text
PASSED
```
---
### Проверка 4 — отсутствие преждевременной production-интеграции
Команда:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "InstrumentAcquisitionService" \
src tests
```
Полученное production-определение находится только в:
```text
src/market_data/acquisition/service.py
```
Остальные использования находятся исключительно в:
```text
tests/unit/market_data/acquisition/test_service.py
```
Не обнаружено подключения к:
```text
ExchangeService
Telegram UI
AutoTrade
Trading runtime
другим production-потребителям
```
Статус:
```text
PASSED
```
---
## 31. Архитектура после Build 012
После завершения Build 012 новая часть подсистемы имеет следующую структуру:
```text
market_data/
└── acquisition/
├── exceptions.py
│ ├── MarketDataAcquisitionError
│ ├── InstrumentReferenceTransportError
│ ├── InstrumentReferenceSchemaError
│ ├── InstrumentReferenceParseError
│ ├── InstrumentReferenceValueError
│ ├── InstrumentReferenceMappingError
│ └── InstrumentFeedRegistryError
├── protocol.py
│ ├── InstrumentDocumentSource
│ ├── InstrumentDocumentHandler
│ └── InstrumentFeedProtocol
├── registry.py
│ └── InstrumentFeedRegistry
├── service.py
│ └── InstrumentAcquisitionService
├── models/
│ └── instrument.py
│ └── Instrument
├── validation/
│ ├── schema.py
│ └── values.py
├── handlers/
│ └── instrument_handler.py
│ └── DzengiInstrumentDocumentHandler
├── feeds/
│ └── instrument_feed.py
│ └── InstrumentFeed
└── adapters/
└── dzengi/
├── models.py
├── parser.py
├── mapper.py
└── rest.py
├── _PayloadRestClient
└── DzengiInstrumentDocumentSource
```
---
## 32. Полная архитектурная цепочка после Build 012
После Build 012 реализована полная изолированная acquisition pipeline:
```text
source_name
InstrumentAcquisitionService
InstrumentFeedRegistry
InstrumentFeedProtocol
InstrumentFeed
InstrumentDocumentSource
DzengiInstrumentDocumentSource
ExchangeRestClient.get_payload()
Dzengi REST API
```
После получения документа:
```text
object
InstrumentDocumentHandler
DzengiInstrumentDocumentHandler
validate_exchange_info_schema()
ValidatedExchangeInfoDocument
parse_exchange_info()
DzengiExchangeInfoResponse
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
Таким образом, техническая цепочка новой acquisition-подсистемы завершена.
---
## 33. Что означает завершение изолированной acquisition pipeline
После Build 012 новая подсистема уже содержит все необходимые уровни:
```text
Internal Model
Raw Models
Schema Validation
Parser
Value Validation
Mapper
Protocols
REST Adapter
Handler
Feed
Registry
Acquisition Service
```
Но она ещё не заменяет legacy implementation.
Существующий бот продолжает использовать старый путь.
---
## 34. Что ещё не реализовано
После Build 012 отсутствуют:
```text
проверка эквивалентности старой и новой реализации;
compatibility mapper Instrument → ExchangeSymbol;
переключение get_exchange_symbols();
перевод normalize_symbol()/symbol_candidates();
переключение validate_symbol();
переключение get_symbol_runtime_status();
подготовка переноса кэша;
перенос кэша;
перевод production-потребителей;
удаление legacy-кода.
```
Эти задачи относятся к следующим Build.
---
## 35. Влияние на legacy-систему
Build 012 не подключён к существующим компонентам:
```text
ExchangeService
ExchangeSymbol
SymbolValidationResult
Telegram UI
AutoTrade
Market Stream
Market Data Runner
Execution Quality
Trading runtime
```
Не изменены:
```text
ExchangeService.get_exchange_symbols()
ExchangeService.validate_symbol()
ExchangeService.get_symbol_runtime_status()
normalize_symbol()
symbol_candidates()
```
Старый production-путь продолжает работать без изменений.
---
## 36. Обратная совместимость
Подтверждено сохранение:
```text
сигнатур существующих legacy-методов;
старых импортов;
существующего формата legacy-ошибок;
Telegram UI;
автоторговли;
runtime-поведения;
legacy ExchangeSymbol;
legacy-кэша.
```
Build 012 имеет полную обратную совместимость.
---
## 37. Классификация изменений
| Изменение | Классификация |
|---|---|
| `InstrumentAcquisitionService` | Обязательное архитектурное изменение |
| Registry lookup → Feed invocation | Обязательное архитектурное изменение |
| Явная dependency injection Registry | Обязательное разделение ответственности |
| Передача `source_name` без изменения | Исключение дублирования ответственности |
| Однократный вызов Registry | Предсказуемое orchestration-поведение |
| Однократный вызов Feed | Предсказуемое orchestration-поведение |
| Возврат результата без копирования | Сохранение контракта Feed |
| Сохранение специализированных ошибок | Улучшение диагностируемости |
| Retry | Не выполняется |
| Кэширование | Не выполняется |
| Production composition | Не выполняется |
| Изменение legacy-кода | Отсутствует |
| Изменение production-поведения | Отсутствует |
---
## 38. Условие завершения Build 012
Все условия выполнены:
```text
InstrumentAcquisitionService
реализован;
Registry
передаётся как явная зависимость;
source_name
передаётся Registry без изменения;
Feed
получается через Registry;
Registry
вызывается ровно один раз;
Feed
вызывается ровно один раз;
результат Feed
возвращается без копирования;
порядок Instrument
сохраняется;
пустой tuple
возвращается без ошибки;
специализированные ошибки
сохраняются без wrapping;
identity ошибки
сохраняется;
Feed
не вызывается при ошибке Registry;
retry
отсутствует;
кэширование
отсутствует;
прямые Dzengi-зависимости
отсутствуют;
unit-тесты
проходят;
полный pytest
проходит;
production runtime
не затронут.
```
---
## 39. Итог Build 012
Build 012 завершён успешно.
Реализовано:
```text
InstrumentAcquisitionService
source_name
InstrumentFeedRegistry.get()
InstrumentFeedProtocol
load_instruments()
tuple[Instrument, ...]
```
Подтверждено:
```text
13 Acquisition Service tests passed
180 total project tests passed
Python compilation passed
No premature production integration detected
Legacy bot behavior unchanged
```
Итоговый статус:
```text
BUILD 012 — COMPLETE
```
---
## 40. Следующий этап
Следующий этап утверждённого плана:
```text
Build 013 — Проверка эквивалентности старой и новой реализации
```
Его задача — до любого переключения production-кода доказать, что legacy и новая implementation получают эквивалентные данные из одного и того же реального `exchangeInfo`.
Целевая схема:
```text
один реальный exchangeInfo document
├──→ legacy implementation
│ ↓
│ list[ExchangeSymbol]
└──→ new acquisition pipeline
tuple[Instrument, ...]
equivalence comparison
```
На Build 013 необходимо определить точный набор полей для сравнения:
```text
symbol;
name;
status;
base_asset;
quote_asset;
asset_type;
market_type;
market_modes;
order_types;
precisions;
tick_size;
tick_value;
step_size;
min_qty;
max_qty;
min_notional;
country;
sector;
industry;
trading_hours.
```
Build 013 не должен:
```text
переключать ExchangeService.get_exchange_symbols();
изменять validate_symbol();
изменять get_symbol_runtime_status();
создавать compatibility mapper;
переносить кэш;
изменять Telegram UI;
изменять AutoTrade;
изменять trading runtime.
```
Только после доказанной эквивалентности можно переходить к:
```text
Build 014 — Compatibility mapper Instrument → ExchangeSymbol
```