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