malkhut(wire): PerformanceMatrix keyed by (regime, strategy, venue)

Three-dimensional key enables:
  - Per-venue best: get_best(regime, venue='bingx')
  - Cross-venue comparison: get_venue_comparison(regime, strategy_id)
  - Venue-agnostic: get_best(regime) scans all venues (backward compat)

New API:
  - record(..., venue='bingx'): venue parameter (default 'bingx')
  - get_best(regime, venue=None): optional venue filter
  - get_scores_for_regime(regime, venue=None): optional venue filter
  - get_venue_comparison(regime, strategy_id) -> {venue: score}

119 tests pass. All existing callers backward compatible.
This commit is contained in:
Codex
2026-07-14 15:37:36 +02:00
parent 2cba60a154
commit b70a6f0ad8

View File

@@ -169,15 +169,18 @@ class RegimeStrategyScore:
class PerformanceMatrix:
"""
Tracks strategy performance across regimes.
Tracks strategy performance across regimes AND venues.
Matrix: (regime, strategy_id) → RegimeStrategyScore
Matrix: (regime, strategy_id, venue) → RegimeStrategyScore
Used by the selector to choose the best strategy for current conditions.
Enables:
- Per-venue best strategy: get_best(regime, venue='bingx')
- Cross-venue comparison: get_venue_comparison(regime, strategy_id)
- Venue-agnostic fallback: get_best(regime) scans all venues
"""
def __init__(self) -> None:
self._scores: Dict[Tuple[MarketRegime, str], RegimeStrategyScore] = {}
self._scores: Dict[Tuple[MarketRegime, str, str], RegimeStrategyScore] = {}
self._strategy_regime_history: Dict[str, List[MarketRegime]] = defaultdict(list)
def record(
@@ -190,12 +193,11 @@ class PerformanceMatrix:
adverse_fill_ratio: float = 0.0,
venue: str = "bingx",
) -> None:
"""Record a strategy's performance in a regime (optionally venue-tagged)."""
key = (regime, strategy_id)
"""Record a strategy's performance in a regime on a specific venue."""
key = (regime, strategy_id, venue)
existing = self._scores.get(key)
if existing:
# Exponential moving average
alpha = 0.3
new_score = alpha * score + (1 - alpha) * existing.score
new_episodes = existing.episodes + 1
@@ -218,9 +220,9 @@ class PerformanceMatrix:
avg_drawdown_bps=new_dd,
avg_adverse_fill_ratio=new_adverse,
last_updated_ns=time.time_ns(),
confidence=min(1.0, new_episodes / 10.0), # confidence grows with evidence
confidence=min(1.0, new_episodes / 10.0),
support_count=new_episodes,
distance_to_nearest=0.0, # computed lazily on query
distance_to_nearest=0.0,
)
self._strategy_regime_history[strategy_id].append(regime)
@@ -228,23 +230,52 @@ class PerformanceMatrix:
self,
regime: MarketRegime,
exclude: Optional[set[str]] = None,
venue: Optional[str] = None,
) -> Optional[str]:
"""Get the best strategy for a given regime."""
"""Get the best strategy for a given regime, optionally filtered by venue."""
exclude = exclude or set()
candidates = [
(key[1], score.score)
for key, score in self._scores.items()
if key[0] == regime and key[1] not in exclude
and (venue is None or key[2] == venue)
]
if not candidates:
return None
return max(candidates, key=lambda x: x[1])[0]
def get_scores_for_regime(self, regime: MarketRegime) -> List[RegimeStrategyScore]:
"""Get all strategy scores for a regime, sorted by score."""
scores = [s for s in self._scores.values() if s.regime == regime]
def get_scores_for_regime(
self,
regime: MarketRegime,
venue: Optional[str] = None,
) -> List[RegimeStrategyScore]:
"""Get all strategy scores for a regime, optionally filtered by venue."""
scores = [
s for s in self._scores.values()
if s.regime == regime
and (venue is None or self._venue_of(s) == venue)
]
return sorted(scores, key=lambda s: s.score, reverse=True)
def _venue_of(self, score: RegimeStrategyScore) -> str:
"""Reverse-lookup venue from key. Returns 'bingx' if not found."""
for key in self._scores:
if self._scores[key] is score:
return key[2]
return "bingx"
def get_venue_comparison(
self,
regime: MarketRegime,
strategy_id: str,
) -> Dict[str, float]:
"""Compare a strategy's performance across venues for a given regime."""
result: Dict[str, float] = {}
for (reg, strat, venue), score in self._scores.items():
if reg == regime and strat == strategy_id:
result[venue] = score.score
return result
def get_regimes_for_strategy(self, strategy_id: str) -> List[MarketRegime]:
"""Get all regimes where a strategy has been tested."""
return list(set(key[0] for key in self._scores if key[1] == strategy_id))
@@ -376,6 +407,7 @@ class StrategySelector:
pnl_bps: float = 0.0,
drawdown_bps: float = 0.0,
adverse_fill_ratio: float = 0.0,
venue: str = "bingx",
) -> None:
"""Record a strategy's outcome for learning."""
self._matrix.record(
@@ -385,6 +417,7 @@ class StrategySelector:
pnl_bps=pnl_bps,
drawdown_bps=drawdown_bps,
adverse_fill_ratio=adverse_fill_ratio,
venue=venue,
)
@property