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
|
||||
Reference in New Issue
Block a user