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

121 lines
3.9 KiB
Python
Raw Normal View History

"""
Structured Observability — per-decision feature attribution and metrics.
Tracks:
- Which features drove each decision
- Feature importance over time
- Decision quality metrics
- Regime-specific performance
"""
from __future__ import annotations
import json
import time
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any, Dict, List, Mapping, Optional, Tuple
from malkhut.state import MarketWorldState
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
from malkhut.features import DefaultFeatureExtractor, FeatureExtractor
@dataclass(frozen=True, slots=True)
class DecisionMetrics:
"""Per-decision metrics."""
ts_ns: int
symbol: str
action_kind: str
approved: bool
plan_latency_ns: int
entropy: float
sims: int
feature_attribution: Mapping[str, float]
regime: str
class StructuredObservability:
"""
Structured observability with per-decision feature attribution.
Tracks which features drive decisions and computes aggregate metrics.
"""
def __init__(self, feature_extractor: Optional[FeatureExtractor] = None) -> None:
self._extractor = feature_extractor or DefaultFeatureExtractor()
self._decisions: list[DecisionMetrics] = []
self._feature_importance: Dict[str, List[float]] = defaultdict(list)
self._regime_performance: Dict[str, List[float]] = defaultdict(list)
self._total_decisions = 0
def record_decision(
self,
state: MarketWorldState,
planned: PlannedPolicy,
decision: RiskDecision,
plan_ns: int,
regime: str = "unknown",
) -> None:
"""Record a decision with full feature attribution."""
fv = self._extractor.extract(state).values
# Compute feature attribution (which features are "active")
attribution = {}
for name, value in fv.items():
if abs(value) > 1e-6:
attribution[name] = value
metrics = DecisionMetrics(
ts_ns=state.ts_ns,
symbol=state.venue.symbol,
action_kind=planned.selected_action.kind.value,
approved=decision.approved,
plan_latency_ns=plan_ns,
entropy=planned.diagnostics.get("entropy", 0.0),
sims=planned.diagnostics.get("sims", 0),
feature_attribution=attribution,
regime=regime,
)
self._decisions.append(metrics)
self._total_decisions += 1
# Track feature importance
for name, value in attribution.items():
self._feature_importance[name].append(value)
# Track regime performance
self._regime_performance[regime].append(1.0 if decision.approved else 0.0)
def get_feature_importance(self, top_n: int = 10) -> List[Tuple[str, float]]:
"""Get top N features by average absolute value."""
scores = []
for name, values in self._feature_importance.items():
avg = sum(abs(v) for v in values) / len(values)
scores.append((name, avg))
scores.sort(key=lambda x: x[1], reverse=True)
return scores[:top_n]
def get_regime_approval_rate(self, regime: str) -> float:
"""Get approval rate for a specific regime."""
approvals = self._regime_performance.get(regime, [])
if not approvals:
return 0.0
return sum(approvals) / len(approvals)
@property
def total_decisions(self) -> int:
return self._total_decisions
@property
def avg_latency_ns(self) -> float:
if not self._decisions:
return 0.0
return sum(d.plan_latency_ns for d in self._decisions) / len(self._decisions)
@property
def avg_entropy(self) -> float:
if not self._decisions:
return 0.0
return sum(d.entropy for d in self._decisions) / len(self._decisions)