Files
siloqy/prod/clean_arch/dita_v2/projection.py
Codex 84e4a50e3f repo hygiene: track the PINK launcher import closure
67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:09:32 +02:00

98 lines
3.6 KiB
Python

"""Hazelcast-compatible projection helpers for DITAv2."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import os
from typing import Any, Callable, Dict, Iterable, List, Optional
from .account import AccountProjection
from .contracts import KernelTransition, TradeSlot, TradeStage, VenueEvent
from .control import KernelControlSnapshot
from .journal import _transition_row
from .utils import json_safe
Writer = Callable[[str, Dict[str, Any]], None]
@dataclass
class HazelcastProjection:
"""Projection helper for BLUE/PINK-compatible durable writes."""
active_slots_map: str = "hz:dita_active_slots"
trade_events_topic: str = "hz:dita_trade_events"
control_map: str = "hz:dita_control"
writer: Optional[Writer] = None
control_snapshot: Optional[KernelControlSnapshot] = None
def write_slot(self, slot: TradeSlot) -> Dict[str, Any]:
row = build_position_state_row(slot, self.control_snapshot)
if self.writer is not None:
self.writer(self.active_slots_map, row)
return row
def write_transition(
self,
*,
transition: KernelTransition,
slot: TradeSlot,
event: Optional[VenueEvent] = None,
control: Optional[KernelControlSnapshot] = None,
) -> Dict[str, Any]:
row = _transition_row(transition=transition, slot=slot, event=event, control=control)
if self.writer is not None:
self.writer(self.trade_events_topic, row)
return row
def write_control(self, control: KernelControlSnapshot) -> Dict[str, Any]:
self.control_snapshot = control
row = control.as_dict()
if self.writer is not None:
self.writer(self.control_map, row)
return row
def build_projection(
*,
writer: Optional[Writer] = None,
client: Optional[Any] = None,
prefer_real_hazelcast: Optional[bool] = None,
control_snapshot: Optional[KernelControlSnapshot] = None,
) -> HazelcastProjection:
"""Build the active projection helper with an operator-visible switch.
The default remains the callback-based projection helper. If a Hazelcast
client is supplied and the caller opts in via ``prefer_real_hazelcast`` or
``DITA_V2_HAZELCAST=REAL``, the helper routes directly through the
client-backed map/topic writer path.
"""
env_choice = os.environ.get("DITA_V2_HAZELCAST", "").strip().upper()
real_requested = prefer_real_hazelcast if prefer_real_hazelcast is not None else env_choice in {"REAL", "REAL_HZ", "HAZELCAST"}
if real_requested and client is not None:
try:
from .hazelcast_projection import HazelcastRowWriter
writer = HazelcastRowWriter(client)
except Exception:
pass
return HazelcastProjection(writer=writer, control_snapshot=control_snapshot)
def build_position_state_row(slot: TradeSlot, control: Optional[KernelControlSnapshot] = None) -> Dict[str, Any]:
"""Build a state row shaped for durable compatibility."""
row = slot.to_dict()
row.update(
{
"runtime_namespace": control.runtime_namespace if control else "dita_v2",
"strategy_namespace": control.strategy_namespace if control else "dita_v2",
"event_namespace": control.event_namespace if control else "dita_v2",
"actor_name": control.actor_name if control else "ExecutionKernel",
"exec_venue": control.exec_venue if control else "bingx",
"data_venue": control.data_venue if control else "binance",
"ledger_authority": control.ledger_authority if control else "exchange",
}
)
return row