PINK DITAv2: Hz writes + vol_ok gate + leverage logging + 8 new tests (94/94 green)
This commit is contained in:
@@ -1252,3 +1252,143 @@ class TestW10HttpErrorMapping:
|
||||
|
||||
def test_dns_error_is_rate_limited(self):
|
||||
assert self._status("Name or service not known") == "RATE_LIMITED"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PinkHzStateWriter: non-blocking Hz write correctness
|
||||
# ============================================================
|
||||
|
||||
class TestPinkHzStateWriter:
|
||||
"""PinkHzStateWriter: payload shape, vol_ok gate, and non-blocking guarantees."""
|
||||
|
||||
def _make_writer_no_hz(self):
|
||||
"""Build a PinkHzStateWriter with a mock client that captures writes."""
|
||||
from prod.clean_arch.dita_v2.hazelcast_projection import PinkHzStateWriter
|
||||
import unittest.mock as mock
|
||||
|
||||
w = object.__new__(PinkHzStateWriter)
|
||||
w._writes = {} # {(map_attr, key): value}
|
||||
|
||||
# Build fake non-blocking IMap proxy
|
||||
def _make_map(name):
|
||||
m = mock.MagicMock(name=f"map:{name}")
|
||||
def _put(key, value):
|
||||
w._writes[(name, key)] = value
|
||||
m.put.side_effect = _put
|
||||
return m
|
||||
|
||||
w._state_map = _make_map("DOLPHIN_STATE_PINK")
|
||||
w._pnl_map = _make_map("DOLPHIN_PNL_PINK")
|
||||
w._client = mock.MagicMock()
|
||||
return w
|
||||
|
||||
def test_engine_snapshot_writes_two_keys(self):
|
||||
w = self._make_writer_no_hz()
|
||||
w.write_engine_snapshot(
|
||||
{"slot_id": 0, "fsm_state": "IDLE"},
|
||||
{"capital": 25000.0, "trade_seq": 42},
|
||||
posture="APEX",
|
||||
)
|
||||
assert ("DOLPHIN_STATE_PINK", "engine_snapshot") in w._writes, (
|
||||
"PinkHzStateWriter must write engine_snapshot key"
|
||||
)
|
||||
assert ("DOLPHIN_STATE_PINK", "latest") in w._writes, (
|
||||
"PinkHzStateWriter must write latest key (BLUE-compatible)"
|
||||
)
|
||||
|
||||
def test_engine_snapshot_has_strategy_pink(self):
|
||||
import json
|
||||
w = self._make_writer_no_hz()
|
||||
w.write_engine_snapshot({"slot_id": 0}, {"capital": 10000.0})
|
||||
snap = json.loads(w._writes[("DOLPHIN_STATE_PINK", "engine_snapshot")])
|
||||
assert snap["strategy"] == "pink", "engine_snapshot must identify as pink"
|
||||
|
||||
def test_latest_key_has_blue_compatible_fields(self):
|
||||
import json
|
||||
w = self._make_writer_no_hz()
|
||||
w.write_engine_snapshot({"slot_id": 0}, {"capital": 5000.0, "realized_pnl_total": 123.4, "trade_seq": 7})
|
||||
latest = json.loads(w._writes[("DOLPHIN_STATE_PINK", "latest")])
|
||||
for field in ("strategy", "capital", "date", "pnl", "trades", "posture", "updated_at"):
|
||||
assert field in latest, f"BLUE-compatible 'latest' key missing field: {field}"
|
||||
|
||||
def test_our_leverage_in_snapshot(self):
|
||||
import json
|
||||
w = self._make_writer_no_hz()
|
||||
w.write_engine_snapshot(
|
||||
{"slot_id": 0, "size": 0.5, "entry_price": 50000.0},
|
||||
{"capital": 25000.0},
|
||||
our_leverage=1.0,
|
||||
)
|
||||
snap = json.loads(w._writes[("DOLPHIN_STATE_PINK", "engine_snapshot")])
|
||||
assert "our_leverage" in snap, "our_leverage (dual-leverage: system layer) must be in Hz snapshot"
|
||||
|
||||
def test_daily_pnl_write(self):
|
||||
import json
|
||||
w = self._make_writer_no_hz()
|
||||
w.write_daily_pnl({"realized_pnl_total": 45.6, "capital": 25000.0, "trade_seq": 3})
|
||||
key = next((k for k in w._writes if k[0] == "DOLPHIN_PNL_PINK"), None)
|
||||
assert key is not None, "write_daily_pnl must write to DOLPHIN_PNL_PINK"
|
||||
row = json.loads(w._writes[key])
|
||||
assert row["pnl"] == 45.6
|
||||
|
||||
def test_write_survives_exception(self):
|
||||
"""Hz write failure must never propagate — observability must not affect trading."""
|
||||
from prod.clean_arch.dita_v2.hazelcast_projection import _hz_write_no_wait
|
||||
import unittest.mock as mock
|
||||
bad_map = mock.MagicMock()
|
||||
bad_map.put.side_effect = RuntimeError("Hz down")
|
||||
_hz_write_no_wait(bad_map, "key", "value") # must not raise
|
||||
|
||||
|
||||
# ============================================================
|
||||
# vol_ok gate in DecisionEngine
|
||||
# ============================================================
|
||||
|
||||
class TestVolOkGate:
|
||||
"""DecisionEngine must block ENTERs when vol_ok=False in scan_payload."""
|
||||
|
||||
def _make_snapshot(self, vol_ok: bool, vdiv: float = -0.03, irp: float = 0.60):
|
||||
from prod.clean_arch.ports.data_feed import MarketSnapshot
|
||||
from datetime import datetime, timezone
|
||||
return MarketSnapshot(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
symbol="BTCUSDT",
|
||||
price=50000.0,
|
||||
velocity_divergence=vdiv,
|
||||
irp_alignment=irp,
|
||||
scan_payload={"vol_ok": vol_ok, "posture": "APEX"},
|
||||
)
|
||||
|
||||
def _engine(self):
|
||||
from prod.clean_arch.dita.decision import DecisionEngine, DecisionConfig
|
||||
cfg = DecisionConfig(
|
||||
vel_div_threshold=-0.02,
|
||||
vel_div_extreme=-0.05,
|
||||
fixed_tp_pct=0.0020,
|
||||
max_hold_bars=250,
|
||||
capital_fraction=0.20,
|
||||
max_leverage=3.0,
|
||||
allow_short=True,
|
||||
allow_long=False,
|
||||
)
|
||||
return DecisionEngine(cfg)
|
||||
|
||||
def _ctx(self, open_positions: int = 0, capital: float = 25000.0):
|
||||
from prod.clean_arch.dita.contracts import DecisionContext
|
||||
return DecisionContext(capital=capital, open_positions=open_positions)
|
||||
|
||||
def test_vol_ok_false_blocks_enter(self):
|
||||
eng = self._engine()
|
||||
snap = self._make_snapshot(vol_ok=False)
|
||||
decision = eng.decide(snap, self._ctx())
|
||||
assert decision.action.value in ("HOLD", "NO_ACTION", "SKIP", "VOL_GATE"), (
|
||||
f"vol_ok=False must block ENTER, got action={decision.action.value!r} reason={getattr(decision, 'reason', '?')!r}"
|
||||
)
|
||||
|
||||
def test_vol_ok_true_allows_enter(self):
|
||||
eng = self._engine()
|
||||
snap = self._make_snapshot(vol_ok=True)
|
||||
decision = eng.decide(snap, self._ctx())
|
||||
assert decision.action.value not in ("VOL_GATE",), (
|
||||
"vol_ok=True must not block on vol_ok gate"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user