# Build 029 — Dzengi Mapper и Quotes Handler

## Статус

**Завершён**

---

## 1. Цель Build 029

Цель Build 029 — реализовать преобразование специализированной модели REST-котировки Dzengi в каноническую модель `Quote` и создать обработчик полного цикла преобразования сырого REST-документа в проверенную внутреннюю модель котировки.

Build является частью поэтапной миграции подсистемы:

**Quotes Feed**

в новую архитектуру:

```text
src/market_data/acquisition/
```

На данном этапе реализованы:

- специализированный Dzengi quote mapper;
- преобразование `DzengiTicker24hrResponse` в канонический `Quote`;
- преобразование цен в `Decimal`;
- преобразование биржевого timestamp в timezone-aware UTC `datetime`;
- фиксация времени получения котировки;
- специализированный `QuotesHandler`;
- полная handler-цепочка обработки сырого REST-документа.

Подключение `Quotes Feed`, registry, `Acquisition Service`, `Quote Store`, `ExchangeService` facade и runtime-потребителей в данный Build не входит.

---

## 2. Место Build 029 в плане миграции Quotes Feed

Утверждённая последовательность:

```text
Build 026 — Аудит текущего контура Quotes Feed
Build 027 — Каноническая модель Quote и специализированные контракты
Build 028 — Dzengi REST quote models, parser и validation
Build 029 — Dzengi mapper и Quotes Handler
Build 030 — Quotes Feed и регистрация в Acquisition Service
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
Build 032 — Канонический Quote Store
Build 033 — Перенос MarketPriceCache на Quote Store
Build 034 — Dzengi WebSocket quote parsing и адаптер
Build 035 — Перевод market runtime на Quotes Feed
Build 036 — Перевод read-only и UI-потребителей
Build 037 — Перевод execution-потребителей
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
Build 039 — Удаление legacy quote parsing и MarketPriceCache
Build 040 — Финальная архитектурная проверка Quotes Feed
```

Build 029 продолжает фундамент, созданный в Builds 027–028.

После его завершения сформирована цепочка:

```text
raw Dzengi REST document
        ↓
schema validation
        ↓
Dzengi quote parser
        ↓
value validation
        ↓
DzengiTicker24hrResponse
        ↓
Dzengi quote mapper
        ↓
canonical Quote
```

---

## 3. Исходное состояние перед Build 029

До начала Build 029 уже были реализованы:

### Build 027

```text
src/market_data/acquisition/models/quote.py
src/market_data/acquisition/protocol.py
```

Были определены:

- каноническая модель `Quote`;
- контракт источника сырого quote-документа;
- контракт обработчика quote-документа;
- контракт готового `Quotes Feed`.

### Build 028

```text
src/market_data/acquisition/adapters/dzengi/models.py
src/market_data/acquisition/adapters/dzengi/parser.py
src/market_data/acquisition/validation/schema.py
src/market_data/acquisition/validation/values.py
src/market_data/acquisition/exceptions.py
```

Были реализованы:

- модель REST-ответа `/api/v1/ticker/24hr`;
- parser quote payload;
- schema validation;
- value validation;
- специализированные ошибки Acquisition layer.

Отсутствовал слой, преобразующий проверенную Dzengi-specific модель в канонический `Quote`, а также единая точка оркестрации всей цепочки обработки сырого документа.

---

## 4. Изменённые и добавленные файлы

В рамках Build 029 изменены только два исходных файла:

```text
src/market_data/acquisition/adapters/dzengi/mapper.py
src/market_data/acquisition/handlers/quotes_handler.py
```

Добавлены два специализированных файла тестов:

```text
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py
tests/unit/market_data/acquisition/handlers/test_quotes_handler.py
```

Другие файлы в рамках фактически применённого Build 029 не изменялись.

---

## 5. Dzengi Quote Mapper

В файле:

```text
src/market_data/acquisition/adapters/dzengi/mapper.py
```

реализовано преобразование:

```text
DzengiTicker24hrResponse
        ↓
Quote
```

Mapper является архитектурной границей между:

```text
exchange-specific adapter model
```

и:

```text
canonical Acquisition model
```

Его ответственность:

- принять проверенную модель `DzengiTicker24hrResponse`;
- преобразовать биржевые значения цен в канонический тип;
- преобразовать биржевой timestamp;
- определить источник данных;
- зафиксировать время получения котировки;
- создать канонический `Quote`.

Mapper не должен:

- выполнять REST-запрос;
- разбирать сырой JSON payload;
- выполнять schema validation сырого документа;
- управлять store или cache;
- обращаться к `ExchangeService`;
- содержать UI-логику;
- содержать execution-логику.

---

## 6. Преобразование модели Dzengi в канонический Quote

Исходная модель адаптера содержит данные, соответствующие REST-ответу Dzengi:

```text
symbol
lastPrice
bidPrice
askPrice
closeTime
```

После parsing и validation эти данные представлены специализированной моделью:

```text
DzengiTicker24hrResponse
```

Mapper преобразует её в:

```text
Quote
```

с канонической семантикой:

```text
symbol
last_price
bid_price
ask_price
source_timestamp
received_at
source
```

Таким образом, API-specific имена:

```text
lastPrice
bidPrice
askPrice
closeTime
```

не выходят за пределы Dzengi adapter layer.

---

## 7. Использование Decimal для цен

Цены преобразуются в `Decimal`.

Целевая семантика:

```text
last_price: Decimal
bid_price: Decimal
ask_price: Decimal
```

Это решение исключает ненужную потерю точности при преобразовании рыночных цен через бинарный `float`.

Архитектурная цепочка:

```text
Dzengi string price
        ↓
Decimal
        ↓
canonical Quote
```

Например:

```text
"64159.45"
        ↓
Decimal("64159.45")
```

Mapper не должен сначала преобразовывать строку в `float`, а затем создавать `Decimal`, поскольку такой путь способен внести артефакты двоичного представления числа.

---

## 8. Преобразование биржевого timestamp

Поле Dzengi:

```text
closeTime
```

содержит Unix timestamp в миллисекундах.

Mapper преобразует его в timezone-aware UTC `datetime`.

Семантика преобразования:

```text
closeTime milliseconds
        ↓
UTC datetime
        ↓
Quote.source_timestamp
```

Использование timezone-aware значения необходимо для однозначного представления времени рыночного события и последующих операций:

- freshness calculation;
- sequence validation;
- event ordering;
- диагностика задержек;
- сопоставление данных из нескольких источников.

---

## 9. Время получения котировки

Помимо биржевого времени события, канонический `Quote` содержит время фактического получения данных платформой:

```text
received_at
```

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

```text
source_timestamp
```

означает время, указанное источником данных;

```text
received_at
```

означает время, когда котировка была преобразована во внутреннюю модель платформы.

Это создаёт фундамент для последующего определения:

- возраста котировки;
- сетевой задержки;
- freshness;
- stale data;
- задержки между биржей и локальной системой.

---

## 10. Источник котировки

Канонический `Quote` получает идентификатор источника:

```text
dzengi
```

Это позволяет внутренней модели не зависеть от конкретного adapter-класса, сохраняя при этом происхождение рыночных данных.

Целевая модель допускает дальнейшую работу с несколькими источниками:

```text
Dzengi
Binance
Coinbase
другие источники
```

При этом приоритетным источником для торговых решений остаётся биржа исполнения.

---

## 11. Quotes Handler

В файле:

```text
src/market_data/acquisition/handlers/quotes_handler.py
```

реализован специализированный обработчик quote-документа.

Его ответственность — оркестрировать существующие специализированные стадии обработки:

```text
raw document
        ↓
schema validation
        ↓
parser
        ↓
value validation
        ↓
mapper
        ↓
Quote
```

Handler является единой точкой преобразования:

```text
object → Quote
```

Он не должен самостоятельно дублировать внутреннюю реализацию:

- schema validation;
- parsing;
- value validation;
- mapping.

Вместо этого handler координирует специализированные компоненты.

---

## 12. Полная цепочка обработки

После Build 029 полный путь REST-документа выглядит следующим образом:

```text
{
    "askPrice": "64159.55",
    "bidPrice": "64159.45",
    "closeTime": 1783887270312,
    "lastPrice": "64159.45",
    "symbol": "BTC/USD_LEVERAGE"
}
        ↓
schema validation
        ↓
Dzengi REST quote parser
        ↓
DzengiTicker24hrResponse
        ↓
value validation
        ↓
Dzengi quote mapper
        ↓
Quote(
    symbol=...,
    last_price=...,
    bid_price=...,
    ask_price=...,
    source_timestamp=...,
    received_at=...,
    source=...
)
```

Таким образом, верхние слои платформы больше не обязаны знать формат ответа Dzengi.

---

## 13. Архитектурные решения Build 029

### 13.1. Mapper изолирует специфику Dzengi

Только adapter layer знает о:

```text
DzengiTicker24hrResponse
lastPrice
bidPrice
askPrice
closeTime
```

После mapping верхние слои работают исключительно с:

```text
Quote
```

---

### 13.2. Handler не зависит от ExchangeService

Новый `QuotesHandler` не использует:

```text
src.integrations.exchange.service.ExchangeService
```

Направление зависимостей остаётся правильным:

```text
external Dzengi payload
        ↓
Acquisition adapter
        ↓
Acquisition handler
        ↓
canonical Quote
```

Обратной зависимости новой подсистемы от legacy integration layer нет.

---

### 13.3. Handler не является Feed

`QuotesHandler` отвечает только за преобразование документа:

```text
object → Quote
```

Он не отвечает за получение документа от биржи.

Получение данных будет ответственностью:

```text
src/market_data/acquisition/feeds/quotes_feed.py
```

на следующем этапе миграции.

---

### 13.4. Handler не является Store

`QuotesHandler` не сохраняет котировки.

Хранение будет реализовано отдельно:

```text
Build 032 — Канонический Quote Store
```

Такое разделение предотвращает смешивание:

```text
acquisition
processing
storage
```

---

### 13.5. Build не изменяет production runtime

В Build 029 не изменены:

```text
src/integrations/exchange/service.py
src/integrations/exchange/market_cache.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```

Не переведены:

```text
UI consumers
execution consumers
strategy consumers
diagnostics consumers
```

Работающий бот продолжает использовать прежний runtime-контур.

---

## 14. Что намеренно не реализовано

В Build 029 не входят:

```text
Quotes Feed
регистрация Quotes Feed
подключение к Acquisition Service
подключение нового REST Quotes Feed к ExchangeService facade
Quote Store
перенос MarketPriceCache на Quote Store
WebSocket quote parsing
WebSocket quote adapter
перевод market runtime
перевод read-only потребителей
перевод UI-потребителей
перевод execution-потребителей
удаление TickerPrice
удаление market snapshot dict layer
удаление legacy quote parsing
удаление MarketPriceCache
```

Каждая из этих задач выполняется только в соответствующем последующем Build.

---

## 15. Проверки

Выполнена синтаксическая проверка:

```bash
python -m py_compile \
  src/market_data/acquisition/adapters/dzengi/mapper.py \
  src/market_data/acquisition/handlers/quotes_handler.py \
  tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \
  tests/unit/market_data/acquisition/handlers/test_quotes_handler.py
```

Результат:

```text
Успешно.
```

Выполнены специализированные тесты Build 029:

```bash
python -m pytest \
  tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \
  tests/unit/market_data/acquisition/handlers/test_quotes_handler.py \
  -q
```

Результат:

```text
12 passed in 0.03s
```

Выполнена полная регрессия проекта:

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

Результат:

```text
457 passed in 0.26s
```

Регрессий не обнаружено.

Количество тестов увеличилось:

```text
После Build 028: 445 passed
После Build 029: 457 passed
```

Добавлено:

```text
12 специализированных тестов
```

---

## 16. Критерии завершения Build 029

Build 029 считается завершённым, поскольку выполнены все необходимые условия:

- [x] реализован специализированный Dzengi quote mapper;
- [x] `DzengiTicker24hrResponse` преобразуется в канонический `Quote`;
- [x] API-specific имена не выходят за пределы adapter layer;
- [x] цены преобразуются в `Decimal`;
- [x] не используется промежуточное преобразование цен через `float`;
- [x] `closeTime` преобразуется в timezone-aware UTC `datetime`;
- [x] фиксируется `received_at`;
- [x] сохраняется источник котировки;
- [x] реализован специализированный `QuotesHandler`;
- [x] handler оркестрирует полный цикл обработки сырого документа;
- [x] handler не дублирует ответственность parser;
- [x] handler не дублирует ответственность validation;
- [x] handler не дублирует ответственность mapper;
- [x] новая реализация не зависит от `ExchangeService`;
- [x] новая реализация не зависит от `MarketPriceCache`;
- [x] production runtime не изменён;
- [x] специализированные тесты проходят;
- [x] полная регрессия проходит.

---

## 17. Итог

В результате Build 029 завершён слой преобразования REST-котировки Dzengi во внутреннюю каноническую модель платформы:

```text
Dzengi REST payload
        ↓
schema validation
        ↓
parser
        ↓
value validation
        ↓
DzengiTicker24hrResponse
        ↓
mapper
        ↓
canonical Quote
```

Также создан единый специализированный обработчик:

```text
QuotesHandler
```

который предоставляет операцию:

```text
raw document → canonical Quote
```

При этом сохранены ключевые архитектурные свойства миграции:

- новая реализация развивается параллельно legacy-контуру;
- работающий бот не сломан;
- `ExchangeService` не изменён;
- `MarketPriceCache` не изменён;
- runtime-потребители не изменены;
- Dzengi-specific формат изолирован внутри adapter layer;
- верхние слои получают каноническую модель `Quote`;
- mapping и orchestration разделены по ответственности;
- сохранена возможность безопасного поэтапного переключения системы.

**Build 029 завершён.**

Следующий этап:

```text
Build 030 — Quotes Feed и регистрация в Acquisition Service
```