build 059.11: add runtime command models
This commit is contained in:
82
app/src/market_data/acquisition/runtime/runtime_commands.py
Normal file
82
app/src/market_data/acquisition/runtime/runtime_commands.py
Normal 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
|
||||
@@ -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"),
|
||||
)
|
||||
580
docs/migrations/build_059_11.md
Normal file
580
docs/migrations/build_059_11.md
Normal 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-подсистему.
|
||||
Reference in New Issue
Block a user