build 039: complete Quotes Feed migration foundation
This commit is contained in:
799
docs/migrations/build_026.md
Normal file
799
docs/migrations/build_026.md
Normal file
@@ -0,0 +1,799 @@
|
||||
# Build 026 — Аудит текущего контура Quotes Feed
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Функциональный модуль:** Quotes Feed
|
||||
**Проект:** Dzentra
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Провести полный аудит существующего контура получения, обработки, кэширования и потребления текущих рыночных котировок перед началом миграции в целевую подсистему:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/
|
||||
```
|
||||
|
||||
Основная задача Build — определить:
|
||||
|
||||
- где сейчас реализовано получение котировок;
|
||||
- какие REST- и WebSocket-источники используются;
|
||||
- какие модели представляют котировку;
|
||||
- где выполняются parsing, validation и mapping;
|
||||
- как работает оперативный кэш котировок;
|
||||
- какие компоненты являются фактическими потребителями ценовых данных;
|
||||
- какие обязанности относятся непосредственно к Quotes Feed;
|
||||
- какие обязанности должны остаться за пределами Acquisition;
|
||||
- в какой последовательности выполнять безопасную миграцию без нарушения работы существующего бота.
|
||||
|
||||
---
|
||||
|
||||
## 2. Итог аудита
|
||||
|
||||
Текущий бот уже имеет функционально работающий контур получения и использования котировок.
|
||||
|
||||
Котировки поступают из двух источников:
|
||||
|
||||
1. REST API;
|
||||
2. WebSocket depth stream.
|
||||
|
||||
При этом архитектурно логика распределена между:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/rest_client.py
|
||||
src/integrations/exchange/ws_client.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Единой канонической модели `Quote` в production-контуре пока нет.
|
||||
|
||||
Одна и та же концепция текущей рыночной котировки представлена несколькими различными контрактами:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
MarketPriceSnapshot
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
Это подтверждает необходимость поэтапной миграции в каноническую модель Quotes Feed.
|
||||
|
||||
---
|
||||
|
||||
## 3. Текущий REST-контур котировок
|
||||
|
||||
Основная реализация находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
Используются следующие методы:
|
||||
|
||||
```python
|
||||
refresh_price_cache()
|
||||
refresh_market_snapshot_cache()
|
||||
get_price()
|
||||
get_market_snapshot()
|
||||
get_execution_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
_get_real_price()
|
||||
```
|
||||
|
||||
Основной источник данных:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
```
|
||||
|
||||
Из ответа используются поля:
|
||||
|
||||
```text
|
||||
lastPrice
|
||||
bidPrice
|
||||
askPrice
|
||||
```
|
||||
|
||||
Текущая цепочка выглядит следующим образом:
|
||||
|
||||
```text
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
↓
|
||||
ExchangeRestClient.get_json()
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
lastPrice / bidPrice / askPrice
|
||||
↓
|
||||
legacy dict snapshot
|
||||
```
|
||||
|
||||
REST-транспорт реализован в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/rest_client.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Текущий WebSocket-контур котировок
|
||||
|
||||
В проекте существуют две реализации обработки WebSocket/depth-данных:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
WebSocket-транспорт находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/ws_client.py
|
||||
```
|
||||
|
||||
Для получения данных используется:
|
||||
|
||||
```python
|
||||
ExchangeWebSocketClient.stream_depth()
|
||||
```
|
||||
|
||||
Из depth payload извлекаются:
|
||||
|
||||
```text
|
||||
best bid
|
||||
best ask
|
||||
```
|
||||
|
||||
После чего рассчитывается:
|
||||
|
||||
```text
|
||||
midpoint = (best_bid + best_ask) / 2
|
||||
```
|
||||
|
||||
Результат записывается в:
|
||||
|
||||
```text
|
||||
MarketPriceCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Текущие модели котировок
|
||||
|
||||
### 5.1. TickerPrice
|
||||
|
||||
Находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Текущий контракт:
|
||||
|
||||
```python
|
||||
@dataclass(slots=True)
|
||||
class TickerPrice:
|
||||
symbol: str
|
||||
price: float
|
||||
source: str
|
||||
updated_at: str
|
||||
```
|
||||
|
||||
Используется как упрощённое представление текущей цены инструмента.
|
||||
|
||||
---
|
||||
|
||||
### 5.2. ExecutionPriceSnapshot
|
||||
|
||||
Находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Содержит:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
is_fresh
|
||||
age_seconds
|
||||
freshness_status
|
||||
spread_percent
|
||||
```
|
||||
|
||||
Эта модель относится прежде всего к execution layer и не должна становиться канонической моделью Quotes Feed.
|
||||
|
||||
---
|
||||
|
||||
### 5.3. MarketPriceSnapshot
|
||||
|
||||
Находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
Содержит:
|
||||
|
||||
```text
|
||||
symbol
|
||||
price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
runtime_key
|
||||
received_monotonic
|
||||
```
|
||||
|
||||
Одновременно выполняет роль:
|
||||
|
||||
- модели записи кэша;
|
||||
- контейнера рыночной цены;
|
||||
- источника информации о возрасте записи.
|
||||
|
||||
---
|
||||
|
||||
### 5.4. Словарные snapshot-контракты
|
||||
|
||||
Ряд методов `ExchangeService` возвращает:
|
||||
|
||||
```python
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
с ключами:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
updated_at
|
||||
source
|
||||
age_seconds
|
||||
```
|
||||
|
||||
Такие словарные контракты используются многими существующими потребителями и должны быть удалены только после их полного перевода на новые типизированные контракты.
|
||||
|
||||
---
|
||||
|
||||
## 6. Основные архитектурные проблемы
|
||||
|
||||
### 6.1. Отсутствует единая каноническая модель Quote
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/quote.py
|
||||
```
|
||||
|
||||
существует в целевой структуре, но текущий production-контур ещё не использует единую каноническую модель `Quote`.
|
||||
|
||||
Вместо неё используются:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
MarketPriceSnapshot
|
||||
dict[str, object]
|
||||
```
|
||||
|
||||
Целевая архитектура должна иметь одну внутреннюю каноническую модель котировки.
|
||||
|
||||
---
|
||||
|
||||
### 6.2. ExchangeService перегружен обязанностями
|
||||
|
||||
В текущем состоянии `ExchangeService` одновременно:
|
||||
|
||||
- вызывает REST API;
|
||||
- получает ticker response;
|
||||
- разбирает поля ответа;
|
||||
- проверяет значения;
|
||||
- создаёт snapshot;
|
||||
- читает кэш;
|
||||
- обновляет кэш;
|
||||
- оценивает freshness;
|
||||
- создаёт execution snapshot;
|
||||
- поддерживает legacy API для существующих потребителей.
|
||||
|
||||
Эти обязанности должны быть постепенно разделены между:
|
||||
|
||||
```text
|
||||
adapters/dzengi/
|
||||
validation/
|
||||
models/
|
||||
handlers/
|
||||
feeds/
|
||||
service.py
|
||||
storage/
|
||||
execution/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.3. WebSocket parsing дублируется
|
||||
|
||||
Сходная логика присутствует одновременно в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Дублируются следующие операции:
|
||||
|
||||
- извлечение вложенного payload;
|
||||
- извлечение `bids`;
|
||||
- извлечение `asks`;
|
||||
- получение первой цены;
|
||||
- преобразование значения в `float`;
|
||||
- проверка положительности цены;
|
||||
- расчёт midpoint.
|
||||
|
||||
Эта логика должна быть централизована в Dzengi adapter:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/parser.py
|
||||
src/market_data/acquisition/adapters/dzengi/mapper.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.4. Quotes Feed и Order Book Feed частично смешаны
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
stream_depth()
|
||||
```
|
||||
|
||||
получает depth-сообщение, относящееся к данным стакана.
|
||||
|
||||
Однако текущие потребители используют из него только:
|
||||
|
||||
```text
|
||||
best bid
|
||||
best ask
|
||||
```
|
||||
|
||||
Для Quotes Feed это допустимый источник Level I quote.
|
||||
|
||||
При этом полный depth не должен переноситься в Quotes Feed, поскольку полный стакан относится к отдельной будущей подсистеме:
|
||||
|
||||
```text
|
||||
Order Book Feed
|
||||
```
|
||||
|
||||
Таким образом, Quotes Feed должен получать из depth только необходимую информацию верхнего уровня:
|
||||
|
||||
```text
|
||||
best bid
|
||||
best ask
|
||||
```
|
||||
|
||||
и формировать из неё канонический `Quote`.
|
||||
|
||||
---
|
||||
|
||||
### 6.5. Кэш расположен в integration layer
|
||||
|
||||
Текущий кэш находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
Он отвечает одновременно за:
|
||||
|
||||
- модель snapshot;
|
||||
- хранение;
|
||||
- runtime partitioning;
|
||||
- возраст записи;
|
||||
- форматирование локального времени.
|
||||
|
||||
В целевой архитектуре хранение котировок не должно принадлежать Acquisition или exchange integration layer.
|
||||
|
||||
Канонический Quote Store должен находиться в storage layer.
|
||||
|
||||
---
|
||||
|
||||
### 6.6. Внутреннее время представлено UI-строкой
|
||||
|
||||
Текущее представление:
|
||||
|
||||
```text
|
||||
DD.MM.YYYY HH:MM:SS
|
||||
```
|
||||
|
||||
например:
|
||||
|
||||
```text
|
||||
10.07.2026 12:00:00
|
||||
```
|
||||
|
||||
является человекочитаемым UI-представлением, а не подходящим внутренним временным контрактом.
|
||||
|
||||
Каноническая модель должна хранить машинное время, например:
|
||||
|
||||
```text
|
||||
exchange_timestamp_ms
|
||||
received_timestamp_ms
|
||||
```
|
||||
|
||||
или timezone-aware `datetime`.
|
||||
|
||||
Форматирование времени для пользователя должно происходить только на UI-границе.
|
||||
|
||||
---
|
||||
|
||||
### 6.7. REST client содержит дублирование
|
||||
|
||||
В:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/rest_client.py
|
||||
```
|
||||
|
||||
существуют два метода:
|
||||
|
||||
```python
|
||||
get_payload()
|
||||
get_json()
|
||||
```
|
||||
|
||||
которые в значительной степени дублируют транспортную реализацию.
|
||||
|
||||
Исправление этого дублирования не является задачей первого этапа Quotes Feed.
|
||||
|
||||
Однако при дальнейшем развитии Dzengi REST adapter не следует создавать дополнительное дублирование транспорта.
|
||||
|
||||
---
|
||||
|
||||
### 6.8. Текущий WebSocket не является обычной push-subscription
|
||||
|
||||
Метод:
|
||||
|
||||
```python
|
||||
stream_depth()
|
||||
```
|
||||
|
||||
работает следующим образом:
|
||||
|
||||
```text
|
||||
открыть постоянное WebSocket-соединение
|
||||
↓
|
||||
отправить новый request
|
||||
↓
|
||||
получить один response
|
||||
↓
|
||||
сделать sleep
|
||||
↓
|
||||
повторить request
|
||||
```
|
||||
|
||||
Таким образом, текущая реализация ближе к polling поверх постоянного WebSocket-соединения, чем к классической push-subscription.
|
||||
|
||||
Кроме того, запуск WebSocket stream из:
|
||||
|
||||
```text
|
||||
src/main.py
|
||||
```
|
||||
|
||||
временно отключён, поскольку runtime probe не подтвердил рабочий endpoint с WebSocket Upgrade 101.
|
||||
|
||||
Поэтому на текущем этапе архитектурно зафиксировано:
|
||||
|
||||
```text
|
||||
REST — рабочий основной источник котировок
|
||||
WebSocket — сохраняемый экспериментальный или резервный транспорт
|
||||
```
|
||||
|
||||
Первая версия нового Quotes Feed не должна зависеть от гарантированной доступности WebSocket.
|
||||
|
||||
---
|
||||
|
||||
## 7. Граница ответственности канонической модели Quote
|
||||
|
||||
Каноническая модель должна представлять непосредственно полученную рыночную котировку.
|
||||
|
||||
В неё должны входить данные уровня:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
exchange_timestamp
|
||||
received_timestamp
|
||||
source
|
||||
```
|
||||
|
||||
Дополнительно могут быть предусмотрены:
|
||||
|
||||
```text
|
||||
sequence_id
|
||||
event_id
|
||||
```
|
||||
|
||||
но только если соответствующий источник Dzengi действительно предоставляет такие значения.
|
||||
|
||||
---
|
||||
|
||||
## 8. Что не должно входить в базовую модель Quote
|
||||
|
||||
В каноническую модель не следует помещать:
|
||||
|
||||
```text
|
||||
runtime_key
|
||||
age_seconds
|
||||
is_fresh
|
||||
freshness_status
|
||||
spread_percent
|
||||
execution side
|
||||
entry price
|
||||
UI-formatted updated_at
|
||||
```
|
||||
|
||||
Причины:
|
||||
|
||||
| Поле | Правильная ответственность |
|
||||
|---|---|
|
||||
| `runtime_key` | Storage |
|
||||
| `age_seconds` | Storage / Access layer |
|
||||
| `is_fresh` | Политика конкретного потребителя |
|
||||
| `freshness_status` | Runtime / consumer policy |
|
||||
| `spread_percent` | Производная метрика |
|
||||
| `execution side` | Execution layer |
|
||||
| `entry price` | Execution layer |
|
||||
| `updated_at` в UI-формате | UI formatting |
|
||||
|
||||
---
|
||||
|
||||
## 9. Фактические потребители котировок
|
||||
|
||||
### 9.1. Потребители `get_price()`
|
||||
|
||||
```text
|
||||
src/telegram/ui/currency_ui.py
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/trading/auto/execution_quality.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9.2. Потребители `get_market_snapshot()`
|
||||
|
||||
```text
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/auto/execution_quality.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
src/trading/diagnostics/snapshot.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9.3. Потребители `get_execution_snapshot()`
|
||||
|
||||
```text
|
||||
src/trading/execution/pricing.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9.4. Потребители `get_fresh_market_snapshot()`
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/trading/debug/execution.py
|
||||
```
|
||||
|
||||
Кроме того, этот метод используется внутри runtime-проверки статуса инструмента.
|
||||
|
||||
---
|
||||
|
||||
## 10. Текущий MarketPriceCache
|
||||
|
||||
Реализация находится в:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_cache.py
|
||||
```
|
||||
|
||||
Основные операции:
|
||||
|
||||
```python
|
||||
MarketPriceCache.set_price()
|
||||
MarketPriceCache.get_price()
|
||||
MarketPriceCache.clear()
|
||||
```
|
||||
|
||||
Ключ записи:
|
||||
|
||||
```text
|
||||
(runtime_key, symbol)
|
||||
```
|
||||
|
||||
Кэш используется из:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
На текущем этапе `MarketPriceCache` нельзя удалять, поскольку он является частью рабочего production-контура.
|
||||
|
||||
Он будет заменён только после появления канонического Quote Store и перевода всех производителей и потребителей.
|
||||
|
||||
---
|
||||
|
||||
## 11. Целевая архитектурная цепочка REST Quotes Feed
|
||||
|
||||
```text
|
||||
Dzengi GET /api/v1/ticker/24hr
|
||||
↓
|
||||
adapters/dzengi/rest.py
|
||||
↓
|
||||
adapters/dzengi/models.py
|
||||
↓
|
||||
adapters/dzengi/parser.py
|
||||
↓
|
||||
validation/schema.py
|
||||
↓
|
||||
validation/values.py
|
||||
↓
|
||||
adapters/dzengi/mapper.py
|
||||
↓
|
||||
models/quote.py
|
||||
↓
|
||||
handlers/quotes_handler.py
|
||||
↓
|
||||
feeds/quotes_feed.py
|
||||
↓
|
||||
acquisition/service.py
|
||||
↓
|
||||
legacy ExchangeService facade
|
||||
↓
|
||||
существующие потребители бота
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Целевая архитектурная цепочка WebSocket Quotes Feed
|
||||
|
||||
```text
|
||||
Dzengi WebSocket depth message
|
||||
↓
|
||||
adapters/dzengi/websocket.py
|
||||
↓
|
||||
adapters/dzengi/parser.py
|
||||
↓
|
||||
извлечение best bid / best ask
|
||||
↓
|
||||
validation/
|
||||
↓
|
||||
adapters/dzengi/mapper.py
|
||||
↓
|
||||
models/quote.py
|
||||
↓
|
||||
handlers/quotes_handler.py
|
||||
↓
|
||||
feeds/quotes_feed.py
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
runtime consumers
|
||||
```
|
||||
|
||||
Полный order book при этом не является частью Quotes Feed и должен в будущем обрабатываться отдельной подсистемой:
|
||||
|
||||
```text
|
||||
Order Book Feed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Принцип безопасной миграции
|
||||
|
||||
Миграция должна выполняться без одномоментной замены рабочего контура.
|
||||
|
||||
Основной принцип:
|
||||
|
||||
```text
|
||||
новая реализация создаётся параллельно
|
||||
↓
|
||||
покрывается тестами
|
||||
↓
|
||||
подключается под существующий facade
|
||||
↓
|
||||
потребители переводятся поэтапно
|
||||
↓
|
||||
legacy удаляется только после подтверждения отсутствия потребителей
|
||||
```
|
||||
|
||||
На переходном этапе сохраняются:
|
||||
|
||||
```text
|
||||
ExchangeService.get_price()
|
||||
ExchangeService.get_market_snapshot()
|
||||
ExchangeService.get_execution_snapshot()
|
||||
ExchangeService.get_fresh_market_snapshot()
|
||||
MarketPriceCache
|
||||
TickerPrice
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
Удаление допускается только в соответствующих поздних Build после полного перевода потребителей.
|
||||
|
||||
---
|
||||
|
||||
## 14. Утверждённый план миграции 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
|
||||
```
|
||||
|
||||
Положение WebSocket-этапа после рабочего REST-контура является намеренным.
|
||||
|
||||
Бот должен сохранить гарантированный рабочий способ получения котировок даже при отсутствии подтверждённого production WebSocket endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 15. Результат Build 026
|
||||
|
||||
В результате Build 026:
|
||||
|
||||
- полностью определён существующий REST-контур котировок;
|
||||
- полностью определён существующий WebSocket/depth-контур;
|
||||
- найдены все текущие модели ценовых данных;
|
||||
- определены прямые производители и потребители котировок;
|
||||
- проанализирован `MarketPriceCache`;
|
||||
- обнаружено дублирование WebSocket parsing;
|
||||
- определена граница между Quotes Feed и Order Book Feed;
|
||||
- определена граница между Acquisition, Storage, Execution и UI;
|
||||
- подтверждена необходимость сохранения legacy facade на время миграции;
|
||||
- определена безопасная последовательность Build 027–040.
|
||||
|
||||
---
|
||||
|
||||
## 16. Статус завершения
|
||||
|
||||
**Build 026 завершён полностью.**
|
||||
|
||||
Дополнительных изменений кода в рамках Build 026 не требуется.
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 027 — Каноническая модель Quote и специализированные контракты
|
||||
```
|
||||
Reference in New Issue
Block a user