diff --git a/MALKHUT/malkhut/training/selector.py b/MALKHUT/malkhut/training/selector.py index 4c4024f..ef49dbd 100644 --- a/MALKHUT/malkhut/training/selector.py +++ b/MALKHUT/malkhut/training/selector.py @@ -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