build 059.11: add runtime command models

This commit is contained in:
2026-07-17 19:40:59 +03:00
parent 4977c7678a
commit d2d8f8fad4
3 changed files with 759 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
# app/src/market_data/acquisition/runtime/runtime_commands.py
from __future__ import annotations
from dataclasses import dataclass
from src.market_data.acquisition.runtime.transport_messages import (
TransportBinaryMessage,
TransportTextMessage,
)
TransportMessage = TransportTextMessage | TransportBinaryMessage
@dataclass(frozen=True, slots=True)
class ConnectCommand:
"""
Команда на открытие транспортного соединения.
Команда выражает намерение Runtime и не выполняет подключение
самостоятельно.
"""
@dataclass(frozen=True, slots=True)
class DisconnectCommand:
"""
Команда на закрытие транспортного соединения.
Команда не содержит сетевой логики и не управляет транспортом
самостоятельно.
"""
@dataclass(frozen=True, slots=True)
class SubscribeCommand:
"""
Команда на регистрацию и отправку транспортной подписки.
subscription_key является стабильным идентификатором подписки,
используемым будущим Subscription Manager.
message содержит уже подготовленный транспортный payload.
Runtime не интерпретирует его содержимое.
"""
subscription_key: str
message: TransportMessage
@dataclass(frozen=True, slots=True)
class UnsubscribeCommand:
"""
Команда на удаление и отправку отмены транспортной подписки.
subscription_key связывает отмену с ранее зарегистрированной
подпиской.
message содержит уже подготовленный транспортный payload.
"""
subscription_key: str
message: TransportMessage
@dataclass(frozen=True, slots=True)
class SendTextCommand:
"""
Команда на отправку текстового транспортного сообщения.
"""
message: TransportTextMessage
@dataclass(frozen=True, slots=True)
class SendBinaryCommand:
"""
Команда на отправку бинарного транспортного сообщения.
"""
message: TransportBinaryMessage

View File

@@ -0,0 +1,97 @@
# app/tests/unit/market_data/acquisition/runtime/test_runtime_commands.py
from __future__ import annotations
from dataclasses import FrozenInstanceError
import pytest
from src.market_data.acquisition.runtime.runtime_commands import (
ConnectCommand,
DisconnectCommand,
SendBinaryCommand,
SendTextCommand,
SubscribeCommand,
UnsubscribeCommand,
)
from src.market_data.acquisition.runtime.transport_messages import (
TransportBinaryMessage,
TransportTextMessage,
)
def test_connect_command_is_marker_command() -> None:
assert ConnectCommand() == ConnectCommand()
def test_disconnect_command_is_marker_command() -> None:
assert DisconnectCommand() == DisconnectCommand()
def test_subscribe_command_contains_subscription_key_and_text_message() -> None:
message = TransportTextMessage(payload='{"action":"subscribe"}')
command = SubscribeCommand(
subscription_key="quotes:BTCUSDT",
message=message,
)
assert command.subscription_key == "quotes:BTCUSDT"
assert command.message is message
def test_subscribe_command_supports_binary_message() -> None:
message = TransportBinaryMessage(payload=b"\x01\x02")
command = SubscribeCommand(
subscription_key="binary:market-data",
message=message,
)
assert command.message is message
def test_unsubscribe_command_contains_subscription_key_and_message() -> None:
message = TransportTextMessage(payload='{"action":"unsubscribe"}')
command = UnsubscribeCommand(
subscription_key="quotes:BTCUSDT",
message=message,
)
assert command.subscription_key == "quotes:BTCUSDT"
assert command.message is message
def test_send_text_command_contains_transport_text_message() -> None:
message = TransportTextMessage(payload="ping")
command = SendTextCommand(message=message)
assert command.message is message
def test_send_binary_command_contains_transport_binary_message() -> None:
message = TransportBinaryMessage(payload=b"\x00")
command = SendBinaryCommand(message=message)
assert command.message is message
def test_subscribe_command_is_immutable() -> None:
command = SubscribeCommand(
subscription_key="quotes:BTCUSDT",
message=TransportTextMessage(payload="subscribe"),
)
with pytest.raises(FrozenInstanceError):
setattr(command, "subscription_key", "quotes:ETHUSDT")
def test_send_text_command_is_immutable() -> None:
command = SendTextCommand(
message=TransportTextMessage(payload="ping"),
)
with pytest.raises(FrozenInstanceError):
setattr(
command,
"message",
TransportTextMessage(payload="pong"),
)

View File

@@ -0,0 +1,580 @@
# Build 059.11 — Runtime Commands
**Migration Build**
---
# Цель Build
После завершения Build 059.10 Runtime уже содержит два фундаментальных уровня:
- Runtime Protocols
- Transport Message Models
Следующим шагом является появление объектов, описывающих **намерения Runtime**.
Данный Build вводит отдельный слой **Runtime Commands**.
Команды описывают **что необходимо сделать**, но не содержат логики выполнения.
---
# Почему понадобился отдельный слой Commands
До настоящего момента Runtime умел описывать:
- транспортные интерфейсы;
- транспортные сообщения.
Однако отсутствовало понятие действия.
Например:
- открыть соединение;
- закрыть соединение;
- зарегистрировать подписку;
- отправить сообщение.
Все эти действия являются командами Runtime.
Они не являются:
- транспортными сообщениями;
- событиями Runtime;
- сетевыми операциями.
Поэтому они выделяются в самостоятельный уровень архитектуры.
---
# Архитектурная идея
Runtime начинает строиться по принципу Command-driven Architecture.
Получается следующая цепочка.
```text
Runtime Service
Runtime Command
Transport
Transport Message
Network
```
Таким образом команды описывают исключительно намерение.
Исполнение намерения остаётся обязанностью Session и Transport.
---
# Новый модуль
Создан файл
```
src/market_data/acquisition/runtime/runtime_commands.py
```
В нём определены immutable-модели Runtime-команд.
---
# Использование Transport Messages
Все команды, связанные с передачей данных, используют модели Build 059.10.
То есть Runtime продолжает работать только с двумя видами транспортных сообщений:
```
TransportTextMessage
TransportBinaryMessage
```
Никакие команды не работают непосредственно со строками или массивами байтов.
Это обеспечивает единый транспортный контракт Runtime.
---
# TransportMessage
В модуле введён локальный type alias.
```python
TransportMessage =
TransportTextMessage
| TransportBinaryMessage
```
Он используется исключительно для типизации.
Никакой новой Runtime-модели не создаётся.
Данный alias позволяет избежать дублирования кода в командах подписки.
---
# Новые команды
В Build добавлены шесть Runtime-команд.
---
## ConnectCommand
Представляет намерение открыть транспортное соединение.
Команда:
- не содержит URL;
- не содержит параметры подключения;
- не выполняет подключение самостоятельно.
Она лишь сообщает Runtime:
> необходимо открыть соединение.
---
## DisconnectCommand
Представляет намерение завершить соединение.
Команда не управляет транспортом.
Она только описывает действие Runtime.
---
## Почему ConnectCommand не содержит URL
Во время проектирования рассматривались варианты добавить:
```
url
headers
timeout
ssl
authentication
```
От данной идеи было принято решение отказаться.
Причины следующие.
---
### Причина №1
URL является частью конфигурации транспорта.
Он не должен передаваться вместе с каждой командой.
---
### Причина №2
Команда должна описывать действие.
Не настройки транспорта.
---
### Причина №3
Подобная архитектура позволяет легко заменить транспорт без изменения Runtime-команд.
---
# SubscribeCommand
Представляет команду регистрации новой подписки Runtime.
Содержит два поля.
```
subscription_key
message
```
---
## subscription_key
Строковый идентификатор подписки.
На текущем этапе Runtime он нигде не интерпретируется.
Однако именно он станет основным ключом будущего Subscription Manager.
Например:
```
quotes:BTCUSDT
```
или
```
candles:ETHUSDT:1m
```
Формат идентификатора пока намеренно не стандартизируется.
Это будет сделано позднее после проектирования Subscription Manager.
---
## message
Содержит транспортное сообщение.
Тип поля:
```
TransportMessage
```
То есть подписка может использовать:
- текстовое сообщение;
- бинарное сообщение.
Runtime не анализирует содержимое payload.
---
# Почему SubscribeCommand хранит готовое сообщение
Рассматривались два варианта.
---
## Вариант 1
Хранить параметры подписки.
Например
```
symbol
interval
channel
```
---
## Вариант 2
Хранить уже подготовленное транспортное сообщение.
Был выбран второй вариант.
Причины.
---
### Runtime не знает протокол биржи
Построением JSON занимается слой выше.
Runtime не должен знать:
- Binance;
- Bybit;
- Dzengi;
- Kraken;
- OKX.
---
### Runtime отвечает только за доставку
Transport получает уже готовое сообщение.
Как оно было сформировано — его не касается.
---
# UnsubscribeCommand
Представляет команду отмены существующей подписки.
Содержит:
```
subscription_key
message
```
По архитектуре полностью симметрична SubscribeCommand.
Это позволит Subscription Manager использовать единый механизм регистрации и удаления подписок.
---
# SendTextCommand
Представляет команду отправки текстового транспортного сообщения.
Содержит
```
TransportTextMessage
```
Никаких дополнительных параметров команда не имеет.
---
# SendBinaryCommand
Представляет команду отправки бинарного транспортного сообщения.
Содержит
```
TransportBinaryMessage
```
Runtime рассматривает его исключительно как транспортный payload.
---
# Почему отсутствует базовый класс Command
Во время проектирования обсуждались варианты:
```
BaseCommand
```
или
```
RuntimeCommand
```
Было принято решение отказаться.
Причины.
---
## Причина №1
Все команды являются immutable dataclass.
Общего поведения между ними нет.
---
## Причина №2
Преждевременная иерархия усложняет архитектуру.
Добавлять её без появления реальной общей логики нецелесообразно.
---
## Причина №3
При необходимости общий базовый тип может быть введён отдельным Build без нарушения обратной совместимости.
---
# Почему отсутствует CommandType
Также обсуждалось использование enum.
Например.
```
CONNECT
DISCONNECT
SUBSCRIBE
```
От идеи отказались.
Тип команды уже определяется её классом.
Дополнительный enum создавал бы дублирование информации.
---
# Почему команды ничего не делают
Очень важный архитектурный принцип Runtime.
Команды являются моделями данных.
Они:
- ничего не выполняют;
- не взаимодействуют с сетью;
- не содержат asyncio;
- не вызывают Transport;
- не знают Session.
Это соответствует принципу Command Pattern.
---
# Что появится позже
Исполнением команд будут заниматься Runtime Services.
Например.
```text
Session
получает
ConnectCommand
вызывает
Transport.connect()
```
Таким образом команда остаётся полностью независимой от реализации транспорта.
---
# Обновлённая архитектура Runtime
После завершения Build Runtime выглядит следующим образом.
```text
Runtime
Protocols
Transport Messages
Runtime Commands
Transport
Network
```
Следующим Build между Commands и Transport появятся Runtime Events.
---
# Что НЕ входит в Build
Сознательно не реализованы:
- Session;
- Command Dispatcher;
- очередь команд;
- asyncio;
- обработчики команд;
- сериализация;
- JSON;
- Subscription Manager;
- Reconnect;
- Heartbeat;
- Ping/Pong.
Все эти элементы будут появляться постепенно отдельными Build.
---
# Проверка Build
Выполнена полная проверка.
---
## Компиляция
```
python -m compileall
```
Успешно.
---
## Unit Tests
Созданы тесты:
```
test_runtime_commands.py
```
Проверяется:
- создание каждой команды;
- корректность хранения TransportMessage;
- поддержка текстовых сообщений;
- поддержка бинарных сообщений;
- immutable-поведение dataclass.
Все тесты успешно пройдены.
---
## Runtime Regression
Проверены совместно:
- Runtime Protocols;
- Transport Messages;
- Runtime Commands.
Все Runtime-тесты успешно завершены.
---
## Проверка репозитория
Выполнен
```
git diff --check
```
Ошибок форматирования не обнаружено.
---
# Итог
Build 059.11 завершает формирование слоя Runtime Commands.
Теперь Runtime имеет три независимых уровня:
```text
Protocols
Transport Messages
Runtime Commands
```
Каждый уровень отвечает только за собственную область ответственности.
Это создаёт устойчивый фундамент для следующих Build, в которых будут реализованы Runtime Events, Subscription Manager, Session и механизмы обеспечения надёжности соединения.
Главный принцип миграции Dzentra остаётся неизменным:
- архитектура развивается небольшими независимыми шагами;
- каждый Build вводит только один новый уровень ответственности;
- существующий торговый бот продолжает работать без необходимости полной одномоментной миграции на новую Runtime-подсистему.