863 lines
23 KiB
Markdown
863 lines
23 KiB
Markdown
# Build 020 — Перенос кэша инструментов в Storage Layer
|
||
|
||
**Engineering Migration Record**
|
||
|
||
---
|
||
|
||
## Контроль документа
|
||
|
||
| Свойство | Значение |
|
||
|---|---|
|
||
| Документ | Build 020 — Перенос кэша инструментов в Storage Layer |
|
||
| Тип документа | Engineering Migration Record |
|
||
| Статус | **Complete** |
|
||
| Проект | Dzentra |
|
||
| Подсистема | Instrument Reference Data |
|
||
| Build | 020 |
|
||
| Язык | Русский |
|
||
| Целевой файл | `docs/migrations/instrument_reference_data/build_020.md` |
|
||
|
||
---
|
||
|
||
## 1. Цель Build 020
|
||
|
||
Цель Build 020 — удалить каноническое хранение справочных данных инструментов из legacy-кэша `ExchangeService._exchange_symbols_cache` и перенести его в специализированный Storage Layer.
|
||
|
||
До выполнения Build 020 `ExchangeService` одновременно отвечал за:
|
||
|
||
- получение справочных данных инструментов;
|
||
- запуск acquisition pipeline;
|
||
- преобразование канонических моделей `Instrument` в legacy-модели `ExchangeSymbol`;
|
||
- хранение результата в собственном class-level cache.
|
||
|
||
Это создавало архитектурную зависимость канонических справочных данных от legacy exchange layer.
|
||
|
||
После Build 020 канонические данные инструментов хранятся в специализированном:
|
||
|
||
```text
|
||
InMemoryInstrumentStore
|
||
```
|
||
|
||
в виде:
|
||
|
||
```text
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Legacy-модели `ExchangeSymbol` больше не являются каноническим представлением справочных данных.
|
||
|
||
---
|
||
|
||
## 2. Архитектурное состояние до Build 020
|
||
|
||
До миграции `ExchangeService` содержал:
|
||
|
||
```python
|
||
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
|
||
```
|
||
|
||
Метод:
|
||
|
||
```python
|
||
get_exchange_symbols()
|
||
```
|
||
|
||
работал по следующей схеме:
|
||
|
||
```text
|
||
get_exchange_symbols()
|
||
↓
|
||
_exchange_symbols_cache
|
||
↓ cache miss
|
||
_load_exchange_symbols_via_acquisition()
|
||
↓
|
||
Instrument Acquisition Pipeline
|
||
↓
|
||
tuple[Instrument, ...]
|
||
↓
|
||
map_instruments_to_exchange_symbols()
|
||
↓
|
||
list[ExchangeSymbol]
|
||
↓
|
||
_exchange_symbols_cache
|
||
```
|
||
|
||
Таким образом, результат новой Instrument Reference Data pipeline сразу преобразовывался в legacy-модель, и именно legacy-представление сохранялось как основной кэш.
|
||
|
||
---
|
||
|
||
## 3. Архитектурная проблема
|
||
|
||
Старый подход имел несколько принципиальных недостатков.
|
||
|
||
### 3.1. Legacy-модель использовалась как каноническое хранилище
|
||
|
||
Каноническая модель:
|
||
|
||
```python
|
||
Instrument
|
||
```
|
||
|
||
содержит полные справочные данные инструмента.
|
||
|
||
Legacy-модель:
|
||
|
||
```python
|
||
ExchangeSymbol
|
||
```
|
||
|
||
является сокращённой compatibility-проекцией и содержит только часть этих данных.
|
||
|
||
Хранение только `ExchangeSymbol` означало потерю полноценного канонического представления после завершения acquisition pipeline.
|
||
|
||
### 3.2. Exchange Layer владел состоянием справочных данных
|
||
|
||
Поле:
|
||
|
||
```python
|
||
ExchangeService._exchange_symbols_cache
|
||
```
|
||
|
||
делало `ExchangeService` владельцем справочных данных инструментов.
|
||
|
||
Это противоречило целевой архитектуре:
|
||
|
||
```text
|
||
Acquisition Layer
|
||
↓
|
||
Canonical Instrument Models
|
||
↓
|
||
Storage Layer
|
||
↓
|
||
Consumers / Compatibility Projections
|
||
```
|
||
|
||
### 3.3. Канонические данные и legacy-проекция были объединены
|
||
|
||
Результат acquisition pipeline немедленно преобразовывался:
|
||
|
||
```text
|
||
Instrument
|
||
↓
|
||
ExchangeSymbol
|
||
```
|
||
|
||
и сохранялся только после преобразования.
|
||
|
||
Это не позволяло независимо использовать полный `Instrument` в будущих подсистемах Dzentra.
|
||
|
||
---
|
||
|
||
## 4. Реализованное архитектурное решение
|
||
|
||
В Build 020 введено разделение между:
|
||
|
||
1. каноническим хранилищем инструментов;
|
||
2. legacy compatibility projection cache.
|
||
|
||
Теперь `ExchangeService` использует:
|
||
|
||
```python
|
||
_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore()
|
||
```
|
||
|
||
для хранения канонических моделей:
|
||
|
||
```python
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
и отдельный:
|
||
|
||
```python
|
||
_exchange_symbols_projection_cache: list[ExchangeSymbol] | None = None
|
||
```
|
||
|
||
для временного кэширования legacy-проекции.
|
||
|
||
Итоговая схема:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
↓
|
||
InstrumentFeed
|
||
↓
|
||
DzengiInstrumentDocumentHandler
|
||
↓
|
||
InstrumentAcquisitionService
|
||
↓
|
||
tuple[Instrument, ...]
|
||
↓
|
||
InMemoryInstrumentStore
|
||
↓
|
||
Canonical Instrument Reference Data
|
||
↓
|
||
map_instruments_to_exchange_symbols()
|
||
↓
|
||
list[ExchangeSymbol]
|
||
↓
|
||
Legacy Compatibility Projection Cache
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Каноническое хранилище
|
||
|
||
Канонические справочные данные теперь находятся в:
|
||
|
||
```python
|
||
ExchangeService._instrument_store
|
||
```
|
||
|
||
Тип зависимости:
|
||
|
||
```python
|
||
InstrumentStoreProtocol
|
||
```
|
||
|
||
Текущая реализация:
|
||
|
||
```python
|
||
InMemoryInstrumentStore
|
||
```
|
||
|
||
Store хранит:
|
||
|
||
```python
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Ключ источника:
|
||
|
||
```text
|
||
dzengi
|
||
```
|
||
|
||
Таким образом, канонические справочные данные больше не зависят от legacy-модели `ExchangeSymbol`.
|
||
|
||
---
|
||
|
||
## 6. Новый production flow
|
||
|
||
Метод:
|
||
|
||
```python
|
||
get_exchange_symbols()
|
||
```
|
||
|
||
сохраняет прежний внешний контракт:
|
||
|
||
```python
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
Это необходимо для сохранения работоспособности существующего бота во время поэтапной миграции.
|
||
|
||
Внутренний flow теперь выглядит следующим образом:
|
||
|
||
```text
|
||
get_exchange_symbols()
|
||
↓
|
||
Проверка exchange_enabled
|
||
↓
|
||
Проверка _exchange_symbols_projection_cache
|
||
↓ cache miss
|
||
Чтение InstrumentStore
|
||
↓ store miss
|
||
_load_instruments_via_acquisition()
|
||
↓
|
||
tuple[Instrument, ...]
|
||
↓
|
||
Сохранение в InstrumentStore
|
||
↓
|
||
map_instruments_to_exchange_symbols()
|
||
↓
|
||
list[ExchangeSymbol]
|
||
↓
|
||
Сохранение в _exchange_symbols_projection_cache
|
||
↓
|
||
Возврат legacy-результата
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Разделение канонического кэша и compatibility projection cache
|
||
|
||
После Build 020 существуют два разных уровня состояния.
|
||
|
||
### 7.1. Каноническое состояние
|
||
|
||
```python
|
||
_instrument_store
|
||
```
|
||
|
||
Хранит:
|
||
|
||
```python
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Назначение:
|
||
|
||
- хранение полной справочной модели инструмента;
|
||
- повторное использование канонических данных;
|
||
- основа для будущих market intelligence consumers;
|
||
- независимость от legacy exchange models.
|
||
|
||
### 7.2. Compatibility projection cache
|
||
|
||
```python
|
||
_exchange_symbols_projection_cache
|
||
```
|
||
|
||
Хранит:
|
||
|
||
```python
|
||
list[ExchangeSymbol] | None
|
||
```
|
||
|
||
Назначение:
|
||
|
||
- сохранить старый контракт `get_exchange_symbols()`;
|
||
- не выполнять повторный compatibility mapping при каждом вызове;
|
||
- обеспечить безопасную постепенную миграцию legacy-кода.
|
||
|
||
`_exchange_symbols_projection_cache` не является источником истины.
|
||
|
||
Источником истины является:
|
||
|
||
```text
|
||
InstrumentStore
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Изменение acquisition loader
|
||
|
||
Старый метод:
|
||
|
||
```python
|
||
_load_exchange_symbols_via_acquisition()
|
||
```
|
||
|
||
удалён.
|
||
|
||
Он одновременно:
|
||
|
||
- запускал acquisition pipeline;
|
||
- получал `Instrument`;
|
||
- выполнял compatibility mapping;
|
||
- возвращал `ExchangeSymbol`.
|
||
|
||
Вместо него используется:
|
||
|
||
```python
|
||
_load_instruments_via_acquisition()
|
||
```
|
||
|
||
Новый метод возвращает:
|
||
|
||
```python
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
и не выполняет:
|
||
|
||
```python
|
||
map_instruments_to_exchange_symbols()
|
||
```
|
||
|
||
Это обеспечивает чистую архитектурную границу:
|
||
|
||
```text
|
||
Acquisition Pipeline
|
||
↓
|
||
Canonical Instrument Models
|
||
```
|
||
|
||
Compatibility mapping выполняется отдельно только там, где действительно требуется legacy-контракт.
|
||
|
||
---
|
||
|
||
## 9. Сохранение обратной совместимости
|
||
|
||
Build 020 не меняет публичный контракт:
|
||
|
||
```python
|
||
ExchangeService.get_exchange_symbols()
|
||
```
|
||
|
||
Он по-прежнему возвращает:
|
||
|
||
```python
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
Благодаря этому продолжают работать существующие consumers, включая:
|
||
|
||
```text
|
||
validate_symbol()
|
||
currency_ui.py
|
||
legacy exchange UI
|
||
существующие unit tests
|
||
```
|
||
|
||
Миграция выполнена без обязательного одновременного переписывания всех legacy consumers.
|
||
|
||
---
|
||
|
||
## 10. Поведение при отключённой бирже
|
||
|
||
При:
|
||
|
||
```python
|
||
exchange_enabled = False
|
||
```
|
||
|
||
метод:
|
||
|
||
```python
|
||
get_exchange_symbols()
|
||
```
|
||
|
||
возвращает:
|
||
|
||
```python
|
||
[]
|
||
```
|
||
|
||
При этом он не должен:
|
||
|
||
- читать `InstrumentStore`;
|
||
- использовать существующий projection cache;
|
||
- запускать acquisition pipeline;
|
||
- обращаться к реальной бирже.
|
||
|
||
Это сохраняет прежнее поведение mock/disabled режима.
|
||
|
||
---
|
||
|
||
## 11. Поведение при cache hit
|
||
|
||
Если существует:
|
||
|
||
```python
|
||
_exchange_symbols_projection_cache
|
||
```
|
||
|
||
метод возвращает существующий объект legacy-проекции без:
|
||
|
||
- чтения acquisition source;
|
||
- повторной обработки документа;
|
||
- повторного compatibility mapping.
|
||
|
||
Если projection cache отсутствует, но канонические данные уже существуют в:
|
||
|
||
```python
|
||
InstrumentStore
|
||
```
|
||
|
||
то acquisition pipeline не запускается повторно.
|
||
|
||
Вместо этого выполняется:
|
||
|
||
```text
|
||
InstrumentStore
|
||
↓
|
||
tuple[Instrument, ...]
|
||
↓
|
||
Compatibility Mapper
|
||
↓
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
Пустой канонический набор:
|
||
|
||
```python
|
||
()
|
||
```
|
||
|
||
также считается валидным cache hit и не должен ошибочно интерпретироваться как отсутствие данных.
|
||
|
||
---
|
||
|
||
## 12. Поведение при cache miss
|
||
|
||
Если одновременно отсутствуют:
|
||
|
||
```text
|
||
_exchange_symbols_projection_cache
|
||
InstrumentStore entry for "dzengi"
|
||
```
|
||
|
||
выполняется полный production flow:
|
||
|
||
```text
|
||
Instrument Source
|
||
↓
|
||
Instrument Handler
|
||
↓
|
||
Instrument Acquisition Service
|
||
↓
|
||
tuple[Instrument, ...]
|
||
↓
|
||
InstrumentStore.set(...)
|
||
↓
|
||
Compatibility Mapper
|
||
↓
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
После успешной загрузки:
|
||
|
||
- канонический `tuple[Instrument, ...]` сохраняется в `InstrumentStore`;
|
||
- legacy-проекция создаётся отдельно;
|
||
- legacy-проекция сохраняется в `_exchange_symbols_projection_cache`.
|
||
|
||
---
|
||
|
||
## 13. Поведение при ошибках
|
||
|
||
Если acquisition pipeline завершается ошибкой:
|
||
|
||
- ошибка логируется как exchange request error;
|
||
- endpoint сохраняется как:
|
||
|
||
```text
|
||
exchangeInfo
|
||
```
|
||
|
||
- исходная ошибка становится причиной `ExchangeError`;
|
||
- канонический store не должен получать частичные или некорректные данные;
|
||
- projection cache не должен заполняться.
|
||
|
||
Таким образом, ошибка acquisition не создаёт ложное успешное состояние.
|
||
|
||
Если ошибка возникает на этапе compatibility mapping:
|
||
|
||
- канонические данные уже могут находиться в `InstrumentStore`;
|
||
- projection cache не должен заполняться некорректным результатом;
|
||
- следующий вызов может повторно построить legacy-проекцию из сохранённых канонических данных без повторного запуска acquisition pipeline.
|
||
|
||
---
|
||
|
||
## 14. Инварианты Build 020
|
||
|
||
После завершения Build 020 действуют следующие обязательные инварианты.
|
||
|
||
1. `ExchangeService._exchange_symbols_cache` отсутствует в production-коде.
|
||
|
||
2. Канонические справочные данные хранятся как:
|
||
|
||
```python
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
3. Владельцем канонического состояния является:
|
||
|
||
```text
|
||
InstrumentStore
|
||
```
|
||
|
||
4. Текущей реализацией store является:
|
||
|
||
```python
|
||
InMemoryInstrumentStore
|
||
```
|
||
|
||
5. `ExchangeService` зависит от абстракции:
|
||
|
||
```python
|
||
InstrumentStoreProtocol
|
||
```
|
||
|
||
6. Старый loader:
|
||
|
||
```python
|
||
_load_exchange_symbols_via_acquisition()
|
||
```
|
||
|
||
отсутствует.
|
||
|
||
7. Новый loader:
|
||
|
||
```python
|
||
_load_instruments_via_acquisition()
|
||
```
|
||
|
||
возвращает только:
|
||
|
||
```python
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
8. Новый loader не вызывает:
|
||
|
||
```python
|
||
map_instruments_to_exchange_symbols()
|
||
```
|
||
|
||
9. Compatibility mapping выполняется после получения канонических моделей из store или acquisition pipeline.
|
||
|
||
10. `_exchange_symbols_projection_cache` не является каноническим источником данных.
|
||
|
||
11. Публичный контракт:
|
||
|
||
```python
|
||
get_exchange_symbols() -> list[ExchangeSymbol]
|
||
```
|
||
|
||
сохранён для обратной совместимости.
|
||
|
||
12. `validate_symbol()` продолжает работать через `get_exchange_symbols()`.
|
||
|
||
13. При отключённой бирже store, projection cache и acquisition pipeline не используются.
|
||
|
||
14. Ошибка acquisition не заполняет канонический store.
|
||
|
||
15. Ошибка acquisition не заполняет projection cache.
|
||
|
||
16. Пустой `tuple[Instrument, ...]` является валидным сохранённым значением и должен отличаться от отсутствия записи в store.
|
||
|
||
---
|
||
|
||
## 15. Изменённые production-файлы
|
||
|
||
Основные изменения Build 020 выполнены в:
|
||
|
||
```text
|
||
app/src/integrations/exchange/service.py
|
||
```
|
||
|
||
Используются ранее подготовленные компоненты Storage Layer:
|
||
|
||
```text
|
||
app/src/storage/instrument_store.py
|
||
app/src/storage/exceptions.py
|
||
```
|
||
|
||
Используется compatibility mapper:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/compatibility.py
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Тестовое покрытие
|
||
|
||
Основные проверки миграции находятся в:
|
||
|
||
```text
|
||
app/tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||
```
|
||
|
||
Дополнительно проверена совместимость:
|
||
|
||
```text
|
||
app/tests/unit/integrations/exchange/test_service_validate_symbol.py
|
||
```
|
||
|
||
И отдельно сохраняется тестовое покрытие самого store:
|
||
|
||
```text
|
||
app/tests/unit/storage/test_instrument_store.py
|
||
```
|
||
|
||
Проверены следующие сценарии:
|
||
|
||
- отключённая биржа возвращает пустой список;
|
||
- отключённая биржа не читает существующий `InstrumentStore`;
|
||
- cache hit legacy-проекции не запускает acquisition pipeline;
|
||
- store hit не запускает acquisition pipeline;
|
||
- store miss запускает acquisition pipeline;
|
||
- загруженные `Instrument` сохраняются в store;
|
||
- пустой tuple корректно обрабатывается как cache hit;
|
||
- compatibility mapping получает именно канонические `Instrument`;
|
||
- legacy-проекция сохраняет прежний тип `list[ExchangeSymbol]`;
|
||
- сохраняется порядок инструментов;
|
||
- acquisition error оборачивается в `ExchangeError`;
|
||
- acquisition error логируется с endpoint `exchangeInfo`;
|
||
- acquisition error не заполняет store;
|
||
- acquisition error не заполняет projection cache;
|
||
- acquisition loader использует реальную processing pipeline;
|
||
- acquisition loader использует registry key `dzengi`;
|
||
- acquisition loader возвращает `tuple[Instrument, ...]`;
|
||
- acquisition loader не вызывает compatibility mapper;
|
||
- `validate_symbol()` сохраняет прежнее поведение.
|
||
|
||
---
|
||
|
||
## 17. Результаты проверок
|
||
|
||
Целевая проверка `get_exchange_symbols()`:
|
||
|
||
```text
|
||
23 passed in 0.12s
|
||
```
|
||
|
||
Целевая проверка `validate_symbol()`:
|
||
|
||
```text
|
||
16 passed in 0.07s
|
||
```
|
||
|
||
Проверка синтаксической компиляции:
|
||
|
||
```text
|
||
py_compile — успешно
|
||
```
|
||
|
||
Полный regression suite:
|
||
|
||
```text
|
||
426 passed in 0.23s
|
||
```
|
||
|
||
Финальная архитектурная проверка подтвердила:
|
||
|
||
```text
|
||
старый _exchange_symbols_cache отсутствует в production-коде;
|
||
старый _load_exchange_symbols_via_acquisition отсутствует;
|
||
канонический store подключён через InstrumentStoreProtocol;
|
||
текущая реализация store — InMemoryInstrumentStore;
|
||
legacy projection cache отделён от канонического store;
|
||
compatibility mapper не вызывается внутри acquisition loader;
|
||
новый acquisition loader возвращает канонические Instrument.
|
||
```
|
||
|
||
---
|
||
|
||
## 18. Архитектурный результат
|
||
|
||
До Build 020:
|
||
|
||
```text
|
||
Exchange API
|
||
↓
|
||
Acquisition Pipeline
|
||
↓
|
||
Instrument
|
||
↓
|
||
Compatibility Mapper
|
||
↓
|
||
ExchangeSymbol
|
||
↓
|
||
ExchangeService class-level cache
|
||
```
|
||
|
||
После Build 020:
|
||
|
||
```text
|
||
Exchange API
|
||
↓
|
||
Acquisition Pipeline
|
||
↓
|
||
Instrument
|
||
↓
|
||
InstrumentStore
|
||
↓
|
||
Canonical Instrument Reference Data
|
||
↓
|
||
Compatibility Mapper
|
||
↓
|
||
ExchangeSymbol
|
||
↓
|
||
Temporary Legacy Projection Cache
|
||
```
|
||
|
||
Ключевое изменение:
|
||
|
||
```text
|
||
ExchangeSymbol больше не является канонически сохраняемой моделью Instrument Reference Data.
|
||
```
|
||
|
||
Каноническим представлением теперь является:
|
||
|
||
```python
|
||
Instrument
|
||
```
|
||
|
||
а владельцем его состояния является:
|
||
|
||
```text
|
||
Storage Layer
|
||
```
|
||
|
||
---
|
||
|
||
## 19. Ограничения текущего этапа
|
||
|
||
Build 020 намеренно не выполняет полную миграцию всех consumers на каноническую модель `Instrument`.
|
||
|
||
На текущем этапе сохраняются:
|
||
|
||
```python
|
||
get_exchange_symbols() -> list[ExchangeSymbol]
|
||
```
|
||
|
||
и:
|
||
|
||
```python
|
||
_exchange_symbols_projection_cache
|
||
```
|
||
|
||
Они являются compatibility-механизмами переходного периода.
|
||
|
||
Также Build 020 не переносит в новый `InstrumentStore`:
|
||
|
||
- market price cache;
|
||
- execution price cache;
|
||
- balance snapshots;
|
||
- journal storage;
|
||
- другие runtime caches.
|
||
|
||
Build 020 касается исключительно хранения канонических Instrument Reference Data.
|
||
|
||
---
|
||
|
||
## 20. Критерии завершения
|
||
|
||
Build 020 считается завершённым, если одновременно выполняются следующие условия:
|
||
|
||
- [x] созданный ранее `InstrumentStore` используется production-кодом;
|
||
- [x] канонические данные хранятся как `tuple[Instrument, ...]`;
|
||
- [x] старый `_exchange_symbols_cache` удалён;
|
||
- [x] старый `_load_exchange_symbols_via_acquisition()` удалён;
|
||
- [x] новый `_load_instruments_via_acquisition()` возвращает канонические модели;
|
||
- [x] acquisition loader не выполняет compatibility mapping;
|
||
- [x] legacy API `get_exchange_symbols()` сохранён;
|
||
- [x] `validate_symbol()` сохраняет прежнее поведение;
|
||
- [x] ошибки acquisition не создают ложное состояние кэша;
|
||
- [x] целевые тесты проходят;
|
||
- [x] `py_compile` проходит;
|
||
- [x] полный regression suite проходит;
|
||
- [x] финальная архитектурная grep-проверка выполнена.
|
||
|
||
---
|
||
|
||
## 21. Итоговый статус
|
||
|
||
```text
|
||
BUILD 020 — COMPLETE
|
||
```
|
||
|
||
Build 020 завершает фактический перенос канонического кэша Instrument Reference Data из legacy `ExchangeService._exchange_symbols_cache` в специализированный Storage Layer.
|
||
|
||
Существующий бот продолжает работать через сохранённый compatibility-контракт:
|
||
|
||
```python
|
||
get_exchange_symbols() -> list[ExchangeSymbol]
|
||
```
|
||
|
||
При этом новая архитектура уже располагает полноценным каноническим хранилищем:
|
||
|
||
```text
|
||
Instrument Acquisition Pipeline
|
||
↓
|
||
tuple[Instrument, ...]
|
||
↓
|
||
InstrumentStore
|
||
```
|
||
|
||
Это создаёт основу для дальнейшего поэтапного перевода consumers с legacy `ExchangeSymbol` на каноническую модель `Instrument` без нарушения работоспособности существующего бота. |