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:
|
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:
|
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)
|
self._strategy_regime_history: Dict[str, List[MarketRegime]] = defaultdict(list)
|
||||||
|
|
||||||
def record(
|
def record(
|
||||||
@@ -190,12 +193,11 @@ class PerformanceMatrix:
|
|||||||
adverse_fill_ratio: float = 0.0,
|
adverse_fill_ratio: float = 0.0,
|
||||||
venue: str = "bingx",
|
venue: str = "bingx",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Record a strategy's performance in a regime (optionally venue-tagged)."""
|
"""Record a strategy's performance in a regime on a specific venue."""
|
||||||
key = (regime, strategy_id)
|
key = (regime, strategy_id, venue)
|
||||||
existing = self._scores.get(key)
|
existing = self._scores.get(key)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Exponential moving average
|
|
||||||
alpha = 0.3
|
alpha = 0.3
|
||||||
new_score = alpha * score + (1 - alpha) * existing.score
|
new_score = alpha * score + (1 - alpha) * existing.score
|
||||||
new_episodes = existing.episodes + 1
|
new_episodes = existing.episodes + 1
|
||||||
@@ -218,9 +220,9 @@ class PerformanceMatrix:
|
|||||||
avg_drawdown_bps=new_dd,
|
avg_drawdown_bps=new_dd,
|
||||||
avg_adverse_fill_ratio=new_adverse,
|
avg_adverse_fill_ratio=new_adverse,
|
||||||
last_updated_ns=time.time_ns(),
|
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,
|
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)
|
self._strategy_regime_history[strategy_id].append(regime)
|
||||||
|
|
||||||
@@ -228,23 +230,52 @@ class PerformanceMatrix:
|
|||||||
self,
|
self,
|
||||||
regime: MarketRegime,
|
regime: MarketRegime,
|
||||||
exclude: Optional[set[str]] = None,
|
exclude: Optional[set[str]] = None,
|
||||||
|
venue: Optional[str] = None,
|
||||||
) -> Optional[str]:
|
) -> 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()
|
exclude = exclude or set()
|
||||||
candidates = [
|
candidates = [
|
||||||
(key[1], score.score)
|
(key[1], score.score)
|
||||||
for key, score in self._scores.items()
|
for key, score in self._scores.items()
|
||||||
if key[0] == regime and key[1] not in exclude
|
if key[0] == regime and key[1] not in exclude
|
||||||
|
and (venue is None or key[2] == venue)
|
||||||
]
|
]
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return None
|
return None
|
||||||
return max(candidates, key=lambda x: x[1])[0]
|
return max(candidates, key=lambda x: x[1])[0]
|
||||||
|
|
||||||
def get_scores_for_regime(self, regime: MarketRegime) -> List[RegimeStrategyScore]:
|
def get_scores_for_regime(
|
||||||
"""Get all strategy scores for a regime, sorted by score."""
|
self,
|
||||||
scores = [s for s in self._scores.values() if s.regime == regime]
|
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)
|
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]:
|
def get_regimes_for_strategy(self, strategy_id: str) -> List[MarketRegime]:
|
||||||
"""Get all regimes where a strategy has been tested."""
|
"""Get all regimes where a strategy has been tested."""
|
||||||
return list(set(key[0] for key in self._scores if key[1] == strategy_id))
|
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,
|
pnl_bps: float = 0.0,
|
||||||
drawdown_bps: float = 0.0,
|
drawdown_bps: float = 0.0,
|
||||||
adverse_fill_ratio: float = 0.0,
|
adverse_fill_ratio: float = 0.0,
|
||||||
|
venue: str = "bingx",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Record a strategy's outcome for learning."""
|
"""Record a strategy's outcome for learning."""
|
||||||
self._matrix.record(
|
self._matrix.record(
|
||||||
@@ -385,6 +417,7 @@ class StrategySelector:
|
|||||||
pnl_bps=pnl_bps,
|
pnl_bps=pnl_bps,
|
||||||
drawdown_bps=drawdown_bps,
|
drawdown_bps=drawdown_bps,
|
||||||
adverse_fill_ratio=adverse_fill_ratio,
|
adverse_fill_ratio=adverse_fill_ratio,
|
||||||
|
venue=venue,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
Reference in New Issue
Block a user