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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user