"""PINK ClickHouse persistence — DITAv2-backed, reads capital from kernel. Row families preserved (same schema, no new columns): - policy_events / v7_decision_events - position_state - account_events - status_snapshots - trade_events - trade_reconstruction - trade_exit_legs - anomaly_events Capital/peak_capital/trade_seq are read from the kernel's AccountProjection (single authority). No duplicate tracking in this module. """ from __future__ import annotations import json import math from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum from typing import Any, Callable, Mapping, Optional from prod.clean_arch.dita import AccountProjection, Decision, DecisionAction, Intent, TradeSide, TradeStage from prod.clean_arch.dita_v2.contracts import KernelDiagnosticCode, KernelOutcome Writer = Callable[[str, dict[str, Any]], None] def _json_safe(value: Any) -> Any: if isinstance(value, Enum): return value.value if isinstance(value, dict): return {str(key): _json_safe(val) for key, val in value.items()} if isinstance(value, (list, tuple)): return [_json_safe(item) for item in value] if hasattr(value, "isoformat"): try: return value.isoformat() except Exception: pass if hasattr(value, "__dict__"): try: return _json_safe(dict(vars(value))) except Exception: pass return value def _json_text(value: Any) -> str: return json.dumps(_json_safe(value), separators=(",", ":"), ensure_ascii=False, default=str) def _direction(side: TradeSide) -> int: return -1 if side == TradeSide.SHORT else 1 def _direction_from_str(side: str) -> int: return -1 if side.upper() in ("SHORT", "SELL") else 1 def _notional(size: float, price: float) -> float: if not math.isfinite(size) or not math.isfinite(price): return 0.0 return abs(size) * abs(price) def _safe_float(value: Any, default: float = 0.0) -> float: try: out = float(value) except Exception: return default if not math.isfinite(out): return default return out def _decision_summary(decision: Decision | None) -> dict[str, Any]: if decision is None: return {} return { "timestamp": decision.timestamp.isoformat() if hasattr(decision.timestamp, "isoformat") else str(decision.timestamp), "decision_id": decision.decision_id, "asset": decision.asset, "action": decision.action.value, "side": decision.side.value, "reason": decision.reason, "confidence": float(decision.confidence or 0.0), "velocity_divergence": float(decision.velocity_divergence or 0.0), "irp_alignment": float(decision.irp_alignment or 0.0), "reference_price": float(decision.reference_price or 0.0), "target_size": float(decision.target_size or 0.0), "leverage": float(decision.leverage or 0.0), "bars_held": int(decision.bars_held or 0), "stage": decision.stage.value, "metadata": _json_safe(decision.metadata), } def _intent_summary(intent: Intent | None) -> dict[str, Any]: if intent is None: return {} return { "timestamp": intent.timestamp.isoformat() if hasattr(intent.timestamp, "isoformat") else str(intent.timestamp), "trade_id": intent.trade_id, "decision_id": intent.decision_id, "asset": intent.asset, "action": intent.action.value, "side": intent.side.value, "reason": intent.reason, "target_size": float(intent.target_size or 0.0), "leverage": float(intent.leverage or 0.0), "reference_price": float(intent.reference_price or 0.0), "confidence": float(intent.confidence or 0.0), "bars_held": int(intent.bars_held or 0), "stage": intent.stage.value, "exit_leg_ratios": [float(r) for r in intent.exit_leg_ratios], "metadata": _json_safe(intent.metadata), } def _outcome_summary(outcome: KernelOutcome | None) -> dict[str, Any]: if outcome is None: return {} return { "accepted": bool(outcome.accepted), "slot_id": int(outcome.slot_id), "trade_id": outcome.trade_id, "state": outcome.state.value, "diagnostic_code": outcome.diagnostic_code.value, "severity": outcome.severity.value, "details": _json_safe(outcome.details), } @dataclass(frozen=True) class PinkClickHousePersistenceConfig: """Row-shape knobs for the PINK ClickHouse mirror.""" strategy: str = "pink" runtime_namespace: str = "pink" strategy_namespace: str = "pink" event_namespace: str = "pink" actor_name: str = "PinkDirectRuntime" exec_venue: str = "bingx" data_venue: str = "binance" ledger_authority: str = "exchange" initial_capital: float = 25_000.0 max_account_leverage: float = 3.0 exchange_leverage_mode: str = "" leverage_mapping_rule: str = "round_half_even_linear_0.5_to_9.0_to_1_to_exchange_cap" class PinkClickHousePersistence: """Durable PINK ClickHouse sink — capital reads from kernel AccountProjection.""" def __init__( self, account: AccountProjection, *, config: PinkClickHousePersistenceConfig | None = None, sink: Writer | None = None, v7_sink: Writer | None = None, ) -> None: self.account = account self.config = config or PinkClickHousePersistenceConfig( runtime_namespace=account.runtime_namespace, strategy_namespace=account.strategy_namespace, event_namespace=account.event_namespace, actor_name=account.actor_name, exec_venue=account.exec_venue, data_venue=account.data_venue, ledger_authority=account.ledger_authority, initial_capital=float(account.snapshot.capital or 25_000.0), ) self._sink = sink or self._resolve_sink("pink") self._v7_sink = v7_sink or self._resolve_v7_sink("pink") # Per-trade incremental leg state for trade_exit_legs row deltas. # Keyed by trade_id; reset on ENTER. Tracks the cumulative realized PnL # and remaining size observed at the previous leg so each leg row carries # an isolated (non-cumulative) pnl_leg / exit_qty. self._leg_state: dict[str, dict[str, Any]] = {} @staticmethod def _resolve_sink(strategy: str) -> Writer: from prod.ch_writer import ch_put_pink return ch_put_pink @staticmethod def _resolve_v7_sink(strategy: str) -> Writer: from prod.ch_writer import ch_put_pink_v7 return ch_put_pink_v7 def _capital(self) -> float: return float(self.account.snapshot.capital or 0.0) def _peak_capital(self) -> float: return float(getattr(self.account.snapshot, "peak_capital", self._capital()) or self._capital()) def _trade_seq(self) -> int: return int(getattr(self.account.snapshot, "trade_seq", 0) or 0) def _equity(self) -> float: return float(self.account.snapshot.equity or self._capital()) # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ def persist_step( self, *, snapshot: Any, decision: Decision, intent: Intent, outcome: KernelOutcome | None = None, slot_dict: dict[str, Any] | None = None, acc_dict: dict[str, Any] | None = None, phase: str = "step", market_state: Mapping[str, Any] | None = None, ) -> None: slot = slot_dict or {} stage = ( TradeStage(decision.stage.value) if hasattr(decision.stage, "value") else TradeStage(decision.stage) if isinstance(decision.stage, str) else TradeStage.ORDER_REQUESTED ) status = self._state_label(slot, phase) self._write_policy_event(snapshot, decision, intent, phase=phase) self._write_account_event(snapshot, decision, intent, stage=stage, slot_dict=slot) self._write_position_state(snapshot, decision, intent, slot_dict=slot, stage=stage, status=status, market_state=market_state) self._write_status_snapshot(snapshot, decision, intent, slot_dict=slot, phase=phase) # Emit anomaly for diagnostic codes (except OK). if outcome is not None and outcome.diagnostic_code != KernelDiagnosticCode.OK: self._write_anomaly( snapshot, decision, intent, anomaly=outcome.diagnostic_code.value, origin="ditav2_kernel", detail=outcome.details, ) if outcome is None: # Decision-only step (HOLD, no execution). return if decision.action == DecisionAction.ENTER: # Reset per-trade leg deltas: a fresh position starts with zero # realized PnL and the full initial size remaining. self._leg_state[intent.trade_id] = { "prev_realized": 0.0, "prev_size": _safe_float( slot.get("initial_size", slot.get("size", 0.0)), 0.0 ) or _safe_float(intent.target_size, 0.0), "prev_leg_id": "", } self._write_trade_reconstruction( snapshot, intent.trade_id, event_type="ENTRY_FILLED", event_id=f"{intent.trade_id}:entry", payload={ "decision": _decision_summary(decision), "intent": _intent_summary(intent), "outcome": _outcome_summary(outcome), "slot": slot, "market_state": _json_safe(market_state or {}), }, market_state=market_state, ) return if decision.action != DecisionAction.EXIT: return partial = slot.get("closed", False) is False and slot.get("size", 0) > 0 # One trade_exit_legs row per exit leg (partial or final), BLUE-schema # compatible so PINK multi-exit trades reconcile against the same table. self._write_trade_exit_leg(snapshot, decision, intent, slot, outcome) self._write_trade_reconstruction( snapshot, intent.trade_id, event_type="PARTIAL_EXIT" if partial else "EXIT", event_id=f"{intent.trade_id}:{'partial' if partial else 'close'}", payload={ "decision": _decision_summary(decision), "intent": _intent_summary(intent), "outcome": _outcome_summary(outcome), "slot": slot, "market_state": _json_safe(market_state or {}), }, market_state=market_state, ) # Terminal trade event. if slot.get("closed", False): self._write_trade_event(snapshot, decision, intent, slot, outcome, market_state=market_state) def persist_recovery_state( self, *, snapshot: Any, acc_dict: dict[str, Any] | None = None, phase: str = "recovery", event_type: str = "RECOVERY", market_state: Mapping[str, Any] | None = None, ) -> None: """Persist recovery-only state after kernel reconcile.""" slot_dict = acc_dict or {} self._write_status_snapshot( snapshot, decision=None, intent=None, slot_dict={}, phase=phase, ) self._write_account_event( snapshot, decision=None, intent=None, stage=TradeStage.TRADE_TERMINAL_WRITTEN, slot_dict={}, event_type=event_type, ) self._write_position_state( snapshot, decision=None, intent=None, slot_dict={}, stage=TradeStage.TRADE_TERMINAL_WRITTEN, status=self._state_label({}, phase), market_state=market_state, ) self._write_trade_reconstruction( snapshot, trade_id=acc_dict.get("trade_id", "") if acc_dict else "", event_type=event_type, event_id=f"recovery:{phase}", payload={"acc_dict": _json_safe(acc_dict or {}), "phase": phase, "market_state": _json_safe(market_state or {})}, market_state=market_state, ) def record_anomaly( self, *, snapshot: Any, decision: Any, intent: Any, anomaly: str, origin: str = "emergent", sensor: str = "", detail: Any = "", rm_meta: float = 0.0, ) -> None: """Persist a DITA anomaly row with legacy-compatible shape.""" self._sink( "anomaly_events", { "ts": snapshot.timestamp.isoformat(), "decision_id": decision.decision_id, "trade_id": intent.trade_id, "symbol": intent.asset, "anomaly": anomaly, "origin": origin, "sensor": sensor, "detail": _json_text(detail) if not isinstance(detail, str) else detail, "rm_meta": float(rm_meta), }, ) # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ @staticmethod def _state_label(slot_dict: dict[str, Any], phase: str) -> str: if slot_dict.get("closed", False): return "CLOSED" if slot_dict.get("size", 0) > 0: if phase.lower().startswith("recovery"): return "RECOVERED_OPEN" return "OPEN" return "FLAT" def _posture(self, slot_dict: dict[str, Any]) -> str: if slot_dict.get("closed", False) or not slot_dict.get("size", 0): return "FLAT" return str(slot_dict.get("side", "FLAT")) def _slot_entry_price(self, slot_dict: dict[str, Any]) -> float: return _safe_float(slot_dict.get("entry_price", 0.0), 0.0) def _slot_size(self, slot_dict: dict[str, Any]) -> float: return _safe_float(slot_dict.get("size", 0.0), 0.0) def _slot_side(self, slot_dict: dict[str, Any]) -> TradeSide: raw = str(slot_dict.get("side", "FLAT")).upper() if raw == "SHORT": return TradeSide.SHORT if raw == "LONG": return TradeSide.LONG return TradeSide.FLAT def _slot_trade_id(self, slot_dict: dict[str, Any]) -> str: return str(slot_dict.get("trade_id", "")) def _slot_asset(self, slot_dict: dict[str, Any]) -> str: return str(slot_dict.get("asset", "")) # ------------------------------------------------------------------ # Row writers # ------------------------------------------------------------------ def _write_anomaly( self, snapshot: Any, decision: Decision, intent: Intent, *, anomaly: str, origin: str = "ditav2_kernel", detail: Any = "", ) -> None: self._sink("anomaly_events", { "ts": snapshot.timestamp.isoformat(), "decision_id": decision.decision_id, "trade_id": intent.trade_id, "symbol": intent.asset, "anomaly": anomaly, "origin": origin, "sensor": "", "detail": _json_text(detail) if not isinstance(detail, str) else detail, "rm_meta": 0.0, }) def _write_policy_event( self, snapshot: Any, decision: Decision, intent: Intent, *, phase: str, ) -> None: price = _safe_float(decision.reference_price, 0.0) quantity = _safe_float(intent.target_size, 0.0) row = { "ts": snapshot.timestamp.isoformat(), "strategy": self.config.strategy, "runtime_namespace": self.config.runtime_namespace, "strategy_namespace": self.config.strategy_namespace, "event_namespace": self.config.event_namespace, "actor_name": self.config.actor_name, "exec_venue": self.config.exec_venue, "data_venue": self.config.data_venue, "source": "ditav2", "trade_id": intent.trade_id, "asset": decision.asset, "side": decision.side.value, "entry_price": price, "current_price": price, "quantity": quantity, "notional": _notional(quantity, price), "leverage": _safe_float(intent.leverage, 1.0), "bar_idx": 0, "decision_seq": self._trade_seq(), "bars_held": int(intent.bars_held or 0), "action": decision.action.value, "reason": decision.reason, "pnl_pct": 0.0, "mfe": 0.0, "mae": 0.0, "mfe_risk": 0.0, "mae_risk": 0.0, "exit_pressure": 0.0, "rv_comp": 0.0, "mae_thresh1": 0.0, "bounce_score": 0.0, "bounce_risk": 0.0, "ob_imbalance": 0.0, "vel_div_entry": float(decision.velocity_divergence or 0.0), "vel_div_now": float(decision.velocity_divergence or 0.0), "v50_vel": 0.0, "v750_vel": 0.0, "exf_funding": 0.0, "exf_dvol": 0.0, "exf_fear_greed": 0.0, "exf_taker": 0.0, "posture": decision.side.value, } self._sink("policy_events", row) self._v7_sink("v7_decision_events", row) def _write_account_event( self, snapshot: Any, decision: Decision | None, intent: Intent | None, *, stage: TradeStage, slot_dict: dict[str, Any], event_type: str | None = None, ) -> None: capital = self._capital() peak_cap = self._peak_capital() is_open = not slot_dict.get("closed", False) and slot_dict.get("size", 0) > 0 open_notional = _notional(self._slot_size(slot_dict), self._slot_entry_price(slot_dict)) if is_open else 0.0 drawdown_pct = 0.0 if peak_cap <= 0 else max(0.0, (peak_cap - capital) / peak_cap) row = { "ts": snapshot.timestamp.isoformat(), "event_type": event_type or stage.value, "strategy": self.config.strategy, "posture": self._posture(slot_dict), "capital": capital, "peak_capital": peak_cap, "drawdown_pct": drawdown_pct, "pnl_today": float(self.account.snapshot.realized_pnl or 0.0), "trades_today": self._trade_seq(), "open_positions": 1 if is_open else 0, "boost": 1.0, "beta": 0.0, "current_open_notional": open_notional, "current_account_leverage": 0.0 if capital <= 0 else open_notional / capital, "exchange_leverage": int(round(_safe_float(slot_dict.get("leverage", 0.0), 0.0))), "exchange_leverage_mode": self.config.exchange_leverage_mode, "leverage_mapping_rule": self.config.leverage_mapping_rule, "runtime_namespace": self.config.runtime_namespace, "strategy_namespace": self.config.strategy_namespace, "event_namespace": self.config.event_namespace, "actor_name": self.config.actor_name, "exec_venue": self.config.exec_venue, "data_venue": self.config.data_venue, "notes": _json_text({ "decision_id": None if decision is None else decision.decision_id, "trade_id": None if intent is None else intent.trade_id, "reason": None if intent is None else intent.reason, "stage": stage.value, }), } self._sink("account_events", row) def _write_position_state( self, snapshot: Any, decision: Decision | None, intent: Intent | None, *, slot_dict: dict[str, Any], stage: TradeStage, status: str, market_state: Mapping[str, Any] | None = None, ) -> None: side = self._slot_side(slot_dict) trade_id = self._slot_trade_id(slot_dict) asset = self._slot_asset(slot_dict) if not trade_id and intent is not None: trade_id = intent.trade_id asset = intent.asset side = intent.side row = { "ts": snapshot.timestamp.isoformat(), "trade_id": trade_id, "asset": asset, "direction": _direction(side), "entry_price": self._slot_entry_price(slot_dict), "quantity": self._slot_size(slot_dict), "notional": _notional(self._slot_size(slot_dict), self._slot_entry_price(slot_dict)), "leverage": _safe_float(slot_dict.get("leverage", 0.0), 0.0), "bucket_id": -1, "entry_bar": int(slot_dict.get("active_leg_index", 0) or 0), "status": status, "exit_reason": slot_dict.get("close_reason", ""), "pnl": _safe_float(slot_dict.get("realized_pnl", 0.0), 0.0), "bars_held": 0, "market_state_bundle_json": _json_text(market_state or {}), "tp_base_pct": 0.0, "tp_effective_pct": 0.0, "our_leverage": _safe_float(slot_dict.get("leverage", 0.0), 0.0), } self._sink("position_state", row) def _write_status_snapshot( self, snapshot: Any, decision: Decision | None, intent: Intent | None, *, slot_dict: dict[str, Any], phase: str, ) -> None: capital = self._capital() peak_cap = self._peak_capital() is_open = not slot_dict.get("closed", False) and slot_dict.get("size", 0) > 0 open_notional = _notional(self._slot_size(slot_dict), self._slot_entry_price(slot_dict)) if is_open else 0.0 leverage = 0.0 if capital <= 0 else open_notional / capital drawdown = 0.0 if peak_cap <= 0 else max(0.0, (peak_cap - capital) / peak_cap) row = { "ts": snapshot.timestamp.isoformat(timespec="milliseconds"), "capital": capital, "roi_pct": 0.0 if self.config.initial_capital <= 0 else ((capital / self.config.initial_capital) - 1.0) * 100.0, "dd_pct": drawdown * 100.0, "trades_executed": self._trade_seq(), "posture": self._posture(slot_dict), "rm": 1.0 if decision is None else max(0.0, min(1.0, decision.confidence)), "vel_div": 0.0 if decision is None else float(decision.velocity_divergence), "vol_ok": 1, "phase": phase, "mhs_status": "GREEN", "boost": 1.0, "cat5": 0.0, "conviction_multiplier": 0.0 if intent is None else float(intent.confidence or 0.0), "exchange_leverage": int(round(_safe_float(slot_dict.get("leverage", 0.0), 0.0))), "exchange_leverage_mode": self.config.exchange_leverage_mode, "leverage_mapping_rule": self.config.leverage_mapping_rule, "account_capital": capital, "portfolio_capital": capital, "current_open_notional": open_notional, "current_account_leverage": leverage, "remaining_notional_capacity": max(0.0, self.config.max_account_leverage * capital - open_notional), "max_account_leverage": self.config.max_account_leverage, "ledger_authority": self.config.ledger_authority, } self._sink("status_snapshots", row) def _write_trade_exit_leg( self, snapshot: Any, decision: Decision, intent: Intent, slot_dict: dict[str, Any], outcome: KernelOutcome | None, ) -> None: """Emit one BLUE-schema-compatible ``trade_exit_legs`` row per exit leg. The DITAv2 kernel uses a single slot with sequential exit legs rather than BLUE's chained per-leg trade_ids, so the chain_* columns describe the leg sequence within this one trade (root = trade_id). Per-leg deltas (exit_qty, pnl_leg) are computed against the previous leg's snapshot held in ``self._leg_state`` so each row is isolated, not cumulative. """ trade_id = intent.trade_id prev = self._leg_state.get(trade_id) or { "prev_realized": 0.0, "prev_size": _safe_float(slot_dict.get("initial_size", 0.0), 0.0), "prev_leg_id": "", } entry_price = self._slot_entry_price(slot_dict) or _safe_float(intent.reference_price, 0.0) exit_price = _safe_float(intent.reference_price, 0.0) or _safe_float(decision.reference_price, 0.0) side = self._slot_side(slot_dict) if side == TradeSide.FLAT: side = intent.side leverage_val = _safe_float(slot_dict.get("leverage", intent.leverage), 1.0) cur_size = self._slot_size(slot_dict) cur_realized = _safe_float(slot_dict.get("realized_pnl", 0.0), 0.0) prev_size = _safe_float(prev.get("prev_size", 0.0), 0.0) prev_realized = _safe_float(prev.get("prev_realized", 0.0), 0.0) # active_leg_index is post-fill (already advanced); the leg that just # filled is therefore one behind. Clamp to a valid ratio index. ratios = slot_dict.get("exit_leg_ratios", []) or [] leg_index = max(0, int(slot_dict.get("active_leg_index", 0) or 0) - 1) fraction = _safe_float(ratios[leg_index], 0.0) if 0 <= leg_index < len(ratios) else 0.0 exit_qty = max(0.0, prev_size - cur_size) pnl_leg = cur_realized - prev_realized capital_after = self._capital() capital_before = capital_after - pnl_leg exit_notional = _notional(exit_qty, exit_price or entry_price) remaining_notional = _notional(cur_size, entry_price) denom = abs(exit_qty * entry_price * max(leverage_val, 1e-9)) pnl_pct_leg = pnl_leg / denom if denom > 0 else 0.0 exit_leg_id = f"{trade_id}:leg{leg_index}" self._sink("trade_exit_legs", { "ts": snapshot.timestamp.isoformat(), "date": snapshot.timestamp.date().isoformat(), "strategy": self.config.strategy, "trade_id": trade_id, "chain_root_trade_id": trade_id, "chain_head_leg_id": f"{trade_id}:leg0", "chain_prev_leg_id": str(prev.get("prev_leg_id", "") or ""), "chain_seq": leg_index, "chain_token": trade_id, "chain_mode": "LIVE", "exit_leg_id": exit_leg_id, "exit_seq": leg_index, "command_id": decision.decision_id, "source": "ditav2", "reason": intent.reason, "asset": intent.asset, "side": side.value, "entry_price": entry_price, "exit_price": exit_price, "fraction": fraction, "capital_before": capital_before, "capital_after": capital_after, "exit_notional": exit_notional, "remaining_notional": remaining_notional, "remaining_qty": cur_size, "pnl_pct_leg": pnl_pct_leg, "pnl_leg": pnl_leg, "pnl_realized_total": cur_realized, "bars_held": int(intent.bars_held or 0), }) # Advance the per-trade leg snapshot for the next leg's delta. self._leg_state[trade_id] = { "prev_realized": cur_realized, "prev_size": cur_size, "prev_leg_id": exit_leg_id, } def _write_trade_event( self, snapshot: Any, decision: Decision, intent: Intent, slot_dict: dict[str, Any], outcome: KernelOutcome | None, *, market_state: Mapping[str, Any] | None = None, ) -> None: entry_price = _safe_float(slot_dict.get("entry_price", 0.0), 0.0) or _safe_float(intent.reference_price, 0.0) quantity = _safe_float(slot_dict.get("initial_size", slot_dict.get("size", 0.0)), 0.0) or _safe_float(intent.target_size, 0.0) exit_price = _safe_float(slot_dict.get("entry_price", 0.0), 0.0) pnl = _safe_float(slot_dict.get("realized_pnl", 0.0), 0.0) pnl_pct = 0.0 leverage_val = _safe_float(slot_dict.get("leverage", intent.leverage), 1.0) denom = abs(quantity * entry_price * max(leverage_val, 1e-9)) if denom > 0: pnl_pct = pnl / denom capital_after = self._capital() capital_before = capital_after - pnl open_notional = _notional(quantity, exit_price or entry_price) conviction = float(intent.confidence or decision.confidence or 0.0) metadata = intent.metadata if intent is not None else (decision.metadata if decision is not None else {}) row = { "ts": snapshot.timestamp.isoformat(), "date": snapshot.timestamp.date().isoformat(), "strategy": self.config.strategy, "trade_id": intent.trade_id, "asset": intent.asset, "side": intent.side.value, "entry_price": entry_price, "exit_price": exit_price, "quantity": quantity, "pnl": pnl, "pnl_pct": pnl_pct, "exit_reason": intent.reason, "vel_div_entry": float(decision.velocity_divergence or 0.0), "boost_at_entry": 1.0, "beta_at_entry": 0.0, "posture": intent.side.value, "leverage": leverage_val, "conviction_multiplier": conviction, "exchange_leverage": int(round(leverage_val)), "exchange_leverage_mode": self.config.exchange_leverage_mode, "leverage_mapping_rule": self.config.leverage_mapping_rule, "runtime_namespace": self.config.runtime_namespace, "strategy_namespace": self.config.strategy_namespace, "event_namespace": self.config.event_namespace, "actor_name": self.config.actor_name, "exec_venue": self.config.exec_venue, "data_venue": self.config.data_venue, "account_capital": capital_after, "portfolio_capital": capital_after, "current_open_notional": open_notional, "remaining_notional_capacity": max(0.0, self.config.max_account_leverage * capital_after - open_notional), "max_account_leverage": self.config.max_account_leverage, "margin_required": 0.0 if leverage_val <= 0 else open_notional / leverage_val, "ledger_authority": self.config.ledger_authority, "regime_signal": 0, "capital_before": capital_before, "capital_after": capital_after, "peak_capital": self._peak_capital(), "drawdown_at_entry": 0.0 if self._peak_capital() <= 0 else max(0.0, (self._peak_capital() - capital_before) / self._peak_capital()), "open_positions_count": 0, "scan_uuid": decision.decision_id, "bars_held": int(intent.bars_held or 0), "entry_payload_json": _json_text({"decision": _decision_summary(decision), "intent": _intent_summary(intent)}), "exit_payload_json": _json_text({"outcome": _outcome_summary(outcome), "slot": _json_safe(slot_dict)}), "execution_payload_json": _json_text({"outcome": _outcome_summary(outcome)}), "friction_payload_json": _json_text({"fees": 0.0}), "event_payload_json": _json_text({"phase": "terminal_close", "trade_id": intent.trade_id}), "market_state_bundle_json": _json_text(market_state or {}), "tp_base_pct": _safe_float(metadata.get("tp_base_pct", 0.0), 0.0), "tp_effective_pct": _safe_float(metadata.get("tp_effective_pct", 0.0), 0.0), "our_leverage": _safe_float(metadata.get("our_leverage", 0.0), 0.0), } self._sink("trade_events", row) def _write_trade_reconstruction( self, snapshot: Any, trade_id: str, *, event_type: str, event_id: str, payload: Any, market_state: Mapping[str, Any] | None = None, ) -> None: self._sink("trade_reconstruction", { "ts": snapshot.timestamp.isoformat(), "trade_id": trade_id, "event_type": event_type, "event_id": event_id, "payload_json": _json_text(payload), "market_state_bundle_json": _json_text(market_state or {}), })