894 lines
22 KiB
Markdown
894 lines
22 KiB
Markdown
# Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
|
||
|
||
## Статус
|
||
|
||
**Завершён.**
|
||
|
||
---
|
||
|
||
## Цель
|
||
|
||
Подключить новый канонический контур **Quotes Feed** как внутренний источник свежих REST-котировок для существующего `ExchangeService`, сохранив без изменений его публичные legacy-контракты и поведение существующих потребителей.
|
||
|
||
Основная архитектурная цель Build 031:
|
||
|
||
```text
|
||
Dzengi GET /api/v1/ticker/24hr
|
||
↓
|
||
DzengiQuoteDocumentSource
|
||
↓
|
||
DzengiQuoteDocumentHandler
|
||
↓
|
||
QuotesFeed
|
||
↓
|
||
QuoteAcquisitionService
|
||
↓
|
||
Quote
|
||
↓
|
||
ExchangeService legacy facade
|
||
↓
|
||
существующие потребители
|
||
```
|
||
|
||
После Build 031 `ExchangeService` больше не должен самостоятельно:
|
||
|
||
- выполнять прямой REST-запрос к `/api/v1/ticker/24hr`;
|
||
- знать транспортные поля `lastPrice`, `bidPrice`, `askPrice`, `closeTime`;
|
||
- разбирать сырой ответ ticker endpoint;
|
||
- выполнять собственный parsing котировки Dzengi.
|
||
|
||
Эти обязанности переданы специализированной подсистеме `market_data/acquisition`.
|
||
|
||
---
|
||
|
||
## Исходное состояние
|
||
|
||
До Build 031 метод:
|
||
|
||
```python
|
||
ExchangeService.get_fresh_market_snapshot()
|
||
```
|
||
|
||
самостоятельно выполнял полный legacy-процесс:
|
||
|
||
```text
|
||
ExchangeService
|
||
↓
|
||
ExchangeRestClient
|
||
↓
|
||
GET /api/v1/ticker/24hr
|
||
↓
|
||
ручное чтение lastPrice / bidPrice / askPrice / closeTime
|
||
↓
|
||
legacy dict snapshot
|
||
```
|
||
|
||
В результате `ExchangeService` одновременно отвечал за:
|
||
|
||
- транспорт;
|
||
- знание конкретного endpoint Dzengi;
|
||
- знание транспортной схемы Dzengi;
|
||
- parsing значений;
|
||
- формирование внутреннего представления котировки;
|
||
- формирование legacy snapshot;
|
||
- обработку freshness.
|
||
|
||
Это нарушало архитектурное разделение ответственности.
|
||
|
||
К моменту начала Build 031 новый канонический Quotes Feed уже был реализован:
|
||
|
||
```text
|
||
DzengiQuoteDocumentSource
|
||
↓
|
||
DzengiQuoteDocumentHandler
|
||
↓
|
||
QuotesFeed
|
||
↓
|
||
QuoteFeedRegistry
|
||
↓
|
||
QuoteAcquisitionService
|
||
↓
|
||
Quote
|
||
```
|
||
|
||
Задачей Build 031 стало подключение этого контура под существующий `ExchangeService` facade.
|
||
|
||
---
|
||
|
||
## Объём изменений
|
||
|
||
### Изменён production-файл
|
||
|
||
```text
|
||
src/integrations/exchange/service.py
|
||
```
|
||
|
||
### Добавлен тестовый файл
|
||
|
||
```text
|
||
tests/unit/integrations/exchange/test_service_quotes_facade.py
|
||
```
|
||
|
||
### Не изменялись
|
||
|
||
```text
|
||
src/integrations/exchange/models.py
|
||
src/integrations/exchange/market_cache.py
|
||
src/integrations/exchange/mock_data.py
|
||
|
||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||
src/market_data/acquisition/feeds/quotes_feed.py
|
||
src/market_data/acquisition/handlers/quotes_handler.py
|
||
src/market_data/acquisition/models/quote.py
|
||
src/market_data/acquisition/registry.py
|
||
src/market_data/acquisition/service.py
|
||
src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
Новый Acquisition-контур уже содержал всю необходимую функциональность и не потребовал дополнительных изменений.
|
||
|
||
---
|
||
|
||
## Реализованная архитектура
|
||
|
||
После Build 031 получение свежей REST-котировки выполняется по следующей цепочке:
|
||
|
||
```text
|
||
ExchangeService.get_fresh_market_snapshot()
|
||
↓
|
||
QuoteAcquisitionService
|
||
↓
|
||
QuoteFeedRegistry
|
||
↓
|
||
QuotesFeed
|
||
↓
|
||
DzengiQuoteDocumentSource
|
||
↓
|
||
GET /api/v1/ticker/24hr
|
||
↓
|
||
DzengiQuoteDocumentHandler
|
||
↓
|
||
schema validation
|
||
↓
|
||
parser
|
||
↓
|
||
value validation
|
||
↓
|
||
mapper
|
||
↓
|
||
canonical Quote
|
||
↓
|
||
legacy-compatible snapshot dict
|
||
```
|
||
|
||
Таким образом, граница ответственности теперь выглядит следующим образом:
|
||
|
||
```text
|
||
market_data/acquisition
|
||
│
|
||
│ отвечает за получение, проверку,
|
||
│ parsing и mapping котировки
|
||
↓
|
||
canonical Quote
|
||
│
|
||
│ временная compatibility boundary
|
||
↓
|
||
ExchangeService facade
|
||
│
|
||
│ сохраняет старые публичные контракты
|
||
↓
|
||
legacy consumers
|
||
```
|
||
|
||
---
|
||
|
||
## Подключение Quote Acquisition pipeline
|
||
|
||
В `ExchangeService` добавлен внутренний путь получения канонической котировки через уже реализованные компоненты Quotes Feed.
|
||
|
||
Используемая цепочка:
|
||
|
||
```text
|
||
DzengiQuoteDocumentSource
|
||
↓
|
||
DzengiQuoteDocumentHandler
|
||
↓
|
||
QuotesFeed
|
||
↓
|
||
QuoteFeedRegistry
|
||
↓
|
||
QuoteAcquisitionService
|
||
↓
|
||
Quote
|
||
```
|
||
|
||
`ExchangeService` теперь получает готовую каноническую модель:
|
||
|
||
```python
|
||
Quote
|
||
```
|
||
|
||
вместо сырого ответа Dzengi:
|
||
|
||
```python
|
||
dict[str, object]
|
||
```
|
||
|
||
Это устраняет зависимость facade от транспортной схемы `ticker/24hr`.
|
||
|
||
---
|
||
|
||
## Изменение get_fresh_market_snapshot()
|
||
|
||
До Build 031 метод самостоятельно выполнял:
|
||
|
||
```text
|
||
создание ExchangeRestClient
|
||
↓
|
||
вызов /api/v1/ticker/24hr
|
||
↓
|
||
чтение lastPrice
|
||
↓
|
||
чтение bidPrice
|
||
↓
|
||
чтение askPrice
|
||
↓
|
||
чтение closeTime / eventTime
|
||
↓
|
||
преобразование значений
|
||
↓
|
||
формирование snapshot
|
||
```
|
||
|
||
После Build 031 метод получает:
|
||
|
||
```python
|
||
quote = self._load_quote_via_acquisition(
|
||
validation.normalized_symbol,
|
||
)
|
||
```
|
||
|
||
После чего выполняет только временную legacy-проекцию:
|
||
|
||
```text
|
||
Quote
|
||
↓
|
||
legacy-compatible dict snapshot
|
||
```
|
||
|
||
Таким образом, `get_fresh_market_snapshot()` больше не является parser транспортного ответа Dzengi.
|
||
|
||
---
|
||
|
||
## Удалённый legacy parsing
|
||
|
||
Из REST quote-пути `ExchangeService` удалено прямое знание следующих транспортных полей:
|
||
|
||
```text
|
||
lastPrice
|
||
bidPrice
|
||
askPrice
|
||
closeTime
|
||
eventTime
|
||
```
|
||
|
||
Также удалён прямой вызов:
|
||
|
||
```text
|
||
GET /api/v1/ticker/24hr
|
||
```
|
||
|
||
из `ExchangeService`.
|
||
|
||
Теперь endpoint и его транспортная схема принадлежат исключительно адаптеру:
|
||
|
||
```text
|
||
src/market_data/acquisition/adapters/dzengi/
|
||
```
|
||
|
||
Это соответствует утверждённой архитектуре Acquisition.
|
||
|
||
---
|
||
|
||
## Legacy-compatible projection
|
||
|
||
Build 031 намеренно не удаляет legacy snapshot layer.
|
||
|
||
Каноническая модель:
|
||
|
||
```python
|
||
Quote
|
||
```
|
||
|
||
временно преобразуется обратно в:
|
||
|
||
```python
|
||
dict[str, object]
|
||
```
|
||
|
||
с сохранением прежней структуры:
|
||
|
||
```python
|
||
{
|
||
"symbol": ...,
|
||
"last_price": ...,
|
||
"bid_price": ...,
|
||
"ask_price": ...,
|
||
"updated_at": ...,
|
||
"source": "fresh_rest",
|
||
"age_seconds": ...,
|
||
"is_fresh": ...,
|
||
}
|
||
```
|
||
|
||
Это необходимо для безопасной поэтапной миграции существующего работающего бота.
|
||
|
||
Удаление этого compatibility layer запланировано на:
|
||
|
||
```text
|
||
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
|
||
```
|
||
|
||
---
|
||
|
||
## Сохранение числового контракта
|
||
|
||
Каноническая модель `Quote` использует точные числовые значения, представленные через `Decimal`.
|
||
|
||
Legacy-потребители ожидают `float`.
|
||
|
||
Поэтому на временной границе совместимости выполняется преобразование:
|
||
|
||
```text
|
||
Quote Decimal
|
||
↓
|
||
ExchangeService compatibility boundary
|
||
↓
|
||
legacy float
|
||
```
|
||
|
||
То есть точность сохраняется внутри новой канонической подсистемы, а преобразование выполняется только при передаче данных старым потребителям.
|
||
|
||
Это временное решение до полного перевода потребителей на канонический `Quote`.
|
||
|
||
---
|
||
|
||
## Сохранение symbol contract
|
||
|
||
Перед получением котировки сохраняется существующая проверка символа:
|
||
|
||
```python
|
||
validation = self.validate_symbol(symbol_to_use)
|
||
```
|
||
|
||
В новый Acquisition pipeline передаётся:
|
||
|
||
```python
|
||
validation.normalized_symbol
|
||
```
|
||
|
||
Таким образом:
|
||
|
||
- невалидный символ не передаётся в Quotes Feed;
|
||
- используется канонически нормализованный символ;
|
||
- существующее поведение `ExchangeService` сохраняется.
|
||
|
||
---
|
||
|
||
## Сохранение source contract
|
||
|
||
Канонический `Quote` содержит источник Acquisition:
|
||
|
||
```text
|
||
dzengi
|
||
```
|
||
|
||
Однако существующий legacy snapshot использует:
|
||
|
||
```text
|
||
fresh_rest
|
||
```
|
||
|
||
В Build 031 сохранено прежнее значение:
|
||
|
||
```python
|
||
"source": "fresh_rest"
|
||
```
|
||
|
||
Это исключает непреднамеренное изменение поведения:
|
||
|
||
- UI;
|
||
- журналирования;
|
||
- диагностики;
|
||
- runtime;
|
||
- существующих потребителей, потенциально зависящих от значения `source`.
|
||
|
||
Переход на каноническую семантику источника должен выполняться отдельно при удалении legacy snapshot layer.
|
||
|
||
---
|
||
|
||
## Сохранение timestamp contract
|
||
|
||
Канонический `Quote` содержит timezone-aware timestamp.
|
||
|
||
На legacy-границе сохраняется прежнее представление:
|
||
|
||
```text
|
||
Quote.exchange_timestamp
|
||
↓
|
||
timestamp в миллисекундах
|
||
↓
|
||
существующие ExchangeService helpers
|
||
↓
|
||
updated_at
|
||
age_seconds
|
||
is_fresh
|
||
```
|
||
|
||
Благодаря этому существующие потребители не получают изменения временной семантики.
|
||
|
||
---
|
||
|
||
## Сохранение freshness contract
|
||
|
||
Сохранена существующая логика определения свежести REST-котировки.
|
||
|
||
Порог:
|
||
|
||
```text
|
||
60 секунд
|
||
```
|
||
|
||
Результат продолжает содержать:
|
||
|
||
```python
|
||
"age_seconds": ...
|
||
"is_fresh": ...
|
||
```
|
||
|
||
Условие остаётся эквивалентным прежнему:
|
||
|
||
```python
|
||
is_fresh = (
|
||
age_seconds is not None
|
||
and age_seconds <= 60
|
||
)
|
||
```
|
||
|
||
Build 031 не меняет политику freshness.
|
||
|
||
---
|
||
|
||
## Сохранение публичных контрактов ExchangeService
|
||
|
||
После Build 031 сохранены без изменения следующие публичные методы:
|
||
|
||
```text
|
||
get_price() -> TickerPrice
|
||
|
||
get_fresh_market_snapshot() -> dict[str, object]
|
||
|
||
refresh_price_cache() -> TickerPrice
|
||
|
||
refresh_market_snapshot_cache() -> dict[str, object]
|
||
|
||
get_market_snapshot() -> dict[str, object]
|
||
|
||
get_execution_snapshot() -> ExecutionPriceSnapshot
|
||
```
|
||
|
||
Это позволяет существующим потребителям продолжать работу без изменений.
|
||
|
||
В частности, не потребовалось изменять:
|
||
|
||
```text
|
||
src/telegram/ui/currency_ui.py
|
||
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/execution/pricing.py
|
||
src/trading/diagnostics/snapshot.py
|
||
src/trading/debug/execution.py
|
||
```
|
||
|
||
---
|
||
|
||
## Влияние на существующие методы ExchangeService
|
||
|
||
Методы:
|
||
|
||
```text
|
||
get_price()
|
||
get_market_snapshot()
|
||
get_execution_snapshot()
|
||
refresh_market_snapshot_cache()
|
||
refresh_price_cache()
|
||
_get_real_price()
|
||
get_symbol_runtime_status()
|
||
```
|
||
|
||
продолжают работать через существующие публичные и внутренние контракты.
|
||
|
||
Поскольку свежая REST-котировка теперь поступает через:
|
||
|
||
```text
|
||
get_fresh_market_snapshot()
|
||
↓
|
||
Quote Acquisition pipeline
|
||
```
|
||
|
||
существующие методы автоматически используют новый канонический REST Quotes Feed без прямого перевода каждого потребителя.
|
||
|
||
---
|
||
|
||
## Обработка ошибок
|
||
|
||
Новый Acquisition-контур использует специализированные ошибки quote-подсистемы.
|
||
|
||
Внешний контракт `ExchangeService` продолжает использовать:
|
||
|
||
```python
|
||
ExchangeError
|
||
```
|
||
|
||
Поэтому на границе facade сохраняется адаптация:
|
||
|
||
```text
|
||
Acquisition error
|
||
↓
|
||
ExchangeService compatibility boundary
|
||
↓
|
||
ExchangeError
|
||
```
|
||
|
||
При этом исходная ошибка сохраняется как:
|
||
|
||
```python
|
||
__cause__
|
||
```
|
||
|
||
Это обеспечивает одновременно:
|
||
|
||
- совместимость существующих потребителей;
|
||
- сохранение исходного контекста ошибки;
|
||
- возможность диагностики первопричины;
|
||
- отсутствие утечки новой модели исключений в legacy-код раньше запланированного этапа миграции.
|
||
|
||
---
|
||
|
||
## Сохранение mock-режима
|
||
|
||
При отключённой реальной биржевой интеграции:
|
||
|
||
```python
|
||
exchange_enabled = False
|
||
```
|
||
|
||
новый Acquisition pipeline не вызывается.
|
||
|
||
Сохраняется прежний mock-контур:
|
||
|
||
```text
|
||
ExchangeService
|
||
↓
|
||
mock_ticker_price()
|
||
↓
|
||
legacy snapshot
|
||
```
|
||
|
||
Build 031 не изменяет поведение mock-режима.
|
||
|
||
---
|
||
|
||
## Тестовое покрытие
|
||
|
||
Добавлен специализированный тестовый файл:
|
||
|
||
```text
|
||
tests/unit/integrations/exchange/test_service_quotes_facade.py
|
||
```
|
||
|
||
Тесты проверяют границу между:
|
||
|
||
```text
|
||
canonical Quote Acquisition
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
legacy ExchangeService facade
|
||
```
|
||
|
||
Проверяемые свойства включают:
|
||
|
||
- использование нового Acquisition pipeline;
|
||
- передачу нормализованного символа;
|
||
- сохранение legacy snapshot contract;
|
||
- преобразование канонических числовых значений в legacy-compatible значения;
|
||
- сохранение `source="fresh_rest"`;
|
||
- сохранение timestamp contract;
|
||
- сохранение freshness contract;
|
||
- адаптацию ошибок в `ExchangeError`;
|
||
- сохранение исходной ошибки в `__cause__`;
|
||
- отсутствие вызова нового Acquisition pipeline в mock-режиме;
|
||
- отсутствие прямого ticker REST parsing в новом facade-пути.
|
||
|
||
---
|
||
|
||
## Регрессионная проверка runtime status
|
||
|
||
Дополнительно выполнен существующий набор тестов:
|
||
|
||
```text
|
||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py
|
||
```
|
||
|
||
Он подтверждает сохранение внешнего runtime-контракта после переключения внутреннего источника REST-котировки.
|
||
|
||
---
|
||
|
||
## Выполненные проверки
|
||
|
||
### Проверка синтаксиса
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/integrations/exchange/service.py \
|
||
tests/unit/integrations/exchange/test_service_quotes_facade.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
успешно
|
||
```
|
||
|
||
### Специализированные и регрессионные тесты
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/integrations/exchange/test_service_quotes_facade.py \
|
||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
38 passed in 0.08s
|
||
```
|
||
|
||
### Полная регрессия проекта
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
502 passed in 0.25s
|
||
```
|
||
|
||
---
|
||
|
||
## Рост тестового покрытия
|
||
|
||
После предыдущего этапа:
|
||
|
||
```text
|
||
Build 030
|
||
498 passed
|
||
```
|
||
|
||
После завершения Build 031:
|
||
|
||
```text
|
||
Build 031
|
||
502 passed
|
||
```
|
||
|
||
Добавлено:
|
||
|
||
```text
|
||
4 новых теста
|
||
```
|
||
|
||
Полная регрессия остаётся зелёной.
|
||
|
||
---
|
||
|
||
## Архитектурный результат
|
||
|
||
### До Build 031
|
||
|
||
```text
|
||
ExchangeService
|
||
↓
|
||
ExchangeRestClient
|
||
↓
|
||
GET /api/v1/ticker/24hr
|
||
↓
|
||
ручной parsing полей Dzengi
|
||
↓
|
||
legacy dict snapshot
|
||
↓
|
||
потребители
|
||
```
|
||
|
||
### После Build 031
|
||
|
||
```text
|
||
ExchangeService legacy facade
|
||
↓
|
||
QuoteAcquisitionService
|
||
↓
|
||
QuoteFeedRegistry
|
||
↓
|
||
QuotesFeed
|
||
↓
|
||
DzengiQuoteDocumentSource
|
||
↓
|
||
DzengiQuoteDocumentHandler
|
||
↓
|
||
schema validation
|
||
↓
|
||
parser
|
||
↓
|
||
value validation
|
||
↓
|
||
mapper
|
||
↓
|
||
canonical Quote
|
||
↓
|
||
legacy-compatible dict projection
|
||
↓
|
||
существующие потребители
|
||
```
|
||
|
||
---
|
||
|
||
## Архитектурные гарантии после Build 031
|
||
|
||
После завершения этапа выполняются следующие гарантии:
|
||
|
||
1. `ExchangeService` больше не выполняет прямой REST-запрос к `/api/v1/ticker/24hr`.
|
||
|
||
2. `ExchangeService` больше не знает транспортные поля:
|
||
|
||
```text
|
||
lastPrice
|
||
bidPrice
|
||
askPrice
|
||
closeTime
|
||
eventTime
|
||
```
|
||
|
||
3. REST-котировка проходит через канонический Quotes Feed.
|
||
|
||
4. Внутренним результатом Acquisition является:
|
||
|
||
```python
|
||
Quote
|
||
```
|
||
|
||
5. Legacy snapshot создаётся только как временная compatibility projection.
|
||
|
||
6. Существующие публичные контракты `ExchangeService` сохранены.
|
||
|
||
7. `MarketPriceCache` пока не изменён.
|
||
|
||
8. WebSocket-контур пока не изменён.
|
||
|
||
9. UI-потребители пока не переведены напрямую на `Quote`.
|
||
|
||
10. Execution-потребители пока не переведены напрямую на `Quote`.
|
||
|
||
11. Полная регрессия проекта проходит успешно:
|
||
|
||
```text
|
||
502 passed
|
||
```
|
||
|
||
---
|
||
|
||
## Что намеренно не входит в Build 031
|
||
|
||
Build 031 не реализует:
|
||
|
||
```text
|
||
Quote Store
|
||
перенос MarketPriceCache
|
||
удаление MarketPriceCache
|
||
изменение MarketPriceSnapshot
|
||
WebSocket quote parsing
|
||
WebSocket quote adapter
|
||
перевод market runtime на Quotes Feed
|
||
прямой перевод UI на Quote
|
||
прямой перевод execution на Quote
|
||
удаление TickerPrice
|
||
удаление ExecutionPriceSnapshot
|
||
удаление legacy market snapshot dict layer
|
||
удаление legacy quote compatibility layer
|
||
```
|
||
|
||
Эти изменения выполняются последующими Build по утверждённому плану.
|
||
|
||
---
|
||
|
||
## Следующий этап
|
||
|
||
```text
|
||
Build 032 — Канонический Quote Store
|
||
```
|
||
|
||
Его задача — создать канонический слой хранения текущих котировок, который станет основой для последующего переноса существующего:
|
||
|
||
```text
|
||
MarketPriceCache
|
||
```
|
||
|
||
на новую архитектуру.
|
||
|
||
Последовательность дальнейшей миграции:
|
||
|
||
```text
|
||
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 031 завершён полностью.**
|
||
|
||
Новый канонический REST Quotes Feed стал внутренним источником свежих котировок для `ExchangeService`, при этом существующий работающий бот сохранил прежние публичные контракты.
|
||
|
||
Ключевой результат:
|
||
|
||
```text
|
||
Было:
|
||
|
||
ExchangeService
|
||
↓
|
||
прямой REST ticker/24hr
|
||
↓
|
||
ручной parsing
|
||
↓
|
||
legacy snapshot
|
||
```
|
||
|
||
```text
|
||
Стало:
|
||
|
||
ExchangeService facade
|
||
↓
|
||
canonical Quotes Feed
|
||
↓
|
||
Quote
|
||
↓
|
||
временная legacy projection
|
||
↓
|
||
существующие потребители
|
||
```
|
||
|
||
Это создаёт безопасную архитектурную основу для следующего этапа:
|
||
|
||
```text
|
||
Build 032 — Канонический Quote Store
|
||
``` |