# Build 025 — Удаление legacy `ExchangeSymbol` compatibility layer

**Статус:** ✅ Завершён  
**Дата:** 2026-07-12  
**Подсистема:** Instrument Reference / Exchange Integration  
**Этап:** Завершение миграции на каноническую модель `Instrument`

---

# Цель Build

Полностью удалить временный compatibility layer, использовавшийся во время поэтапного перехода от legacy-модели:

```text
ExchangeSymbol
```

к канонической модели:

```text
Instrument
```

После завершения Build все production-потребители работают через единый канонический контур:

```text
Instrument Acquisition
        ↓
Instrument Store
        ↓
get_instruments()
        ↓
validate_symbol()
        ↓
Instrument
```

---

# Предпосылки

На предыдущих этапах были выполнены:

- создание канонической модели `Instrument`;
- построение Instrument Acquisition pipeline;
- создание `InstrumentStoreProtocol`;
- реализация `InMemoryInstrumentStore`;
- перенос кэша справочника инструментов в Storage;
- добавление `ExchangeService.get_instruments()`;
- перевод `validate_symbol()` на канонический справочник;
- перевод Telegram UI и runtime-потребителей на `Instrument`;
- удаление неиспользуемого legacy Market Handler.

После этого legacy-контур сохранялся только как временная проекция:

```text
Instrument
    ↓
compatibility.py
    ↓
ExchangeSymbol
    ↓
get_exchange_symbols()
```

Реальных production-потребителей этого контура больше не осталось.

---

# Предварительный аудит

Выполнен поиск production-зависимостей:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  -E "ExchangeSymbol|get_exchange_symbols|map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols|_exchange_symbols_projection_cache" \
  src
```

Установлено, что все найденные элементы находились только внутри самого legacy-контура:

```text
src/market_data/acquisition/compatibility.py
src/integrations/exchange/models.py
src/integrations/exchange/service.py
```

Канонические production-потребители уже использовали:

```text
get_instruments()
validate_symbol()
Instrument
```

---

# Аудит legacy parser helpers

Проверены методы:

```text
_extract_exchange_symbols_raw()
_parse_exchange_symbol()
_parse_exchange_symbol_status()
_parse_market_modes()
_extract_filter_value()
```

Выполнено:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  -E "_extract_exchange_symbols_raw|_parse_exchange_symbol\(|_parse_exchange_symbol_status|_parse_market_modes|_extract_filter_value" \
  src tests
```

Подтверждено, что эти методы использовались только устаревшим equivalence-тестом и больше не требовались production-коду.

---

# Подготовка тестового контура

Перед удалением production compatibility layer были обновлены канонические тесты.

## Обновлён файл

```text
tests/unit/integrations/exchange/test_service_instruments.py
```

Из него удалены тесты legacy-проекции:

```text
test_get_exchange_symbols_uses_get_instruments
test_get_exchange_symbols_maps_canonical_instruments
test_get_exchange_symbols_projection_cache_preserves_identity
```

Сохранены все тесты канонического поведения:

- отключённая биржа;
- Instrument Store hit;
- Instrument Store miss;
- загрузка через acquisition;
- сохранение результата в Store;
- повторное использование Store;
- поддержка пустого immutable-набора;
- общий Store между экземплярами `ExchangeService`;
- обработка acquisition errors;
- логирование ошибок;
- отсутствие заполнения Store при ошибке.

---

## Обновлён файл

```text
tests/unit/integrations/exchange/test_service_validate_symbol.py
```

Legacy-проверка через monkeypatch метода:

```text
get_exchange_symbols()
```

заменена проверкой прямого использования:

```text
get_instruments()
```

Дополнительно добавлен отрицательный архитектурный тест:

```python
def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None:
    assert not hasattr(
        ExchangeService,
        "get_exchange_symbols",
    )
```

---

## Обновлён файл

```text
tests/unit/telegram/ui/test_currency_ui.py
```

Legacy-проверка отсутствия вызова `get_exchange_symbols()` заменена прямой проверкой использования канонического:

```text
get_instruments()
```

---

# Проверка подготовительного пакета

Выполнена компиляция:

```bash
python -m py_compile \
  tests/unit/integrations/exchange/test_service_instruments.py \
  tests/unit/integrations/exchange/test_service_validate_symbol.py \
  tests/unit/telegram/ui/test_currency_ui.py
```

Результат:

```text
Ошибок нет.
```

Выполнены целевые тесты:

```bash
python -m pytest \
  tests/unit/integrations/exchange/test_service_instruments.py \
  tests/unit/integrations/exchange/test_service_validate_symbol.py \
  tests/unit/telegram/ui/test_currency_ui.py \
  -q
```

Результат:

```text
49 passed
```

---

# Удалённые migration-only файлы

Полностью удалены:

```text
src/market_data/acquisition/compatibility.py

tests/unit/market_data/acquisition/test_compatibility.py
tests/unit/market_data/acquisition/test_equivalence_comparator.py

tests/unit/integrations/exchange/test_service_exchange_symbols.py

tests/integration/market_data/acquisition/test_instrument_reference_equivalence.py
tests/support/instrument_reference_equivalence.py
```

Эти файлы обслуживали только временный compatibility/equivalence-контур и завершили свою миграционную задачу.

---

# Изменения в `models.py`

Из файла:

```text
src/integrations/exchange/models.py
```

полностью удалена legacy-модель:

```python
@dataclass(slots=True)
class ExchangeSymbol:
    ...
```

Модель:

```python
SymbolValidationResult
```

сохраняет канонический контракт:

```python
symbol_info: Instrument | None
```

Импорт `Instrument` выполняется через:

```python
TYPE_CHECKING
```

что исключает runtime-cycle и сохраняет корректную типизацию.

---

# Изменения в `service.py`

Из файла:

```text
src/integrations/exchange/service.py
```

удалены следующие элементы.

## Legacy imports

Удалены:

```python
ExchangeSymbol
```

и:

```python
from src.market_data.acquisition.compatibility import (
    map_instruments_to_exchange_symbols,
)
```

---

## Legacy projection cache

Удалено поле:

```python
_exchange_symbols_projection_cache
```

Теперь `ExchangeService` хранит только канонический Store:

```python
_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore()
```

---

## Legacy API

Полностью удалён метод:

```python
get_exchange_symbols()
```

Единственным публичным API справочника инструментов остаётся:

```python
get_instruments()
```

---

## Legacy parser helpers

Удалены методы:

```text
_extract_exchange_symbols_raw()
_parse_exchange_symbol()
_parse_exchange_symbol_status()
_parse_market_modes()
_extract_filter_value()
```

Их функции полностью заменены новой pipeline:

```text
Dzengi REST document
        ↓
Dzengi parser
        ↓
validation
        ↓
mapper
        ↓
Instrument
```

---

## Сохранённый helper

Метод:

```python
_safe_str()
```

сохранён, так как продолжает использоваться обработкой trading fee payload.

---

# Финальный production-контур

После удаления compatibility layer работа со справочником инструментов выполняется так:

```text
DzengiInstrumentDocumentSource
        ↓
DzengiInstrumentDocumentHandler
        ↓
InstrumentFeed
        ↓
InstrumentFeedRegistry
        ↓
InstrumentAcquisitionService
        ↓
tuple[Instrument, ...]
        ↓
Instrument Store
        ↓
ExchangeService.get_instruments()
```

Проверка пользовательского символа выполняется через:

```text
validate_symbol()
        ↓
SymbolValidationResult
        ↓
symbol_info: Instrument | None
```

---

# Архитектурные отрицательные тесты

Добавлены проверки физического отсутствия legacy API.

## Отсутствие legacy-метода

```python
def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None:
    assert not hasattr(
        ExchangeService,
        "get_exchange_symbols",
    )
```

## Отсутствие legacy projection cache

```python
def test_exchange_service_has_no_legacy_projection_cache() -> None:
    assert not hasattr(
        ExchangeService,
        "_exchange_symbols_projection_cache",
    )
```

---

# Финальный аудит

Выполнено:

```bash
grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  -E "ExchangeSymbol|get_exchange_symbols|map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols|_exchange_symbols_projection_cache|market_data\.acquisition\.compatibility" \
  src tests
```

Остались только ожидаемые упоминания в отрицательных архитектурных тестах:

```text
test_exchange_service_has_no_legacy_get_exchange_symbols
test_exchange_service_has_no_legacy_projection_cache
```

Других production- или test-зависимостей не обнаружено.

---

# Проверка компиляции

Выполнено:

```bash
python -m py_compile \
  src/integrations/exchange/models.py \
  src/integrations/exchange/service.py \
  tests/unit/integrations/exchange/test_service_instruments.py \
  tests/unit/integrations/exchange/test_service_validate_symbol.py \
  tests/unit/telegram/ui/test_currency_ui.py
```

Результат:

```text
Ошибок нет.
```

---

# Целевой Regression Suite

Выполнено:

```bash
python -m pytest \
  tests/unit/integrations/exchange/test_service_instruments.py \
  tests/unit/integrations/exchange/test_service_validate_symbol.py \
  tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
  tests/unit/integrations/exchange/test_market_stream.py \
  tests/unit/integrations/exchange/test_market_data_runner.py \
  tests/unit/telegram/ui/test_currency_ui.py \
  -q
```

Результат:

```text
94 passed
```

---

# Полный Regression Suite

Выполнено:

```bash
python -m pytest -q
```

Результат:

```text
423 passed
```

Снижение общего числа тестов относительно предыдущего Build является ожидаемым, поскольку были удалены временные compatibility- и equivalence-тесты вместе с соответствующим legacy-кодом.

---

# Архитектурный итог

До Build 025:

```text
Instrument
    ├── canonical consumers
    └── compatibility mapper
            ↓
        ExchangeSymbol
            ↓
        legacy projection cache
```

После Build 025:

```text
Instrument
    ↓
Instrument Store
    ↓
canonical consumers
```

В проекте больше не существует:

```text
ExchangeSymbol
compatibility.py
get_exchange_symbols()
_exchange_symbols_projection_cache
Instrument → ExchangeSymbol mapping
legacy exchangeInfo parser helpers
migration equivalence framework
```

---

# Итог Build

Build 025 полностью завершён.

Подтверждено:

- все production-потребители переведены на `Instrument`;
- удалена legacy-модель `ExchangeSymbol`;
- удалён временный compatibility mapper;
- удалён legacy-метод `get_exchange_symbols()`;
- удалён projection cache;
- удалены неиспользуемые parser helpers;
- удалены migration-only и equivalence-тесты;
- сохранены и усилены канонические тесты Instrument Store;
- добавлены архитектурные тесты отсутствия legacy API;
- компиляция проходит без ошибок;
- целевой Regression Suite успешно пройден — **94 passed**;
- полный Regression Suite успешно пройден — **423 passed**.

**Статус Build:** ✅ Завершён.
