1602 lines
33 KiB
Markdown
1602 lines
33 KiB
Markdown
# Build 011 — Instrument Feed Registry
|
||
|
||
**Статус:** Завершён
|
||
**Подсистема:** `market_data/acquisition`
|
||
**Область:** Instrument Reference Data
|
||
**Тип изменения:** Изолированное добавление Registry для регистрации и получения Instrument Feed без подключения к production runtime
|
||
**Результат полного набора тестов:** `167 passed`
|
||
|
||
---
|
||
|
||
## 1. Цель Build 011
|
||
|
||
Цель Build 011 — реализовать Registry для регистрации и получения доступных Instrument Feed по идентификатору источника.
|
||
|
||
Реализован класс:
|
||
|
||
```text
|
||
InstrumentFeedRegistry
|
||
```
|
||
|
||
Его архитектурная граница:
|
||
|
||
```text
|
||
source_name
|
||
↓
|
||
InstrumentFeedRegistry
|
||
↓
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
Registry позволяет:
|
||
|
||
```text
|
||
зарегистрировать Feed;
|
||
получить Feed по имени источника;
|
||
запретить неявную повторную регистрацию;
|
||
явно сообщить об отсутствии запрошенного Feed.
|
||
```
|
||
|
||
Build 011 не хранит модели `Instrument`, не выполняет acquisition, не создаёт кэш и не подключается к существующему `ExchangeService`.
|
||
|
||
---
|
||
|
||
## 2. Почему Build 011 выполняется именно сейчас
|
||
|
||
До начала Build 011 были завершены:
|
||
|
||
```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 010 уже существовала полная техническая цепочка:
|
||
|
||
```text
|
||
Dzengi REST API
|
||
↓
|
||
DzengiInstrumentDocumentSource
|
||
↓
|
||
InstrumentFeed
|
||
↓
|
||
DzengiInstrumentDocumentHandler
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Однако будущему `Acquisition Service` ещё требовался механизм получения нужного Feed без прямой зависимости от конкретной реализации.
|
||
|
||
Build 011 создаёт эту границу:
|
||
|
||
```text
|
||
Acquisition Service
|
||
↓
|
||
source_name
|
||
↓
|
||
InstrumentFeedRegistry
|
||
↓
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Архитектурное уточнение ответственности Registry
|
||
|
||
Registry в Build 011 не является хранилищем актуального справочника инструментов.
|
||
|
||
Он не хранит:
|
||
|
||
```text
|
||
tuple[Instrument, ...];
|
||
последний успешный справочник;
|
||
предыдущий snapshot;
|
||
TTL;
|
||
время обновления;
|
||
возраст данных;
|
||
последнюю ошибку;
|
||
индекс символов;
|
||
legacy ExchangeSymbol.
|
||
```
|
||
|
||
Его единственная предметная ответственность:
|
||
|
||
```text
|
||
source_name
|
||
→
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
Хранение и кэширование актуальных данных относятся к другим архитектурным слоям и будущим Build:
|
||
|
||
```text
|
||
Build 019 — подготовка переноса кэша в Storage
|
||
Build 020 — перенос кэша
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Изменённые файлы
|
||
|
||
В рамках Build 011 изменены:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/exceptions.py
|
||
app/src/market_data/acquisition/registry.py
|
||
```
|
||
|
||
Создан тестовый файл:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/test_registry.py
|
||
```
|
||
|
||
Другие production-файлы не изменялись.
|
||
|
||
---
|
||
|
||
## 5. Реализованный InstrumentFeedRegistry
|
||
|
||
В файле:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/registry.py
|
||
```
|
||
|
||
реализован класс:
|
||
|
||
```python
|
||
class InstrumentFeedRegistry:
|
||
...
|
||
```
|
||
|
||
Registry предоставляет два публичных метода:
|
||
|
||
```python
|
||
def register(
|
||
self,
|
||
source_name: str,
|
||
feed: InstrumentFeedProtocol,
|
||
) -> None:
|
||
...
|
||
```
|
||
|
||
и:
|
||
|
||
```python
|
||
def get(
|
||
self,
|
||
source_name: str,
|
||
) -> InstrumentFeedProtocol:
|
||
...
|
||
```
|
||
|
||
Минимальный публичный контракт:
|
||
|
||
```text
|
||
register(source_name, feed)
|
||
↓
|
||
регистрация Feed
|
||
|
||
get(source_name)
|
||
↓
|
||
получение зарегистрированного Feed
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Внутреннее хранение
|
||
|
||
Registry использует внутренний типизированный словарь:
|
||
|
||
```python
|
||
dict[str, InstrumentFeedProtocol]
|
||
```
|
||
|
||
Архитектурная схема:
|
||
|
||
```text
|
||
"dzengi"
|
||
↓
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
Пример:
|
||
|
||
```text
|
||
{
|
||
"dzengi": <InstrumentFeedProtocol>
|
||
}
|
||
```
|
||
|
||
Внутренний словарь:
|
||
|
||
```text
|
||
не возвращается наружу;
|
||
не содержит Instrument;
|
||
не является предметным кэшем;
|
||
не содержит результатов Feed;
|
||
используется только как индекс зарегистрированных Feed.
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Регистрация Feed
|
||
|
||
Метод:
|
||
|
||
```python
|
||
register(
|
||
source_name: str,
|
||
feed: InstrumentFeedProtocol,
|
||
) -> None
|
||
```
|
||
|
||
выполняет следующую последовательность:
|
||
|
||
```text
|
||
получить source_name
|
||
↓
|
||
удалить внешние пробелы
|
||
↓
|
||
проверить непустое имя
|
||
↓
|
||
проверить соответствие InstrumentFeedProtocol
|
||
↓
|
||
проверить отсутствие существующей регистрации
|
||
↓
|
||
сохранить Feed
|
||
```
|
||
|
||
Пример:
|
||
|
||
```python
|
||
registry.register(
|
||
"dzengi",
|
||
feed,
|
||
)
|
||
```
|
||
|
||
После этого:
|
||
|
||
```python
|
||
registry.get("dzengi")
|
||
```
|
||
|
||
возвращает тот же объект `feed`.
|
||
|
||
---
|
||
|
||
## 8. Получение Feed
|
||
|
||
Метод:
|
||
|
||
```python
|
||
get(
|
||
source_name: str,
|
||
) -> InstrumentFeedProtocol
|
||
```
|
||
|
||
выполняет:
|
||
|
||
```text
|
||
получить source_name
|
||
↓
|
||
удалить внешние пробелы
|
||
↓
|
||
проверить непустое имя
|
||
↓
|
||
найти зарегистрированный Feed
|
||
↓
|
||
вернуть тот же объект Feed
|
||
```
|
||
|
||
Если Feed отсутствует, выбрасывается:
|
||
|
||
```text
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
Registry не создаёт Feed автоматически и не пытается использовать default source.
|
||
|
||
---
|
||
|
||
## 9. Сохранение identity Feed
|
||
|
||
Registry сохраняет и возвращает исходный объект Feed без:
|
||
|
||
```text
|
||
копирования;
|
||
оборачивания;
|
||
создания proxy;
|
||
создания нового Feed;
|
||
изменения объекта.
|
||
```
|
||
|
||
Если зарегистрирован:
|
||
|
||
```python
|
||
feed = StubInstrumentFeed()
|
||
|
||
registry.register("dzengi", feed)
|
||
```
|
||
|
||
то:
|
||
|
||
```python
|
||
registry.get("dzengi") is feed
|
||
```
|
||
|
||
равно:
|
||
|
||
```text
|
||
True
|
||
```
|
||
|
||
Это подтверждено unit-тестами.
|
||
|
||
---
|
||
|
||
## 10. Проверка InstrumentFeedProtocol
|
||
|
||
Перед регистрацией выполняется:
|
||
|
||
```python
|
||
isinstance(feed, InstrumentFeedProtocol)
|
||
```
|
||
|
||
Это возможно благодаря тому, что `InstrumentFeedProtocol` объявлен как runtime-checkable Protocol.
|
||
|
||
Архитектурная проверка:
|
||
|
||
```text
|
||
объект
|
||
↓
|
||
соответствует InstrumentFeedProtocol?
|
||
├── да → регистрация разрешена
|
||
└── нет → InstrumentFeedRegistryError
|
||
```
|
||
|
||
Registry не требует явного наследования от Protocol.
|
||
|
||
Используется structural typing:
|
||
|
||
```text
|
||
если объект реализует требуемый контракт,
|
||
он соответствует Protocol.
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Registry не вызывает Feed
|
||
|
||
При выполнении:
|
||
|
||
```python
|
||
registry.register("dzengi", feed)
|
||
```
|
||
|
||
не вызывается:
|
||
|
||
```python
|
||
feed.load_instruments()
|
||
```
|
||
|
||
При выполнении:
|
||
|
||
```python
|
||
registry.get("dzengi")
|
||
```
|
||
|
||
также не вызывается:
|
||
|
||
```python
|
||
feed.load_instruments()
|
||
```
|
||
|
||
Registry только хранит и возвращает Feed.
|
||
|
||
Цепочка:
|
||
|
||
```text
|
||
register()
|
||
↓
|
||
сохранить ссылку на Feed
|
||
|
||
get()
|
||
↓
|
||
вернуть ссылку на Feed
|
||
```
|
||
|
||
Отсутствует:
|
||
|
||
```text
|
||
load_instruments()
|
||
```
|
||
|
||
Это подтверждено отдельными unit-тестами.
|
||
|
||
---
|
||
|
||
## 12. Нормализация имени источника
|
||
|
||
Registry выполняет только:
|
||
|
||
```python
|
||
source_name.strip()
|
||
```
|
||
|
||
Пример:
|
||
|
||
```text
|
||
" dzengi "
|
||
↓
|
||
"dzengi"
|
||
```
|
||
|
||
Поэтому после:
|
||
|
||
```python
|
||
registry.register(" dzengi ", feed)
|
||
```
|
||
|
||
оба вызова:
|
||
|
||
```python
|
||
registry.get("dzengi")
|
||
registry.get(" dzengi ")
|
||
```
|
||
|
||
возвращают тот же Feed.
|
||
|
||
---
|
||
|
||
## 13. Что Registry не делает с именем источника
|
||
|
||
Registry намеренно не выполняет:
|
||
|
||
```text
|
||
lower()
|
||
casefold()
|
||
replace()
|
||
alias resolution
|
||
automatic source mapping
|
||
```
|
||
|
||
Поэтому:
|
||
|
||
```text
|
||
"dzengi"
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
"DZENGI"
|
||
```
|
||
|
||
являются разными ключами.
|
||
|
||
Можно одновременно зарегистрировать:
|
||
|
||
```python
|
||
registry.register("dzengi", first_feed)
|
||
registry.register("DZENGI", second_feed)
|
||
```
|
||
|
||
После этого:
|
||
|
||
```text
|
||
get("dzengi")
|
||
↓
|
||
first_feed
|
||
|
||
get("DZENGI")
|
||
↓
|
||
second_feed
|
||
```
|
||
|
||
Такое поведение исключает скрытую нормализацию без утверждённого контракта.
|
||
|
||
---
|
||
|
||
## 14. Запрет пустого имени источника
|
||
|
||
Registry отклоняет:
|
||
|
||
```text
|
||
""
|
||
" "
|
||
" "
|
||
"\t"
|
||
"\n"
|
||
```
|
||
|
||
После:
|
||
|
||
```python
|
||
source_name.strip()
|
||
```
|
||
|
||
такие значения становятся пустыми.
|
||
|
||
Выбрасывается:
|
||
|
||
```text
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
с сообщением:
|
||
|
||
```text
|
||
Имя источника Instrument Feed не должно быть пустым.
|
||
```
|
||
|
||
Проверка действует как для:
|
||
|
||
```text
|
||
register()
|
||
```
|
||
|
||
так и для:
|
||
|
||
```text
|
||
get()
|
||
```
|
||
|
||
---
|
||
|
||
## 15. Запрет повторной регистрации
|
||
|
||
Следующая последовательность запрещена:
|
||
|
||
```python
|
||
registry.register("dzengi", first_feed)
|
||
registry.register("dzengi", second_feed)
|
||
```
|
||
|
||
Вторая операция выбрасывает:
|
||
|
||
```text
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
с диагностикой:
|
||
|
||
```text
|
||
Instrument Feed для источника 'dzengi' уже зарегистрирован.
|
||
```
|
||
|
||
Registry не выполняет молчаливую замену.
|
||
|
||
---
|
||
|
||
## 16. Почему молчаливая замена запрещена
|
||
|
||
Молчаливая операция:
|
||
|
||
```text
|
||
existing Feed
|
||
↓
|
||
register same source name
|
||
↓
|
||
new Feed silently replaces old Feed
|
||
```
|
||
|
||
могла бы незаметно изменить production acquisition pipeline.
|
||
|
||
Поэтому используется fail-fast поведение:
|
||
|
||
```text
|
||
duplicate source name
|
||
↓
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
Если в будущем понадобится контролируемая замена Feed, она должна быть реализована отдельным явно определённым контрактом.
|
||
|
||
В Build 011 такой контракт не нужен.
|
||
|
||
---
|
||
|
||
## 17. Повторная регистрация после нормализации
|
||
|
||
Проверка duplicate выполняется после:
|
||
|
||
```python
|
||
source_name.strip()
|
||
```
|
||
|
||
Поэтому:
|
||
|
||
```python
|
||
registry.register("dzengi", first_feed)
|
||
registry.register(" dzengi ", second_feed)
|
||
```
|
||
|
||
считается повторной регистрацией одного и того же источника.
|
||
|
||
Вторая операция завершается:
|
||
|
||
```text
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
Исходный Feed при этом сохраняется.
|
||
|
||
---
|
||
|
||
## 18. Отсутствие молчаливой замены Feed
|
||
|
||
После:
|
||
|
||
```python
|
||
registry.register("dzengi", first_feed)
|
||
```
|
||
|
||
и неуспешной попытки:
|
||
|
||
```python
|
||
registry.register("dzengi", second_feed)
|
||
```
|
||
|
||
результат:
|
||
|
||
```python
|
||
registry.get("dzengi") is first_feed
|
||
```
|
||
|
||
остаётся:
|
||
|
||
```text
|
||
True
|
||
```
|
||
|
||
Таким образом, ошибочная повторная регистрация не изменяет состояние Registry.
|
||
|
||
---
|
||
|
||
## 19. Поведение для отсутствующего источника
|
||
|
||
Если выполнить:
|
||
|
||
```python
|
||
registry.get("dzengi")
|
||
```
|
||
|
||
до регистрации Feed, выбрасывается:
|
||
|
||
```text
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
с диагностикой:
|
||
|
||
```text
|
||
Instrument Feed для источника 'dzengi' не зарегистрирован.
|
||
```
|
||
|
||
Registry не:
|
||
|
||
```text
|
||
возвращает None;
|
||
создаёт Feed автоматически;
|
||
выбирает default Feed;
|
||
выполняет fallback на другой источник.
|
||
```
|
||
|
||
Отсутствие Feed является явной ошибкой Registry.
|
||
|
||
---
|
||
|
||
## 20. Новая ошибка InstrumentFeedRegistryError
|
||
|
||
В файле:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
добавлен класс:
|
||
|
||
```python
|
||
class InstrumentFeedRegistryError(MarketDataAcquisitionError):
|
||
pass
|
||
```
|
||
|
||
Иерархия acquisition-ошибок после Build 011:
|
||
|
||
```text
|
||
MarketDataAcquisitionError
|
||
├── InstrumentReferenceTransportError
|
||
├── InstrumentReferenceSchemaError
|
||
├── InstrumentReferenceParseError
|
||
├── InstrumentReferenceValueError
|
||
├── InstrumentReferenceMappingError
|
||
└── InstrumentFeedRegistryError
|
||
```
|
||
|
||
Ошибка используется для:
|
||
|
||
```text
|
||
пустого имени источника;
|
||
объекта, не соответствующего InstrumentFeedProtocol;
|
||
повторной регистрации;
|
||
запроса отсутствующего Feed.
|
||
```
|
||
|
||
---
|
||
|
||
## 21. Почему не созданы отдельные Registry exceptions
|
||
|
||
В Build 011 намеренно не добавлены:
|
||
|
||
```text
|
||
DuplicateInstrumentFeedError
|
||
InstrumentFeedNotFoundError
|
||
InvalidInstrumentFeedNameError
|
||
InvalidInstrumentFeedError
|
||
```
|
||
|
||
На текущем этапе для них нет отдельных алгоритмов обработки.
|
||
|
||
Все ошибки относятся к одной архитектурной категории:
|
||
|
||
```text
|
||
Instrument Feed Registry error
|
||
```
|
||
|
||
Поэтому используется один класс:
|
||
|
||
```text
|
||
InstrumentFeedRegistryError
|
||
```
|
||
|
||
Дополнительная детализация исключений без реальной необходимости только усложнила бы контракт.
|
||
|
||
---
|
||
|
||
## 22. Dependency Injection
|
||
|
||
Registry не создаёт Feed самостоятельно.
|
||
|
||
Отсутствует:
|
||
|
||
```python
|
||
self._feed = InstrumentFeed(...)
|
||
```
|
||
|
||
Также Registry не создаёт:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
DzengiInstrumentDocumentHandler
|
||
ExchangeRestClient
|
||
```
|
||
|
||
Правильная будущая композиция:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
+
|
||
DzengiInstrumentDocumentHandler
|
||
↓
|
||
InstrumentFeed
|
||
↓
|
||
registry.register("dzengi", feed)
|
||
```
|
||
|
||
Composition остаётся явной и находится за пределами Registry.
|
||
|
||
---
|
||
|
||
## 23. Почему Registry хранит Feed, а не Source и Handler отдельно
|
||
|
||
К моменту Build 011 уже существует архитектурная capability:
|
||
|
||
```text
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
Feed инкапсулирует взаимодействие:
|
||
|
||
```text
|
||
InstrumentDocumentSource
|
||
↓
|
||
InstrumentDocumentHandler
|
||
```
|
||
|
||
Если Registry начал бы отдельно хранить:
|
||
|
||
```text
|
||
Source
|
||
Handler
|
||
```
|
||
|
||
ему пришлось бы знать, как их соединять.
|
||
|
||
Это нарушило бы границу Build 010.
|
||
|
||
Правильная схема:
|
||
|
||
```text
|
||
Source
|
||
+
|
||
Handler
|
||
↓
|
||
InstrumentFeed
|
||
↓
|
||
InstrumentFeedRegistry
|
||
```
|
||
|
||
Registry работает только с готовым:
|
||
|
||
```text
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
---
|
||
|
||
## 24. Что Registry не хранит
|
||
|
||
Внутри Registry отсутствуют:
|
||
|
||
```text
|
||
Instrument;
|
||
tuple[Instrument, ...];
|
||
результат load_instruments();
|
||
последний успешный справочник;
|
||
предыдущий справочник;
|
||
snapshot;
|
||
timestamp;
|
||
TTL;
|
||
cache age;
|
||
последняя ошибка Feed;
|
||
legacy ExchangeSymbol.
|
||
```
|
||
|
||
Registry хранит только:
|
||
|
||
```text
|
||
dict[str, InstrumentFeedProtocol]
|
||
```
|
||
|
||
---
|
||
|
||
## 25. Что Registry не делает
|
||
|
||
Build 011 сознательно не выполняет:
|
||
|
||
```text
|
||
REST-запросы;
|
||
получение exchangeInfo;
|
||
schema validation;
|
||
parsing;
|
||
value validation;
|
||
mapping;
|
||
вызов load_instruments();
|
||
retry;
|
||
backoff;
|
||
кэширование;
|
||
хранение Instrument;
|
||
сравнение snapshot;
|
||
фильтрацию инструментов;
|
||
сортировку инструментов;
|
||
дедупликацию инструментов;
|
||
нормализацию торговых символов;
|
||
создание Source;
|
||
создание Handler;
|
||
создание Feed;
|
||
создание Acquisition Service;
|
||
создание compatibility mapper;
|
||
изменение ExchangeService;
|
||
изменение AutoTrade;
|
||
изменение Telegram UI;
|
||
изменение trading runtime.
|
||
```
|
||
|
||
Единственная ответственность:
|
||
|
||
```text
|
||
source_name
|
||
↔
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
---
|
||
|
||
## 26. Реализованные тестовые сценарии
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/test_registry.py
|
||
```
|
||
|
||
Фактически выполнено:
|
||
|
||
```text
|
||
25 tests
|
||
```
|
||
|
||
Проверены следующие сценарии:
|
||
|
||
1. регистрация и получение Feed;
|
||
2. сохранение identity Feed;
|
||
3. соответствие объекта `InstrumentFeedProtocol`;
|
||
4. несколько Feed под разными именами;
|
||
5. удаление внешних пробелов из имени;
|
||
6. запрет пустой строки при регистрации;
|
||
7. запрет строки из пробелов при регистрации;
|
||
8. запрет tab/newline при регистрации;
|
||
9. запрет пустой строки при `get()`;
|
||
10. запрет строки из пробелов при `get()`;
|
||
11. запрет tab/newline при `get()`;
|
||
12. запрет повторной регистрации;
|
||
13. отсутствие замены исходного Feed при duplicate;
|
||
14. duplicate после нормализации внешних пробелов;
|
||
15. сохранение case-sensitive поведения;
|
||
16. ошибка при запросе отсутствующего Feed;
|
||
17. запрет объекта без `InstrumentFeedProtocol`;
|
||
18. отсутствие вызова Feed при регистрации;
|
||
19. отсутствие вызова Feed при получении;
|
||
20. хранение Feed, а не результата `Instrument`;
|
||
21. наследование `InstrumentFeedRegistryError` от `MarketDataAcquisitionError`.
|
||
|
||
Часть сценариев реализована через параметризацию, поэтому фактическое количество выполненных тестовых случаев составляет:
|
||
|
||
```text
|
||
25
|
||
```
|
||
|
||
---
|
||
|
||
## 27. Проверка регистрации и получения Feed
|
||
|
||
Тест подтверждает:
|
||
|
||
```python
|
||
registry = InstrumentFeedRegistry()
|
||
feed = StubInstrumentFeed()
|
||
|
||
registry.register("dzengi", feed)
|
||
|
||
result = registry.get("dzengi")
|
||
|
||
assert result is feed
|
||
```
|
||
|
||
Это доказывает:
|
||
|
||
```text
|
||
Feed зарегистрирован;
|
||
Feed доступен по source_name;
|
||
identity объекта сохранена.
|
||
```
|
||
|
||
---
|
||
|
||
## 28. Проверка нескольких источников
|
||
|
||
Registry поддерживает несколько независимых записей:
|
||
|
||
```text
|
||
"dzengi"
|
||
↓
|
||
dzengi_feed
|
||
|
||
"secondary"
|
||
↓
|
||
secondary_feed
|
||
```
|
||
|
||
Тест подтверждает:
|
||
|
||
```python
|
||
assert registry.get("dzengi") is dzengi_feed
|
||
assert registry.get("secondary") is secondary_feed
|
||
```
|
||
|
||
Registry не имеет встроенного ограничения на один источник.
|
||
|
||
---
|
||
|
||
## 29. Проверка отсутствия вызова Feed
|
||
|
||
Тестовый Feed содержит счётчик:
|
||
|
||
```python
|
||
self.load_call_count = 0
|
||
```
|
||
|
||
Метод:
|
||
|
||
```python
|
||
load_instruments()
|
||
```
|
||
|
||
увеличивает счётчик.
|
||
|
||
После:
|
||
|
||
```python
|
||
registry.register("dzengi", feed)
|
||
```
|
||
|
||
подтверждено:
|
||
|
||
```python
|
||
assert feed.load_call_count == 0
|
||
```
|
||
|
||
После:
|
||
|
||
```python
|
||
registry.get("dzengi")
|
||
```
|
||
|
||
также подтверждено:
|
||
|
||
```python
|
||
assert feed.load_call_count == 0
|
||
```
|
||
|
||
Таким образом, Registry не запускает acquisition pipeline.
|
||
|
||
---
|
||
|
||
## 30. Проверка хранения Feed, а не результата Feed
|
||
|
||
Тест создаёт:
|
||
|
||
```python
|
||
instruments = (
|
||
_instrument(),
|
||
)
|
||
```
|
||
|
||
и Feed:
|
||
|
||
```python
|
||
feed = StubInstrumentFeed(
|
||
instruments=instruments,
|
||
)
|
||
```
|
||
|
||
После регистрации:
|
||
|
||
```python
|
||
registered_feed = registry.get("dzengi")
|
||
```
|
||
|
||
подтверждено:
|
||
|
||
```python
|
||
assert registered_feed is feed
|
||
assert registered_feed is not instruments
|
||
```
|
||
|
||
Registry хранит сам Feed, а не:
|
||
|
||
```text
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
---
|
||
|
||
## 31. Выполненные проверки
|
||
|
||
### Проверка 1 — unit-тесты Registry
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/market_data/acquisition/test_registry.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
......................... [100%]
|
||
25 passed in 0.02s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 2 — Python compilation
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/market_data/acquisition/exceptions.py \
|
||
src/market_data/acquisition/registry.py \
|
||
tests/unit/market_data/acquisition/test_registry.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
Команда завершилась без ошибок и без вывода.
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 3 — полный набор тестов проекта
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
....................................................................................................................................................................... [100%]
|
||
167 passed in 0.09s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 4 — отсутствие преждевременной production-интеграции
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "InstrumentFeedRegistry|InstrumentFeedRegistryError" \
|
||
src tests
|
||
```
|
||
|
||
Полученные production-использования находятся только в:
|
||
|
||
```text
|
||
src/market_data/acquisition/registry.py
|
||
src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
Остальные использования находятся исключительно в:
|
||
|
||
```text
|
||
tests/unit/market_data/acquisition/test_registry.py
|
||
```
|
||
|
||
Не обнаружено подключения к:
|
||
|
||
```text
|
||
Acquisition Service
|
||
ExchangeService
|
||
Telegram UI
|
||
AutoTrade
|
||
Trading runtime
|
||
другим production-потребителям
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
## 32. Архитектура после Build 011
|
||
|
||
После завершения Build 011 новая часть подсистемы имеет следующую структуру:
|
||
|
||
```text
|
||
market_data/
|
||
└── acquisition/
|
||
├── exceptions.py
|
||
│ ├── MarketDataAcquisitionError
|
||
│ ├── InstrumentReferenceTransportError
|
||
│ ├── InstrumentReferenceSchemaError
|
||
│ ├── InstrumentReferenceParseError
|
||
│ ├── InstrumentReferenceValueError
|
||
│ ├── InstrumentReferenceMappingError
|
||
│ └── InstrumentFeedRegistryError
|
||
│
|
||
├── protocol.py
|
||
│ ├── InstrumentDocumentSource
|
||
│ ├── InstrumentDocumentHandler
|
||
│ └── InstrumentFeedProtocol
|
||
│
|
||
├── registry.py
|
||
│ └── InstrumentFeedRegistry
|
||
│
|
||
├── 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
|
||
service.py
|
||
```
|
||
|
||
---
|
||
|
||
## 33. Полная архитектурная цепочка после Build 011
|
||
|
||
После Build 011 существуют две связанные архитектурные части.
|
||
|
||
### Регистрация capability
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
+
|
||
DzengiInstrumentDocumentHandler
|
||
↓
|
||
InstrumentFeed
|
||
↓
|
||
InstrumentFeedRegistry.register(
|
||
"dzengi",
|
||
feed,
|
||
)
|
||
```
|
||
|
||
### Получение capability будущим сервисом
|
||
|
||
```text
|
||
Acquisition Service
|
||
↓
|
||
source_name = "dzengi"
|
||
↓
|
||
InstrumentFeedRegistry.get("dzengi")
|
||
↓
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
После получения Feed будущий `Acquisition Service` сможет выполнить:
|
||
|
||
```text
|
||
InstrumentFeedProtocol.load_instruments()
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Эта orchestration logic относится к следующему:
|
||
|
||
```text
|
||
Build 012 — Acquisition Service
|
||
```
|
||
|
||
---
|
||
|
||
## 34. Полный acquisition pipeline после Build 011
|
||
|
||
Технически уже реализована следующая цепочка:
|
||
|
||
```text
|
||
source_name
|
||
↓
|
||
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, ...]
|
||
```
|
||
|
||
Однако Registry сам эту цепочку не запускает.
|
||
|
||
---
|
||
|
||
## 35. Что ещё не реализовано
|
||
|
||
После Build 011 отсутствуют:
|
||
|
||
```text
|
||
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();
|
||
перенос кэша.
|
||
```
|
||
|
||
Поэтому новая acquisition pipeline всё ещё остаётся изолированной от существующего production runtime.
|
||
|
||
---
|
||
|
||
## 36. Влияние на legacy-систему
|
||
|
||
Build 011 не подключён к существующим компонентам:
|
||
|
||
```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-путь продолжает работать без изменений.
|
||
|
||
---
|
||
|
||
## 37. Обратная совместимость
|
||
|
||
Подтверждено сохранение:
|
||
|
||
```text
|
||
сигнатур существующих legacy-методов;
|
||
старых импортов;
|
||
существующего формата legacy-ошибок;
|
||
Telegram UI;
|
||
автоторговли;
|
||
runtime-поведения;
|
||
legacy ExchangeSymbol;
|
||
legacy-кэша.
|
||
```
|
||
|
||
Build 011 имеет полную обратную совместимость.
|
||
|
||
---
|
||
|
||
## 38. Классификация изменений
|
||
|
||
| Изменение | Классификация |
|
||
|---|---|
|
||
| `InstrumentFeedRegistry` | Обязательное архитектурное изменение |
|
||
| Индексация Feed по `source_name` | Обязательное архитектурное изменение |
|
||
| Проверка `InstrumentFeedProtocol` | Улучшение надёжности |
|
||
| Запрет повторной регистрации | Улучшение надёжности |
|
||
| `InstrumentFeedRegistryError` | Обязательная диагностическая граница |
|
||
| Удаление внешних пробелов из имени | Улучшение надёжности |
|
||
| Сохранение case-sensitive ключей | Отсутствие скрытого изменения поведения |
|
||
| Хранение `Instrument` | Не выполняется |
|
||
| Вызов Feed | Не выполняется |
|
||
| Кэширование | Не выполняется |
|
||
| Изменение legacy-кода | Отсутствует |
|
||
| Изменение production-поведения | Отсутствует |
|
||
|
||
---
|
||
|
||
## 39. Условие завершения Build 011
|
||
|
||
Все условия выполнены:
|
||
|
||
```text
|
||
InstrumentFeedRegistry
|
||
реализован;
|
||
|
||
InstrumentFeedProtocol
|
||
может быть зарегистрирован;
|
||
|
||
Feed
|
||
возвращается по source_name;
|
||
|
||
identity Feed
|
||
сохраняется;
|
||
|
||
несколько источников
|
||
поддерживаются;
|
||
|
||
внешние пробелы имени
|
||
удаляются;
|
||
|
||
регистр имени
|
||
сохраняется;
|
||
|
||
пустые ключи
|
||
отклоняются;
|
||
|
||
повторная регистрация
|
||
отклоняется;
|
||
|
||
неуспешная повторная регистрация
|
||
не заменяет исходный Feed;
|
||
|
||
отсутствующий Feed
|
||
вызывает InstrumentFeedRegistryError;
|
||
|
||
невалидный Feed
|
||
отклоняется;
|
||
|
||
Registry
|
||
не вызывает load_instruments();
|
||
|
||
Registry
|
||
не хранит Instrument;
|
||
|
||
Registry
|
||
не выполняет кэширование;
|
||
|
||
unit-тесты
|
||
проходят;
|
||
|
||
полный pytest
|
||
проходит;
|
||
|
||
production runtime
|
||
не затронут.
|
||
```
|
||
|
||
---
|
||
|
||
## 40. Итог Build 011
|
||
|
||
Build 011 завершён успешно.
|
||
|
||
Реализовано:
|
||
|
||
```text
|
||
InstrumentFeedRegistry
|
||
|
||
source_name
|
||
↓
|
||
register()
|
||
↓
|
||
InstrumentFeedProtocol
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
source_name
|
||
↓
|
||
get()
|
||
↓
|
||
тот же InstrumentFeedProtocol
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
```text
|
||
25 Registry tests passed
|
||
167 total project tests passed
|
||
Python compilation passed
|
||
No premature production integration detected
|
||
Legacy bot behavior unchanged
|
||
```
|
||
|
||
Итоговый статус:
|
||
|
||
```text
|
||
BUILD 011 — COMPLETE
|
||
```
|
||
|
||
---
|
||
|
||
## 41. Следующий этап
|
||
|
||
Следующий этап утверждённого плана:
|
||
|
||
```text
|
||
Build 012 — Acquisition Service
|
||
```
|
||
|
||
Его задача — реализовать application-level orchestration над Registry и Feed:
|
||
|
||
```text
|
||
source_name
|
||
↓
|
||
Acquisition Service
|
||
↓
|
||
InstrumentFeedRegistry
|
||
↓
|
||
InstrumentFeedProtocol
|
||
↓
|
||
load_instruments()
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Build 012 не должен:
|
||
|
||
```text
|
||
изменять ExchangeService;
|
||
переключать legacy get_exchange_symbols();
|
||
создавать compatibility mapper;
|
||
переносить legacy-кэш;
|
||
подключаться к Telegram UI;
|
||
изменять AutoTrade;
|
||
изменять trading runtime.
|
||
```
|
||
|
||
После успешного завершения Build 012 будет закончена изолированная новая acquisition pipeline, после чего можно будет перейти к:
|
||
|
||
```text
|
||
Build 013 — проверка эквивалентности старой и новой реализации.
|
||
``` |