1556 lines
32 KiB
Markdown
1556 lines
32 KiB
Markdown
# 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
|
||
``` |