1411 lines
30 KiB
Markdown
1411 lines
30 KiB
Markdown
# 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.
|
||
``` |