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