Files
dzentra_bot/docs/migrations/build_010.md

1411 lines
30 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 010 — Instrument Feed
**Статус:** Завершён
**Подсистема:** `market_data/acquisition`
**Область:** Instrument Reference Data
**Тип изменения:** Изолированное добавление orchestration-компонента Feed без подключения к production runtime
**Результат полного набора тестов:** `142 passed`
---
## 1. Цель Build 010
Цель Build 010 — реализовать общий Instrument Feed, соединяющий уже существующие контракты:
```text
InstrumentDocumentSource
InstrumentDocumentHandler
```
и предоставляющий единый интерфейс:
```text
InstrumentFeedProtocol
```
Реализован класс:
```text
InstrumentFeed
```
Архитектурная граница Build 010:
```text
InstrumentFeed.load_instruments()
InstrumentDocumentSource.fetch_instrument_document()
object
InstrumentDocumentHandler.handle_instrument_document()
tuple[Instrument, ...]
```
Build 010 не создаёт Registry, Acquisition Service, compatibility mapper и не подключается к существующему `ExchangeService`.
---
## 2. Почему Build 010 выполняется именно сейчас
До начала Build 010 были завершены:
```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 009 существовали две независимые части acquisition pipeline.
### Получение документа
```text
Dzengi REST API
ExchangeRestClient.get_payload()
DzengiInstrumentDocumentSource
object
```
### Обработка документа
```text
object
DzengiInstrumentDocumentHandler
validate_exchange_info_schema()
parse_exchange_info()
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
До Build 010 эти две части не были соединены общим orchestration-компонентом.
Build 010 реализовал эту связь через абстрактные контракты:
```text
InstrumentDocumentSource
InstrumentDocumentHandler
```
без зависимости самого Feed от конкретной биржи или формата документа.
---
## 3. Изменённые файлы
В рамках Build 010 реализован production-файл:
```text
app/src/market_data/acquisition/feeds/instrument_feed.py
```
Создан тестовый файл:
```text
app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py
```
Другие production-файлы не изменялись.
---
## 4. Реализованный Instrument Feed
В файле:
```text
app/src/market_data/acquisition/feeds/instrument_feed.py
```
реализован класс:
```python
class InstrumentFeed:
...
```
Его публичный метод:
```python
def load_instruments(self) -> tuple[Instrument, ...]:
...
```
соответствует контракту:
```text
InstrumentFeedProtocol
```
---
## 5. Зависимости Instrument Feed
`InstrumentFeed` получает две обязательные зависимости:
```python
def __init__(
self,
*,
source: InstrumentDocumentSource,
handler: InstrumentDocumentHandler,
) -> None:
...
```
Первая зависимость:
```text
InstrumentDocumentSource
```
отвечает за получение исходного документа.
Вторая зависимость:
```text
InstrumentDocumentHandler
```
отвечает за преобразование исходного документа во внутренние модели:
```text
tuple[Instrument, ...]
```
Обе зависимости передаются явно через конструктор.
---
## 6. Реализованный orchestration pipeline
Метод:
```python
load_instruments()
```
выполняет только две операции:
```python
document = self._source.fetch_instrument_document()
return self._handler.handle_instrument_document(document)
```
Полная последовательность:
```text
InstrumentFeed.load_instruments()
source.fetch_instrument_document()
object
handler.handle_instrument_document(document)
tuple[Instrument, ...]
```
Feed не реализует самостоятельно transport, parsing, validation или mapping.
---
## 7. Разделение ответственности
После Build 010 обязанности компонентов разделены следующим образом.
### InstrumentDocumentSource
Отвечает за:
```text
получение документа от внешнего источника.
```
Конкретная реализация:
```text
DzengiInstrumentDocumentSource
```
---
### InstrumentDocumentHandler
Отвечает за:
```text
преобразование исходного документа
во внутренние модели Instrument.
```
Конкретная реализация:
```text
DzengiInstrumentDocumentHandler
```
---
### InstrumentFeed
Отвечает только за:
```text
получить документ через Source;
передать документ Handler;
вернуть результат Handler.
```
Feed не знает внутренней реализации Source и Handler.
---
## 8. Независимость Instrument Feed от Dzengi
В `instrument_feed.py` отсутствуют прямые зависимости от:
```text
DzengiInstrumentDocumentSource
DzengiInstrumentDocumentHandler
ExchangeRestClient
DzengiExchangeInfoResponse
DzengiExchangeInfoSymbol
Dzengi parser
Dzengi mapper
Dzengi validation pipeline
```
Feed зависит только от общих контрактов:
```text
InstrumentDocumentSource
InstrumentDocumentHandler
```
и внутренней модели:
```text
Instrument
```
Архитектурная зависимость:
```text
InstrumentFeed
InstrumentDocumentSource Protocol
конкретная реализация определяется снаружи
```
```text
InstrumentFeed
InstrumentDocumentHandler Protocol
конкретная реализация определяется снаружи
```
Таким образом, Feed остаётся source-independent orchestration-компонентом.
---
## 9. Почему Feed не создаёт зависимости самостоятельно
В Build 010 намеренно не используется:
```python
self._source = DzengiInstrumentDocumentSource()
self._handler = DzengiInstrumentDocumentHandler()
```
Такое решение напрямую связало бы общий Feed с конкретным источником Dzengi.
Вместо этого используется явная dependency injection:
```python
InstrumentFeed(
source=source,
handler=handler,
)
```
Это обеспечивает:
```text
независимость от конкретной биржи;
явные архитектурные зависимости;
тестируемость без сети;
возможность другой реализации Source;
возможность другой реализации Handler;
отсутствие скрытой composition logic.
```
Создание конкретной production-композиции не относится к ответственности Feed.
---
## 10. Почему аргументы конструктора keyword-only
Конструктор объявлен как:
```python
def __init__(
self,
*,
source: InstrumentDocumentSource,
handler: InstrumentDocumentHandler,
) -> None:
...
```
Символ:
```text
*
```
делает аргументы keyword-only.
Корректное создание:
```python
InstrumentFeed(
source=source,
handler=handler,
)
```
Это явно показывает роли зависимостей и исключает позиционную неоднозначность.
---
## 11. Отсутствие нового Feed exception
В Build 010 намеренно не создавалась ошибка:
```text
InstrumentReferenceFeedError
```
Feed сохраняет без дополнительного wrapping специализированные ошибки нижележащих компонентов:
```text
InstrumentReferenceTransportError
InstrumentReferenceSchemaError
InstrumentReferenceParseError
InstrumentReferenceValueError
InstrumentReferenceMappingError
```
Это сохраняет точную диагностическую классификацию отказов.
---
## 12. Отсутствие try/except в Instrument Feed
В `load_instruments()` намеренно отсутствует:
```python
try:
...
except Exception:
...
```
Если Source выбрасывает:
```text
InstrumentReferenceTransportError
```
Feed передаёт тот же объект исключения вызывающему коду.
Если Handler выбрасывает:
```text
InstrumentReferenceSchemaError
InstrumentReferenceParseError
InstrumentReferenceValueError
InstrumentReferenceMappingError
```
Feed также передаёт исходное исключение без изменения.
Цепочка:
```text
нижний компонент
специализированное исключение
InstrumentFeed
то же исключение
внешний потребитель
```
---
## 13. Поведение при transport error
Если:
```text
source.fetch_instrument_document()
```
выбрасывает:
```text
InstrumentReferenceTransportError
```
то:
```text
ошибка передаётся без wrapping;
handler не вызывается;
повторный запрос не выполняется.
```
Последовательность:
```text
InstrumentFeed.load_instruments()
source.fetch_instrument_document()
InstrumentReferenceTransportError
выполнение прекращается
```
Handler в этом случае не получает документ.
---
## 14. Поведение при processing error
Если Source успешно возвращает документ:
```text
object
```
но Handler выбрасывает специализированную processing-ошибку, например:
```text
InstrumentReferenceValueError
```
то Feed:
```text
не перехватывает ошибку;
не заменяет её другим типом;
не повторяет Source;
не повторяет Handler.
```
Последовательность:
```text
source
document
handler
InstrumentReferenceValueError
внешний потребитель
```
---
## 15. Отсутствие retry
Build 010 не реализует retry.
Если Source завершился ошибкой:
```text
InstrumentReferenceTransportError
```
Feed не выполняет:
```text
повторный REST-запрос;
задержку;
backoff;
повторный вызов Source.
```
В рамках одного вызова:
```python
feed.load_instruments()
```
Source вызывается ровно один раз.
Retry policy, если она потребуется, должна находиться на другом архитектурном уровне и не должна скрыто появляться внутри базового Feed.
---
## 16. Передача документа без изменения
Документ, возвращённый Source:
```python
document = self._source.fetch_instrument_document()
```
передаётся непосредственно Handler:
```python
self._handler.handle_instrument_document(document)
```
Feed не выполняет:
```text
копирование;
преобразование;
нормализацию;
фильтрацию;
валидацию;
оборачивание;
изменение структуры.
```
Тестом подтверждено сохранение identity:
```python
handler.documents[0] is document
```
---
## 17. Возврат результата без изменения
Если Handler возвращает:
```python
instruments: tuple[Instrument, ...]
```
Feed возвращает непосредственно тот же объект:
```python
return self._handler.handle_instrument_document(document)
```
Feed не выполняет:
```python
tuple(instruments)
```
или другое копирование коллекции.
Тестом подтверждено:
```python
result is instruments
```
Это гарантирует отсутствие скрытого преобразования результата.
---
## 18. Сохранение порядка инструментов
Feed не выполняет:
```text
сортировку;
фильтрацию;
дедупликацию;
перегруппировку.
```
Если Handler возвращает:
```text
BTC/USD_LEVERAGE
ETH/USD_LEVERAGE
XRP/USD_LEVERAGE
```
Feed возвращает инструменты в том же порядке:
```text
BTC/USD_LEVERAGE
ETH/USD_LEVERAGE
XRP/USD_LEVERAGE
```
Ответственность за порядок данных не переносится в Feed.
---
## 19. Поведение при пустом результате
Если Handler возвращает:
```python
()
```
Feed возвращает:
```python
()
```
без ошибки.
Feed не определяет, является ли пустой справочник:
```text
допустимым состоянием;
временной ошибкой;
критическим отказом;
условием для сохранения предыдущего snapshot.
```
Такая policy logic не относится к ответственности Feed.
---
## 20. Что Instrument Feed не делает
Build 010 сознательно не выполняет:
```text
REST-запросы самостоятельно;
парсинг JSON;
schema validation;
value validation;
mapping;
retry;
backoff;
кэширование;
фильтрацию инструментов;
сортировку инструментов;
удаление дубликатов;
нормализацию символов;
проверку exchange_enabled;
создание Registry;
создание Acquisition Service;
создание legacy ExchangeSymbol;
создание compatibility mapper;
логирование;
формирование Telegram-сообщений;
изменение ExchangeService;
изменение AutoTrade;
изменение trading runtime.
```
Единственная ответственность Feed:
```text
Source
Handler
tuple[Instrument, ...]
```
---
## 21. Реализованные тестовые сценарии
Создан файл:
```text
app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py
```
Реализовано 10 тестов.
Проверены следующие сценарии:
1. `InstrumentFeed` соответствует `InstrumentFeedProtocol`;
2. Source вызывается один раз;
3. Handler вызывается один раз;
4. документ передаётся Handler без изменения;
5. результат Handler возвращается без изменения;
6. порядок инструментов сохраняется;
7. пустой `tuple` возвращается без ошибки;
8. `InstrumentReferenceTransportError` сохраняется без wrapping;
9. processing error сохраняется без wrapping;
10. после transport error Source не вызывается повторно.
---
## 22. Проверка соответствия InstrumentFeedProtocol
Тестом подтверждено:
```python
assert isinstance(feed, InstrumentFeedProtocol)
```
Это возможно благодаря:
```text
@runtime_checkable
```
в определении Protocol и структурному соответствию метода:
```python
load_instruments() -> tuple[Instrument, ...]
```
Feed не наследуется явно от Protocol.
Используется structural typing:
```text
если объект реализует требуемый контракт,
он соответствует Protocol.
```
---
## 23. Проверка вызова Source
Тестовый Source ведёт счётчик:
```python
self.call_count = 0
```
При каждом вызове:
```python
fetch_instrument_document()
```
значение увеличивается.
После:
```python
feed.load_instruments()
```
подтверждено:
```python
assert source.call_count == 1
```
Таким образом, Feed не выполняет скрытых повторных запросов.
---
## 24. Проверка вызова Handler
Тестовый Handler сохраняет полученные документы:
```python
self.documents: list[object] = []
```
При вызове:
```python
handle_instrument_document(document)
```
документ добавляется в список.
После:
```python
feed.load_instruments()
```
подтверждено:
```python
assert len(handler.documents) == 1
```
Handler вызывается ровно один раз.
---
## 25. Проверка transport error
Создан исходный объект ошибки:
```python
original_error = InstrumentReferenceTransportError(
"Не удалось получить exchangeInfo."
)
```
Source выбрасывает именно этот объект.
Тест подтверждает:
```python
assert exc_info.value is original_error
```
Это доказывает отсутствие:
```text
wrapping;
замены исключения;
создания нового объекта ошибки.
```
Дополнительно подтверждено:
```python
assert source.call_count == 1
assert handler.documents == []
```
То есть:
```text
Source вызван один раз;
Handler не вызван.
```
---
## 26. Проверка processing error
Создан исходный объект:
```python
original_error = InstrumentReferenceValueError(
"Некорректное значение."
)
```
Handler выбрасывает именно этот объект.
Тест подтверждает:
```python
assert exc_info.value is original_error
```
Дополнительно:
```python
assert source.call_count == 1
assert handler.documents == [document]
```
Это подтверждает корректную последовательность:
```text
Source успешно вызван один раз
документ передан Handler
Handler выбросил processing error
ошибка вышла из Feed без изменения
```
---
## 27. Выполненные проверки
### Проверка 1 — unit-тесты Instrument Feed
Команда:
```bash
python -m pytest \
tests/unit/market_data/acquisition/feeds/test_instrument_feed.py \
-q
```
Результат:
```text
.......... [100%]
10 passed in 0.01s
```
Статус:
```text
PASSED
```
---
### Проверка 2 — Python compilation
Команда:
```bash
python -m py_compile \
src/market_data/acquisition/feeds/instrument_feed.py \
tests/unit/market_data/acquisition/feeds/test_instrument_feed.py
```
Результат:
```text
Команда завершилась без ошибок и без вывода.
```
Статус:
```text
PASSED
```
---
### Проверка 3 — полный набор тестов проекта
Команда:
```bash
python -m pytest -q
```
Результат:
```text
.............................................................................................................................................. [100%]
142 passed in 0.08s
```
Статус:
```text
PASSED
```
---
### Проверка 4 — отсутствие преждевременной production-интеграции
Команда:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "InstrumentFeed|InstrumentFeedProtocol" \
src tests
```
Полученные production-использования:
```text
src/market_data/acquisition/protocol.py:
InstrumentFeedProtocol
src/market_data/acquisition/feeds/instrument_feed.py:
InstrumentFeed
```
Остальные использования находятся исключительно в unit-тестах:
```text
tests/unit/market_data/acquisition/test_protocol.py
tests/unit/market_data/acquisition/feeds/test_instrument_feed.py
```
Не обнаружено подключения к:
```text
ExchangeService
Registry
Acquisition Service
Telegram UI
AutoTrade
Trading runtime
другим production-потребителям
```
Статус:
```text
PASSED
```
---
## 28. Архитектура после Build 010
После завершения Build 010 новая часть подсистемы имеет следующую структуру:
```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
├── handlers/
│ └── instrument_handler.py
│ └── DzengiInstrumentDocumentHandler
├── feeds/
│ └── instrument_feed.py
│ └── InstrumentFeed
└── adapters/
└── dzengi/
├── models.py
├── parser.py
├── mapper.py
└── rest.py
├── _PayloadRestClient
└── DzengiInstrumentDocumentSource
```
На текущем этапе ещё не реализованы:
```text
registry.py
service.py
```
---
## 29. Полная архитектурная цепочка после Build 010
После Build 010 реализована единая техническая цепочка:
```text
Dzengi REST API
ExchangeRestClient.get_payload()
DzengiInstrumentDocumentSource
object
InstrumentFeed
DzengiInstrumentDocumentHandler
validate_exchange_info_schema()
ValidatedExchangeInfoDocument
parse_exchange_info()
DzengiExchangeInfoResponse
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
При этом сам `InstrumentFeed` не знает, что используются:
```text
DzengiInstrumentDocumentSource
DzengiInstrumentDocumentHandler
```
Для него зависимости представлены только общими контрактами:
```text
InstrumentDocumentSource
InstrumentDocumentHandler
```
---
## 30. Что ещё не реализовано
После Build 010 новая acquisition pipeline уже способна технически выполнить:
```text
REST API
document
validation
parsing
value validation
mapping
tuple[Instrument, ...]
```
Однако пока отсутствуют:
```text
Registry;
Acquisition Service;
официальная production composition;
equivalence verification с legacy implementation;
compatibility mapper Instrument → ExchangeSymbol;
переключение get_exchange_symbols();
перевод normalize_symbol()/symbol_candidates();
переключение validate_symbol();
переключение get_symbol_runtime_status();
перенос кэша.
```
Поэтому новая цепочка остаётся изолированной от существующего production runtime.
---
## 31. Влияние на legacy-систему
Build 010 не подключён к существующим компонентам:
```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-путь продолжает работать без изменений.
---
## 32. Обратная совместимость
Подтверждено сохранение:
```text
сигнатур существующих методов;
старых импортов;
существующего формата legacy-ошибок;
Telegram UI;
автоторговли;
runtime-поведения;
legacy ExchangeSymbol;
legacy-кэша.
```
Build 010 имеет полную обратную совместимость.
---
## 33. Классификация изменений
| Изменение | Классификация |
|---|---|
| `InstrumentFeed` | Обязательное архитектурное изменение |
| Соединение Source и Handler | Обязательное архитектурное изменение |
| Dependency injection Source и Handler | Обязательное разделение ответственности |
| Keyword-only зависимости | Улучшение надёжности API |
| Сохранение специализированных ошибок | Улучшение диагностируемости |
| Новый Feed exception | Не создаётся |
| Retry | Не добавляется |
| Кэширование | Не добавляется |
| Фильтрация и сортировка | Не выполняются |
| Изменение legacy-кода | Отсутствует |
| Изменение production-поведения | Отсутствует |
---
## 34. Условие завершения Build 010
Все условия выполнены:
```text
InstrumentFeed
реализован;
InstrumentFeedProtocol
соблюдён;
InstrumentDocumentSource
передаётся как явная зависимость;
InstrumentDocumentHandler
передаётся как явная зависимость;
Source
вызывается ровно один раз;
Handler
вызывается ровно один раз после успешного Source;
документ
передаётся Handler без изменения;
результат
возвращается без копирования и преобразования;
порядок инструментов
сохраняется;
пустой tuple
поддерживается;
специализированные ошибки
сохраняются без wrapping;
retry
отсутствует;
Dzengi-зависимости в Feed
отсутствуют;
unit-тесты
проходят;
полный pytest
проходит;
Feed
не подключён к production runtime.
```
---
## 35. Итог Build 010
Build 010 завершён успешно.
Реализовано:
```text
InstrumentFeed
InstrumentDocumentSource
fetch_instrument_document()
object
InstrumentDocumentHandler
handle_instrument_document()
tuple[Instrument, ...]
```
Подтверждено:
```text
10 Instrument Feed tests passed
142 total project tests passed
Python compilation passed
No premature production integration detected
Legacy bot behavior unchanged
```
Итоговый статус:
```text
BUILD 010 — COMPLETE
```
---
## 36. Следующий этап
Следующий этап утверждённого плана:
```text
Build 011 — Registry
```
Registry хранит доступные Instrument Feed и предоставляет нужный Feed по идентификатору источника.
Хранение актуального справочника относится к будущему переносу кэша:
```text
Build 019 — подготовка переноса кэша
Build 020 — перенос кэша в Storage
```
Предполагаемая архитектурная граница:
```text
tuple[Instrument, ...]
Instrument Registry
доступ к актуальному справочнику инструментов
```
Build 011 не должен:
```text
создавать Acquisition Service;
изменять ExchangeService;
переключать legacy get_exchange_symbols();
создавать compatibility mapper;
переносить legacy-кэш;
подключаться к Telegram UI;
изменять AutoTrade;
изменять trading runtime.
```