Files
NexQuant/rdagent/components/backtesting/protections/low_performance.py
T

104 lines
3.5 KiB
Python
Raw Normal View History

"""
Low Performance Filter
Blocks trading for factors/portfolios with consistently poor performance.
"""
from dataclasses import dataclass
from datetime import datetime
from .base import BaseProtection, ProtectionConfig, ProtectionResult, ProtectionType, ProtectionScope
@dataclass
class LowPerformanceConfig(ProtectionConfig):
"""Configuration for LowPerformance protection."""
min_sharpe_ratio: float = 0.5 # Minimum acceptable Sharpe
min_win_rate: float = 0.40 # Minimum 40% win rate
min_trades: int = 20 # Need at least this many trades to evaluate
class LowPerformanceProtection(BaseProtection):
"""
Filters out consistently underperforming factors.
Prevents wasting resources on factors that statistical analysis
shows are unlikely to become profitable.
"""
def __init__(self, config: LowPerformanceConfig):
super().__init__(config)
self.config: LowPerformanceConfig = config
@property
def scope(self) -> ProtectionScope:
return ProtectionScope.FACTOR
def check(
self,
returns: list[float],
timestamps: list[datetime],
current_equity: float,
peak_equity: float,
**kwargs
) -> ProtectionResult:
"""Check if performance is below minimum standards."""
self.record_check()
if not self.config.enabled:
return ProtectionResult(
should_block=False,
reason="Protection disabled",
protection_type=ProtectionType.LOW_PERFORMANCE
)
# Need minimum number of trades
if len(returns) < self.config.min_trades:
return ProtectionResult(
should_block=False,
reason=f"Insufficient data ({len(returns)} < {self.config.min_trades} trades)",
protection_type=ProtectionType.LOW_PERFORMANCE,
severity=0.0
)
# Calculate metrics
import numpy as np
returns_array = np.array(returns)
# Win rate
wins = int(np.sum(returns_array > 0))
win_rate = wins / len(returns)
# Sharpe ratio (annualized, assuming daily returns)
mean_return = float(np.mean(returns_array))
std_return = float(np.std(returns_array))
sharpe = (mean_return / std_return * np.sqrt(252)) if std_return > 0 else 0
# Check thresholds
reasons = []
severity = 0.0
if sharpe < self.config.min_sharpe_ratio:
reasons.append(f"Sharpe {sharpe:.2f} < {self.config.min_sharpe_ratio}")
severity = max(severity, (self.config.min_sharpe_ratio - sharpe) / self.config.min_sharpe_ratio)
if win_rate < self.config.min_win_rate:
reasons.append(f"Win rate {win_rate*100:.1f}% < {self.config.min_win_rate*100:.1f}%")
severity = max(severity, (self.config.min_win_rate - win_rate) / self.config.min_win_rate)
if reasons:
result = ProtectionResult(
should_block=True,
reason=" | ".join(reasons),
protection_type=ProtectionType.LOW_PERFORMANCE,
severity=severity
)
self.record_check(blocked=True)
return result
return ProtectionResult(
should_block=False,
reason=f"Performance acceptable (Sharpe: {sharpe:.2f}, Win rate: {win_rate*100:.1f}%)",
protection_type=ProtectionType.LOW_PERFORMANCE,
severity=severity
)