Files
sentiment-engine/MALKHUT/malkhut/training/observability.py

101 lines
3.1 KiB
Python
Raw Normal View History

"""
Structured Observability — compact JSONL logging for every decision.
Records:
- Every planner decision with features and diagnostics
- Every risk gate decision
- Every venue action
- Every fill/discrepancy
- Replayable audit trail
"""
from __future__ import annotations
import json
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Mapping, Optional
from malkhut.state import MarketWorldState
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
@dataclass(frozen=True, slots=True)
class DecisionRecord:
"""One decision in the audit trail."""
ts_ns: int
symbol: str
action_kind: str
action_side: Optional[str]
action_price: Optional[float]
approved: bool
risk_reason: str
plan_latency_ns: int
policy_version: str
root_entropy: float
sims: int
features: Mapping[str, float] = field(default_factory=dict)
diagnostics: Mapping[str, Any] = field(default_factory=dict)
class ObservabilityLogger:
"""
Compact JSONL logger for every decision.
One line per decision. Replayable audit trail.
"""
def __init__(self, log_path: str = "decisions.log") -> None:
self._log_path = log_path
self._decisions: list[DecisionRecord] = []
self._total_decisions = 0
def log_decision(
self,
state: MarketWorldState,
planned: PlannedPolicy,
decision: RiskDecision,
plan_ns: int,
features: Optional[Mapping[str, float]] = None,
) -> None:
"""Log a decision to the audit trail."""
record = DecisionRecord(
ts_ns=state.ts_ns,
symbol=state.venue.symbol,
action_kind=planned.selected_action.kind.value,
action_side=planned.selected_action.side.value if planned.selected_action.side else None,
action_price=planned.selected_action.price_ticks_from_best if planned.selected_action else None,
approved=decision.approved,
risk_reason=decision.reason,
plan_latency_ns=plan_ns,
policy_version="live",
root_entropy=planned.diagnostics.get("entropy", 0.0),
sims=planned.diagnostics.get("sims", 0),
features=features or {},
)
self._decisions.append(record)
self._total_decisions += 1
# Write to JSONL
try:
with open(self._log_path, "a") as f:
f.write(json.dumps({
"ts": record.ts_ns,
"sym": record.symbol,
"act": record.action_kind,
"side": record.action_side,
"app": record.approved,
"risk": record.risk_reason,
"lat_ns": record.plan_latency_ns,
"entropy": round(record.root_entropy, 4),
"sims": record.sims,
}, separators=(",", ":")) + "\n")
except OSError:
pass
@property
def total_decisions(self) -> int:
return self._total_decisions
def get_recent(self, n: int = 10) -> List[DecisionRecord]:
return self._decisions[-n:]