PINK DITAv2: Hz writes + vol_ok gate + leverage logging + 8 new tests (94/94 green)

This commit is contained in:
Codex
2026-06-03 13:26:36 +02:00
parent 0f2d3f556d
commit 8d85d75ded
6 changed files with 1570 additions and 6 deletions

View File

@@ -0,0 +1,176 @@
from __future__ import annotations
import json
from datetime import datetime, timezone
from typing import Any, Optional, Protocol
from .contracts import KernelTransition, TradeSlot
from .control import KernelControlSnapshot
from .journal import _transition_row
from .projection import build_position_state_row
from .utils import json_safe
# ── Fire-and-forget Hz write helpers ─────────────────────────────────────────
def _hz_write_no_wait(hz_map: Any, key: str, value: str) -> None:
"""Submit Hz write to the client's internal thread pool. Never blocks.
.put() without .blocking() returns a hazelcast Future immediately.
The Future is intentionally discarded — the network write is already
queued in the Hz client's thread pool and is not cancelled by GC.
Hz writes are observability-only; any failure must never affect trading.
"""
try:
hz_map.put(key, value)
except Exception:
pass
def _json_encode(payload: dict) -> str:
return json.dumps(payload, separators=(",", ":"), ensure_ascii=False, default=str)
def _utcnow_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _today_iso() -> str:
return datetime.now(timezone.utc).date().isoformat()
class HazelcastClientLike(Protocol):
def get_map(self, name: str): ...
def get_topic(self, name: str): ...
class HazelcastProjector:
"""Durable BLUE/PINK-compatible projection mirror."""
def __init__(
self,
client: HazelcastClientLike | None = None,
*,
active_slots_map: str = "dita_active_slots",
events_topic: str = "dita_trade_events",
) -> None:
self.client = client
self.active_slots_map = active_slots_map
self.events_topic = events_topic
def publish_slot(self, slot: TradeSlot) -> None:
if self.client is None:
return
self.client.get_map(self.active_slots_map).put(slot.trade_id, build_position_state_row(slot))
def publish_event(self, event_type: str, payload: dict[str, Any]) -> None:
if self.client is None:
return
topic = self.client.get_topic(self.events_topic)
topic.publish(
json.dumps(
{"event_type": event_type, "payload": json_safe(payload)},
ensure_ascii=False,
sort_keys=True,
default=str,
)
)
class HazelcastRowWriter:
"""Callback bridge for ``HazelcastProjection`` writer hooks."""
def __init__(self, client: HazelcastClientLike) -> None:
self.client = client
def __call__(self, name: str, row: dict[str, Any]) -> None:
if name.endswith("trade_events"):
self.client.get_topic(name).publish(
json.dumps(row, ensure_ascii=False, sort_keys=True, default=str)
)
return
if name.endswith("control"):
key = "control"
else:
key = str(row.get("trade_id", row.get("slot_id", row.get("event_id", ""))))
self.client.get_map(name).put(key, json_safe(row))
# ── PINK DITAv2 non-blocking Hz state writer ──────────────────────────────────
class PinkHzStateWriter:
"""Non-blocking Hz writer for PINK DITAv2 kernel state.
Dedicated Hz client (separate from the data-feed read client).
All writes are fire-and-forget: .put() returns a Future that is intentionally
discarded. A failed write = missed TUI update only — never affects trading.
BLUE-compatible schema (same shape as DOLPHIN_STATE_BLUE) written to
DOLPHIN_STATE_PINK / DOLPHIN_PNL_PINK — no overlap with BLUE maps.
"""
def __init__(
self,
cluster: str,
host: str,
state_map_name: str,
pnl_map_name: str,
) -> None:
import hazelcast
self._client = hazelcast.HazelcastClient(
cluster_name=cluster,
cluster_members=[host],
)
# Non-blocking proxies (.put() returns Future, does NOT block)
self._state_map = self._client.get_map(state_map_name)
self._pnl_map = self._client.get_map(pnl_map_name)
def write_engine_snapshot(
self,
slot_dict: dict,
acc_dict: dict,
posture: str = "APEX",
our_leverage: float = 0.0,
) -> None:
"""Write full engine state. Called after every kernel mutation (non-blocking)."""
payload: dict[str, Any] = {
"strategy": "pink",
"capital": acc_dict.get("capital", 0.0),
"equity": acc_dict.get("equity", 0.0),
"available_capital": acc_dict.get("available_capital", 0.0),
"pnl": acc_dict.get("realized_pnl_total", 0.0),
"fee_total": acc_dict.get("fee_total", 0.0),
"open_positions": int(acc_dict.get("open_positions", 0)),
"trade_seq": int(acc_dict.get("trade_seq", 0)),
"posture": posture,
"capital_frozen": bool(acc_dict.get("capital_frozen", False)),
"our_leverage": our_leverage,
"slot": slot_dict,
"updated_at": _utcnow_iso(),
}
_hz_write_no_wait(self._state_map, "engine_snapshot", _json_encode(payload))
# Compact "latest" key — same shape as BLUE's DOLPHIN_STATE_BLUE["latest"]
_hz_write_no_wait(self._state_map, "latest", _json_encode({
"strategy": "pink",
"capital": payload["capital"],
"date": _today_iso(),
"pnl": payload["pnl"],
"trades": payload["trade_seq"],
"posture": posture,
"updated_at": payload["updated_at"],
}))
def write_daily_pnl(self, acc_dict: dict, posture: str = "APEX") -> None:
"""Write per-date PnL row. Called on trade close only."""
_hz_write_no_wait(self._pnl_map, _today_iso(), _json_encode({
"pnl": acc_dict.get("realized_pnl_total", 0.0),
"capital": acc_dict.get("capital", 0.0),
"trades": int(acc_dict.get("trade_seq", 0)),
"posture": posture,
}))
def close(self) -> None:
try:
self._client.shutdown()
except Exception:
pass

View File

@@ -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"
)