# Build 028 — Dzengi REST Quote Models, Parser и Validation

## Статус

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

---

## 1. Цель Build 028

Цель Build 028 — реализовать специализированный слой приёма, разбора и первичной проверки REST-ответа Dzengi для текущей рыночной котировки инструмента, не изменяя существующее поведение работающего бота и не подключая новую реализацию к production runtime до следующих этапов миграции.

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

**Quotes Feed**

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

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

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

- модель сырого REST-ответа Dzengi;
- parser REST-ответа `/api/v1/ticker/24hr`;
- проверка структуры входящего payload;
- проверка допустимости значений котировки;
- специализированные ошибки обработки quote payload.

Подключение mapper, handler, feed, service, store и перевод runtime-потребителей в данный Build не входят.

---

## 2. Место Build 028 в плане миграции 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 028 продолжает фундамент, созданный в Build 027.

Целевая цепочка после завершения следующих этапов:

```text
Dzengi REST /api/v1/ticker/24hr
        ↓
adapters/dzengi/rest.py
        ↓
adapters/dzengi/parser.py
        ↓
adapters/dzengi/models.py
        ↓
validation/schema.py
        ↓
validation/values.py
        ↓
adapters/dzengi/mapper.py
        ↓
handlers/quotes_handler.py
        ↓
feeds/quotes_feed.py
        ↓
acquisition/service.py
        ↓
Quote Store
        ↓
потребители платформы
```

---

## 3. Исходные данные

Для проектирования реализации использован реальный успешный ответ Dzengi:

```json
{
  "askPrice": "64159.55",
  "bidPrice": "64159.45",
  "closeTime": 1783887270312,
  "highPrice": "64261.45",
  "lastPrice": "64159.45",
  "lastQty": "5.0",
  "lowPrice": "63590.7",
  "openPrice": "63785.75",
  "openTime": 1783814400000,
  "prevClosePrice": "63785.75",
  "priceChange": "368.85",
  "priceChangePercent": "0.57822",
  "quoteVolume": "616402.92146",
  "symbol": "BTC/USD_LEVERAGE",
  "volume": "9.6002",
  "weightedAvgPrice": "64159.50"
}
```

Для базовой модели текущей котировки используются поля:

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

Остальные поля ответа `/api/v1/ticker/24hr` относятся к расширенной 24-часовой статистике рынка и не включаются в базовую модель `Quote`.

Это сохраняет правильное разделение ответственностей между:

- текущей котировкой;
- рыночной статистикой;
- OHLCV;
- trades;
- order book;
- другими специализированными типами рыночных данных.

---

## 4. Реализованные компоненты

В рамках 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
```

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

```text
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py
tests/unit/market_data/acquisition/validation/test_quote_schema.py
tests/unit/market_data/acquisition/validation/test_quote_values.py
```

---

## 5. Dzengi REST Quote Model

В файле:

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

реализована модель сырой котировки Dzengi.

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

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

Архитектурная граница:

```text
Dzengi API payload
        ↓
Dzengi REST quote model
        ↓
mapper
        ↓
canonical Quote
```

Модель адаптера является специфичной для Dzengi и не должна использоваться напрямую верхними слоями платформы.

---

## 6. REST Quote Parser

В файле:

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

реализован специализированный parser REST-котировки.

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

1. принять необработанный ответ API;
2. определить фактический quote payload;
3. поддержать прямую структуру ответа;
4. поддержать wrapped payload;
5. проверить структуру через schema validation;
6. извлечь необходимые поля;
7. проверить значения через value validation;
8. вернуть специализированную Dzengi quote model.

Поддерживаемые формы payload:

```text
Прямой payload
```

```json
{
  "symbol": "BTC/USD_LEVERAGE",
  "lastPrice": "64159.45",
  "bidPrice": "64159.45",
  "askPrice": "64159.55",
  "closeTime": 1783887270312
}
```

и wrapped payload:

```json
{
  "payload": {
    "symbol": "BTC/USD_LEVERAGE",
    "lastPrice": "64159.45",
    "bidPrice": "64159.45",
    "askPrice": "64159.55",
    "closeTime": 1783887270312
  }
}
```

Parser не создаёт канонический `Quote`. Это ответственность mapper, реализуемого в Build 029.

---

## 7. Schema Validation

В файле:

```text
src/market_data/acquisition/validation/schema.py
```

реализована проверка структуры quote payload.

Проверяются обязательные поля:

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

Schema validation отвечает только на вопрос:

> Имеет ли входящее сообщение необходимую структуру для дальнейшей обработки?

Она не должна:

- преобразовывать значения;
- вычислять midpoint;
- определять freshness;
- создавать `Quote`;
- обращаться к сети;
- обращаться к store;
- зависеть от `ExchangeService`.

---

## 8. Value Validation

В файле:

```text
src/market_data/acquisition/validation/values.py
```

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

Контролируются следующие инварианты:

```text
symbol != empty
last_price > 0
bid_price > 0
ask_price > 0
close_time >= 0
bid_price <= ask_price
```

Проверка:

```text
bid_price <= ask_price
```

является важным базовым инвариантом котировки.

Payload, в котором:

```text
bid_price > ask_price
```

не должен бесконтрольно попадать в канонический слой платформы.

---

## 9. Исключения

В файле:

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

используются специализированные исключения Acquisition layer для ошибок обработки рыночных данных.

Ошибки quote parsing и validation не должны зависеть от:

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

Это необходимо для соблюдения направления зависимостей:

```text
market_data/acquisition
        X
integrations/exchange legacy layer
```

Новая подсистема Acquisition не должна архитектурно зависеть от legacy `ExchangeService`.

---

## 10. Архитектурные решения Build 028

### 10.1. Каноническая модель не зависит от формата Dzengi

Поля API:

```text
lastPrice
bidPrice
askPrice
closeTime
```

существуют только внутри Dzengi adapter layer.

Во внутренних слоях платформы используются канонические имена:

```text
last_price
bid_price
ask_price
source_timestamp_ms
```

Преобразование между ними является ответственностью mapper.

---

### 10.2. Parser не выполняет mapping

Разделение сохраняется строго:

```text
parser
    ↓
разбирает внешний payload

validation
    ↓
проверяет структуру и значения

mapper
    ↓
преобразует adapter model в canonical model
```

Это предотвращает смешивание:

- API-specific parsing;
- validation;
- domain mapping.

---

### 10.3. REST quote не зависит от legacy TickerPrice

Новая цепочка не использует:

```text
src.integrations.exchange.models.TickerPrice
```

`TickerPrice` остаётся временной legacy-моделью и будет удалён только после перевода всех потребителей согласно плану миграции.

---

### 10.4. REST quote не зависит от MarketPriceCache

Build 028 не изменяет:

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

и не записывает данные в:

```text
MarketPriceCache
```

Миграция хранения выполняется отдельно:

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

---

### 10.5. Runtime-поведение бота не изменено

На этапе Build 028:

- новый parser не подключён к production runtime;
- `ExchangeService` продолжает работать по прежнему интерфейсу;
- `MarketPriceCache` не изменён;
- `MarketDataRunner` не изменён;
- execution-потребители не изменены;
- UI-потребители не изменены.

Таким образом, Build 028 является безопасным additive-этапом миграции.

---

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

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

```text
Dzengi mapper
Quotes Handler
Quotes Feed
регистрация Quotes Feed
подключение к Acquisition Service
подключение к ExchangeService facade
Quote Store
перенос MarketPriceCache
WebSocket quote parser
перевод MarketDataRunner
перевод UI-потребителей
перевод execution-потребителей
удаление TickerPrice
удаление market snapshot dict layer
удаление MarketPriceCache
```

Эти изменения выполняются только в соответствующих последующих Build.

---

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

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

```bash
python -m py_compile \
  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 \
  tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \
  tests/unit/market_data/acquisition/validation/test_quote_schema.py \
  tests/unit/market_data/acquisition/validation/test_quote_values.py
```

Результат:

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

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

```bash
python -m pytest \
  tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \
  tests/unit/market_data/acquisition/validation/test_quote_schema.py \
  tests/unit/market_data/acquisition/validation/test_quote_values.py \
  -q
```

Результат:

```text
22 passed in 0.02s
```

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

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

Результат:

```text
445 passed in 0.27s
```

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

---

## 13. Критерии завершения Build 028

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

- [x] получен реальный успешный ответ `/api/v1/ticker/24hr`;
- [x] определён минимальный набор полей текущей котировки;
- [x] реализована специализированная Dzengi REST quote model;
- [x] реализован REST quote parser;
- [x] поддержан прямой payload;
- [x] поддержан wrapped payload;
- [x] реализована schema validation;
- [x] реализована value validation;
- [x] проверяются положительные цены;
- [x] проверяется временная метка;
- [x] проверяется инвариант `bid_price <= ask_price`;
- [x] новая реализация не зависит от legacy `TickerPrice`;
- [x] новая реализация не зависит от `MarketPriceCache`;
- [x] production runtime не изменён;
- [x] специализированные тесты проходят;
- [x] полная регрессия проходит.

---

## 14. Итог

В результате Build 028 создан специализированный входной контур для REST-котировок Dzengi:

```text
Dzengi /api/v1/ticker/24hr
        ↓
raw payload
        ↓
schema validation
        ↓
Dzengi quote parser
        ↓
value validation
        ↓
Dzengi REST quote model
```

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

- новая реализация добавлена параллельно legacy-контуру;
- работающий бот не сломан;
- публичное поведение `ExchangeService` не изменено;
- отсутствует зависимость новой Acquisition subsystem от legacy quote models;
- parsing, validation и будущий mapping разделены по ответственности;
- сохранена возможность безопасного поэтапного переключения потребителей.

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

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

```text
Build 029 — Dzengi mapper и Quotes Handler
```