build 059.5: add internal candle close event model
This commit is contained in:
29
app/src/market_data/acquisition/models/candle_close.py
Normal file
29
app/src/market_data/acquisition/models/candle_close.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# app/src/market_data/acquisition/models/candle_close.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
# Внутреннее неизменяемое уведомление о закрытии OHLC-свечи.
|
||||
#
|
||||
# Модель не является канонической Candle, поскольку WebSocket-событие
|
||||
# не содержит volume. Она используется как сигнал для последующего
|
||||
# REST reconciliation и получения полной OHLCV-свечи.
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CandleCloseEvent:
|
||||
symbol: str
|
||||
interval: str
|
||||
candle_type: str
|
||||
|
||||
open_time: datetime
|
||||
received_at: datetime
|
||||
|
||||
open_price: Decimal
|
||||
high_price: Decimal
|
||||
low_price: Decimal
|
||||
close_price: Decimal
|
||||
|
||||
source: str
|
||||
@@ -0,0 +1,142 @@
|
||||
# app/tests/unit/market_data/acquisition/models/test_candle_close.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import FrozenInstanceError
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
from src.market_data.acquisition.models.candle_close import (
|
||||
CandleCloseEvent,
|
||||
)
|
||||
|
||||
|
||||
def _event() -> CandleCloseEvent:
|
||||
return CandleCloseEvent(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
interval="1m",
|
||||
candle_type="classic",
|
||||
open_time=datetime(
|
||||
2026,
|
||||
7,
|
||||
16,
|
||||
18,
|
||||
39,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
received_at=datetime(
|
||||
2026,
|
||||
7,
|
||||
16,
|
||||
18,
|
||||
40,
|
||||
0,
|
||||
125000,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
open_price=Decimal("63992.00"),
|
||||
high_price=Decimal("64032.55"),
|
||||
low_price=Decimal("63984.00"),
|
||||
close_price=Decimal("64032.55"),
|
||||
source="dzengi_websocket_ohlc",
|
||||
)
|
||||
|
||||
|
||||
def test_candle_close_event_preserves_all_fields() -> None:
|
||||
event = _event()
|
||||
|
||||
assert event.symbol == "BTC/USD_LEVERAGE"
|
||||
assert event.interval == "1m"
|
||||
assert event.candle_type == "classic"
|
||||
|
||||
assert event.open_time == datetime(
|
||||
2026,
|
||||
7,
|
||||
16,
|
||||
18,
|
||||
39,
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
assert event.received_at == datetime(
|
||||
2026,
|
||||
7,
|
||||
16,
|
||||
18,
|
||||
40,
|
||||
0,
|
||||
125000,
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
|
||||
assert event.open_price == Decimal("63992.00")
|
||||
assert event.high_price == Decimal("64032.55")
|
||||
assert event.low_price == Decimal("63984.00")
|
||||
assert event.close_price == Decimal("64032.55")
|
||||
|
||||
assert event.source == "dzengi_websocket_ohlc"
|
||||
|
||||
|
||||
def test_candle_close_event_is_immutable() -> None:
|
||||
event = _event()
|
||||
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
event.close_price = Decimal("1") # type: ignore[misc]
|
||||
|
||||
|
||||
def test_candle_close_event_uses_slots() -> None:
|
||||
event = _event()
|
||||
|
||||
assert not hasattr(event, "__dict__")
|
||||
|
||||
|
||||
def test_candle_close_event_is_not_canonical_candle() -> None:
|
||||
event = _event()
|
||||
|
||||
assert not isinstance(event, Candle)
|
||||
|
||||
|
||||
def test_candle_close_event_has_no_volume_field() -> None:
|
||||
event = _event()
|
||||
|
||||
assert not hasattr(event, "volume")
|
||||
|
||||
|
||||
def test_candle_close_event_supports_heikin_ashi_type() -> None:
|
||||
event = CandleCloseEvent(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
interval="1m",
|
||||
candle_type="heikin-ashi",
|
||||
open_time=datetime(
|
||||
2026,
|
||||
7,
|
||||
16,
|
||||
18,
|
||||
39,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
received_at=datetime(
|
||||
2026,
|
||||
7,
|
||||
16,
|
||||
18,
|
||||
40,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
open_price=Decimal("64068.18"),
|
||||
high_price=Decimal("64128.80"),
|
||||
low_price=Decimal("64068.18"),
|
||||
close_price=Decimal("64100.85"),
|
||||
source="dzengi_websocket_ohlc",
|
||||
)
|
||||
|
||||
assert event.candle_type == "heikin-ashi"
|
||||
|
||||
|
||||
def test_candle_close_event_keeps_open_and_receive_times_separate() -> None:
|
||||
event = _event()
|
||||
|
||||
assert event.received_at > event.open_time
|
||||
assert event.received_at != event.open_time
|
||||
555
docs/migrations/build_059_5.md
Normal file
555
docs/migrations/build_059_5.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Build 059.5 — Internal Candle Close Event Model
|
||||
|
||||
**Проект:** Dzentra
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Этап:** 059.5
|
||||
**Статус:** Completed
|
||||
|
||||
---
|
||||
|
||||
# Цель
|
||||
|
||||
Добавить внутреннюю immutable-модель уведомления о закрытии OHLC-свечи.
|
||||
|
||||
Модель должна представлять уже проверенное WebSocket-событие внутри Dzentra, но не должна заменять каноническую модель:
|
||||
|
||||
```text
|
||||
Candle
|
||||
```
|
||||
|
||||
Причина разделения заключается в том, что фактическое событие Dzengi WebSocket:
|
||||
|
||||
```text
|
||||
ohlc.event
|
||||
```
|
||||
|
||||
содержит:
|
||||
|
||||
```text
|
||||
symbol
|
||||
interval
|
||||
type
|
||||
t
|
||||
o
|
||||
h
|
||||
l
|
||||
c
|
||||
```
|
||||
|
||||
но не содержит:
|
||||
|
||||
```text
|
||||
volume
|
||||
```
|
||||
|
||||
Поэтому WebSocket OHLC-событие не является полноценной OHLCV-свечой.
|
||||
|
||||
---
|
||||
|
||||
# Причина изменения
|
||||
|
||||
После Build 059.4 проект умеет:
|
||||
|
||||
1. структурно проверять входящее WebSocket-сообщение;
|
||||
2. преобразовывать его в transport-модель;
|
||||
3. проверять предметную допустимость значений.
|
||||
|
||||
Подготовленная цепочка выглядит так:
|
||||
|
||||
```text
|
||||
Raw WebSocket message
|
||||
↓
|
||||
ValidatedWebSocketOhlcDocument
|
||||
↓
|
||||
DzengiWebSocketOhlcEvent
|
||||
↓
|
||||
Value Validation
|
||||
```
|
||||
|
||||
Однако transport-модель:
|
||||
|
||||
```text
|
||||
DzengiWebSocketOhlcEvent
|
||||
```
|
||||
|
||||
принадлежит адаптеру конкретного внешнего источника Dzengi.
|
||||
|
||||
Перед дальнейшим использованием в runtime требуется источник-независимая внутренняя модель Dzentra.
|
||||
|
||||
---
|
||||
|
||||
# Реализовано
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/candle_close.py
|
||||
```
|
||||
|
||||
Добавлена модель:
|
||||
|
||||
```text
|
||||
CandleCloseEvent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Структура модели
|
||||
|
||||
Модель содержит следующие поля:
|
||||
|
||||
```text
|
||||
symbol
|
||||
interval
|
||||
candle_type
|
||||
|
||||
open_time
|
||||
received_at
|
||||
|
||||
open_price
|
||||
high_price
|
||||
low_price
|
||||
close_price
|
||||
|
||||
source
|
||||
```
|
||||
|
||||
Полное назначение модели:
|
||||
|
||||
```text
|
||||
внутреннее уведомление Dzentra
|
||||
о завершении OHLC-свечи,
|
||||
требующее последующего REST reconciliation
|
||||
для получения полной OHLCV-свечи
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Контракт модели
|
||||
|
||||
Модель объявлена как:
|
||||
|
||||
```text
|
||||
frozen=True
|
||||
slots=True
|
||||
```
|
||||
|
||||
Это обеспечивает:
|
||||
|
||||
- неизменяемость;
|
||||
- отсутствие динамического `__dict__`;
|
||||
- уменьшение вероятности случайного изменения события;
|
||||
- компактное представление в памяти;
|
||||
- предсказуемый runtime-контракт.
|
||||
|
||||
---
|
||||
|
||||
# Отличие от transport-модели
|
||||
|
||||
Transport-модель:
|
||||
|
||||
```text
|
||||
DzengiWebSocketOhlcEvent
|
||||
```
|
||||
|
||||
отражает внешний контракт Dzengi.
|
||||
|
||||
Она содержит:
|
||||
|
||||
```text
|
||||
open_time: int
|
||||
OHLC: str | int | float
|
||||
```
|
||||
|
||||
Внутренняя модель:
|
||||
|
||||
```text
|
||||
CandleCloseEvent
|
||||
```
|
||||
|
||||
содержит:
|
||||
|
||||
```text
|
||||
open_time: datetime
|
||||
received_at: datetime
|
||||
OHLC: Decimal
|
||||
source: str
|
||||
```
|
||||
|
||||
Таким образом transport-особенности внешнего API не распространяются на внутренние слои Dzentra.
|
||||
|
||||
---
|
||||
|
||||
# Отличие от канонической Candle
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```text
|
||||
Candle
|
||||
```
|
||||
|
||||
представляет полноценную OHLCV-свечу.
|
||||
|
||||
Она содержит:
|
||||
|
||||
```text
|
||||
volume: Decimal
|
||||
```
|
||||
|
||||
Модель:
|
||||
|
||||
```text
|
||||
CandleCloseEvent
|
||||
```
|
||||
|
||||
не содержит `volume`.
|
||||
|
||||
Это преднамеренное архитектурное решение.
|
||||
|
||||
WebSocket-событие не должно:
|
||||
|
||||
- подставлять фиктивный нулевой объём;
|
||||
- использовать nullable volume;
|
||||
- притворяться полной OHLCV-свечой;
|
||||
- наследоваться от `Candle`;
|
||||
- передаваться потребителям, ожидающим каноническую свечу.
|
||||
|
||||
Полная `Candle` будет получена только после REST reconciliation.
|
||||
|
||||
---
|
||||
|
||||
# Поля времени
|
||||
|
||||
## open_time
|
||||
|
||||
Поле:
|
||||
|
||||
```text
|
||||
open_time
|
||||
```
|
||||
|
||||
представляет время открытия завершённой свечи.
|
||||
|
||||
Для минутной свечи это начало минутного интервала, к которому относится событие.
|
||||
|
||||
Тип:
|
||||
|
||||
```text
|
||||
datetime
|
||||
```
|
||||
|
||||
Ожидаемая timezone:
|
||||
|
||||
```text
|
||||
UTC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## received_at
|
||||
|
||||
Поле:
|
||||
|
||||
```text
|
||||
received_at
|
||||
```
|
||||
|
||||
представляет момент получения события системой Dzentra.
|
||||
|
||||
Оно не равно `open_time`.
|
||||
|
||||
Разделение двух времён необходимо для:
|
||||
|
||||
- измерения задержки WebSocket;
|
||||
- диагностики запоздавших сообщений;
|
||||
- определения порядка фактической обработки;
|
||||
- runtime-метрик;
|
||||
- журналирования;
|
||||
- анализа качества источника данных.
|
||||
|
||||
---
|
||||
|
||||
# Поля OHLC
|
||||
|
||||
Внутренняя модель использует:
|
||||
|
||||
```text
|
||||
Decimal
|
||||
```
|
||||
|
||||
для:
|
||||
|
||||
```text
|
||||
open_price
|
||||
high_price
|
||||
low_price
|
||||
close_price
|
||||
```
|
||||
|
||||
Это исключает дальнейшее распространение внешних типов:
|
||||
|
||||
```text
|
||||
str
|
||||
int
|
||||
float
|
||||
```
|
||||
|
||||
Преобразование transport-значений в `Decimal` будет реализовано в следующем этапе:
|
||||
|
||||
```text
|
||||
Build 059.6 — Mapper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Поле candle_type
|
||||
|
||||
Поле:
|
||||
|
||||
```text
|
||||
candle_type
|
||||
```
|
||||
|
||||
сохраняет тип серии свечей.
|
||||
|
||||
Подтверждённые runtime-значения:
|
||||
|
||||
```text
|
||||
classic
|
||||
heikin-ashi
|
||||
```
|
||||
|
||||
Для production-интеграции Candles Feed основной интерес представляет:
|
||||
|
||||
```text
|
||||
classic
|
||||
```
|
||||
|
||||
Поскольку именно классические свечи будут использоваться для REST reconciliation с каноническими OHLCV-свечами.
|
||||
|
||||
`heikin-ashi` остаётся поддерживаемым внутренним типом события, но не должен автоматически преобразовываться в обычную каноническую `Candle`.
|
||||
|
||||
---
|
||||
|
||||
# Поле source
|
||||
|
||||
Поле:
|
||||
|
||||
```text
|
||||
source
|
||||
```
|
||||
|
||||
фиксирует происхождение внутреннего события.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
dzengi_websocket_ohlc
|
||||
```
|
||||
|
||||
Это позволит в дальнейшем:
|
||||
|
||||
- различать источники;
|
||||
- поддерживать несколько адаптеров;
|
||||
- вести диагностику;
|
||||
- применять источник-зависимую политику reconciliation;
|
||||
- не встраивать имя Dzengi в общую доменную модель.
|
||||
|
||||
---
|
||||
|
||||
# Новый внутренний конвейер
|
||||
|
||||
После Build 059.5 архитектура выглядит так:
|
||||
|
||||
```text
|
||||
Raw WebSocket message
|
||||
↓
|
||||
Schema Validation
|
||||
↓
|
||||
ValidatedWebSocketOhlcDocument
|
||||
↓
|
||||
Parser
|
||||
↓
|
||||
DzengiWebSocketOhlcEvent
|
||||
↓
|
||||
Value Validation
|
||||
↓
|
||||
Mapper
|
||||
↓
|
||||
CandleCloseEvent
|
||||
↓
|
||||
REST reconciliation
|
||||
↓
|
||||
Canonical Candle
|
||||
```
|
||||
|
||||
На текущем этапе модель `CandleCloseEvent` уже существует, но mapper ещё не реализован.
|
||||
|
||||
---
|
||||
|
||||
# Unit Tests
|
||||
|
||||
Создан файл:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/models/test_candle_close.py
|
||||
```
|
||||
|
||||
Проверяются:
|
||||
|
||||
- сохранение всех полей;
|
||||
- неизменяемость модели;
|
||||
- использование `slots`;
|
||||
- отсутствие `__dict__`;
|
||||
- отсутствие наследования от `Candle`;
|
||||
- отсутствие поля `volume`;
|
||||
- поддержка типа `heikin-ashi`;
|
||||
- раздельное хранение `open_time` и `received_at`.
|
||||
|
||||
Всего выполнено:
|
||||
|
||||
```text
|
||||
7 tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Проверка синтаксиса
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m compileall \
|
||||
src/market_data/acquisition/models/candle_close.py \
|
||||
tests/unit/market_data/acquisition/models/test_candle_close.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
успешно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Целевые тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q \
|
||||
tests/unit/market_data/acquisition/models/test_candle_close.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
7 passed in 0.01s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Регрессия слоя моделей
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q \
|
||||
tests/unit/market_data/acquisition/models
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
42 passed in 0.02s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Проверка форматирования
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Ошибок форматирования не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
# Созданные файлы
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/candle_close.py
|
||||
tests/unit/market_data/acquisition/models/test_candle_close.py
|
||||
docs/migrations/build_059_5.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Совместимость
|
||||
|
||||
Изменение полностью обратно совместимо.
|
||||
|
||||
Не изменены:
|
||||
|
||||
- каноническая модель `Candle`;
|
||||
- transport-модель Dzengi;
|
||||
- REST Candles Feed;
|
||||
- parser;
|
||||
- value validation;
|
||||
- WebSocket Adapter;
|
||||
- ExchangeService;
|
||||
- runtime;
|
||||
- Market Analysis;
|
||||
- Trading.
|
||||
|
||||
Новая модель пока не подключена к runtime и не влияет на работающий бот.
|
||||
|
||||
---
|
||||
|
||||
# Ограничения этапа
|
||||
|
||||
Build 059.5 не выполняет:
|
||||
|
||||
- преобразование transport-модели;
|
||||
- преобразование timestamp;
|
||||
- преобразование OHLC в `Decimal`;
|
||||
- установку реального `received_at`;
|
||||
- mapping;
|
||||
- WebSocket Adapter;
|
||||
- REST reconciliation;
|
||||
- проверку соответствия REST и WebSocket OHLC;
|
||||
- создание канонической `Candle`;
|
||||
- публикацию события в runtime.
|
||||
|
||||
---
|
||||
|
||||
# Итог
|
||||
|
||||
Build 059.5 добавляет источник-независимую внутреннюю модель:
|
||||
|
||||
```text
|
||||
CandleCloseEvent
|
||||
```
|
||||
|
||||
Она представляет подтверждённое уведомление о завершении OHLC-свечи без фиктивного объёма.
|
||||
|
||||
Модель создаёт архитектурную границу между:
|
||||
|
||||
```text
|
||||
внешним Dzengi WebSocket контрактом
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
внутренним runtime Dzentra
|
||||
```
|
||||
|
||||
Следующий этап:
|
||||
|
||||
```text
|
||||
Build 059.6 — Mapper
|
||||
```
|
||||
Reference in New Issue
Block a user