mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
feat: Add Trading Protection System with 4 protections + comprehensive tests
Implement automatic trading protection system to prevent excessive losses: PROTECTIONS (100% original code, NOT copied from Freqtrade): - Max Drawdown Protection: Blocks trading when DD > 15% (configurable) - Cooldown Period: 4h mandatory rest after 5% loss - Stoploss Guard: Detects stoploss clusters (>5 per day) - Low Performance Filter: Filters factors with Sharpe < 0.5, Win Rate < 40% ARCHITECTURE: - Base protection interface with common utilities - 4 specialized protection implementations - ProtectionManager orchestrates all active protections - Time-based blocking with automatic expiry TESTS (32 total, ALL PASS): - 25 unit tests in test/backtesting/test_protections.py - 7 integration tests in test/integration/test_all_features.py - Tests cover: normal operation, edge cases, error handling DOCUMENTATION: - Update QWEN.md with development guidelines for AI assistant * Mandatory rules: Update QWEN.md, README, requirements.txt, tests * Pre-commit checklist * Example workflow - Update README.md with protection system features - Update project structure with new modules All code is 100% original - NO license issues with Freqtrade GPLv3.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Trading Protection System for Predix.
|
||||
|
||||
Prevents excessive losses by automatically pausing trading
|
||||
when risk thresholds are exceeded.
|
||||
|
||||
Usage:
|
||||
from rdagent.components.backtesting.protections import (
|
||||
ProtectionManager,
|
||||
MaxDrawdownProtection,
|
||||
CooldownProtection,
|
||||
StoplossGuardProtection,
|
||||
LowPerformanceProtection,
|
||||
)
|
||||
|
||||
manager = ProtectionManager()
|
||||
manager.create_default_protections()
|
||||
|
||||
result = manager.check_all(
|
||||
returns=[0.01, -0.02, 0.015],
|
||||
timestamps=[...],
|
||||
current_equity=98000,
|
||||
peak_equity=100000
|
||||
)
|
||||
|
||||
if result.should_block:
|
||||
print(f"Trading blocked: {result.reason}")
|
||||
"""
|
||||
|
||||
from .base import (
|
||||
BaseProtection,
|
||||
ProtectionConfig,
|
||||
ProtectionResult,
|
||||
ProtectionType,
|
||||
ProtectionScope,
|
||||
)
|
||||
from .max_drawdown import MaxDrawdownProtection, MaxDrawdownConfig
|
||||
from .cooldown import CooldownProtection, CooldownConfig
|
||||
from .stoploss_guard import StoplossGuardProtection, StoplossGuardConfig
|
||||
from .low_performance import LowPerformanceProtection, LowPerformanceConfig
|
||||
from .protection_manager import ProtectionManager
|
||||
|
||||
__all__ = [
|
||||
"BaseProtection",
|
||||
"ProtectionConfig",
|
||||
"ProtectionResult",
|
||||
"ProtectionType",
|
||||
"ProtectionScope",
|
||||
"MaxDrawdownProtection",
|
||||
"MaxDrawdownConfig",
|
||||
"CooldownProtection",
|
||||
"CooldownConfig",
|
||||
"StoplossGuardProtection",
|
||||
"StoplossGuardConfig",
|
||||
"LowPerformanceProtection",
|
||||
"LowPerformanceConfig",
|
||||
"ProtectionManager",
|
||||
]
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Trading Protection System
|
||||
|
||||
Prevents excessive losses by automatically pausing trading when risk thresholds are exceeded.
|
||||
|
||||
Inspired by common trading protection patterns, implemented from scratch for Predix.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ProtectionType(Enum):
|
||||
"""Type of protection mechanism."""
|
||||
MAX_DRAWDOWN = "max_drawdown"
|
||||
COOLDOWN = "cooldown"
|
||||
STOPLOSS_GUARD = "stoploss_guard"
|
||||
LOW_PERFORMANCE = "low_performance"
|
||||
|
||||
|
||||
class ProtectionScope(Enum):
|
||||
"""What scope does this protection apply to?"""
|
||||
GLOBAL = "global" # All trading
|
||||
FACTOR = "factor" # Per-factor
|
||||
PORTFOLIO = "portfolio" # Per portfolio
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProtectionResult:
|
||||
"""Result of a protection check."""
|
||||
should_block: bool # True if trading should be blocked
|
||||
reason: str # Why was it blocked?
|
||||
until: Optional[datetime] = None # If time-based, when does it expire?
|
||||
protection_type: Optional[ProtectionType] = None
|
||||
severity: float = 0.0 # How severe is the issue (0-1)
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""Check if protection is currently active."""
|
||||
if not self.until:
|
||||
return self.should_block
|
||||
return datetime.now() < self.until and self.should_block
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProtectionConfig:
|
||||
"""Base configuration for a protection."""
|
||||
enabled: bool = True
|
||||
lookback_period_hours: int = 24 # How far back to look
|
||||
severity_threshold: float = 0.8 # At what severity to block
|
||||
|
||||
|
||||
class BaseProtection(ABC):
|
||||
"""
|
||||
Base class for all trading protections.
|
||||
|
||||
Each protection checks specific conditions and returns ProtectionResult.
|
||||
Multiple protections can be combined in ProtectionManager.
|
||||
"""
|
||||
|
||||
def __init__(self, config: ProtectionConfig):
|
||||
self.config = config
|
||||
self.last_check: Optional[datetime] = None
|
||||
self.total_checks: int = 0
|
||||
self.total_blocks: int = 0
|
||||
|
||||
@abstractmethod
|
||||
def check(
|
||||
self,
|
||||
returns: list[float],
|
||||
timestamps: list[datetime],
|
||||
current_equity: float,
|
||||
peak_equity: float,
|
||||
**kwargs
|
||||
) -> ProtectionResult:
|
||||
"""
|
||||
Check if protection should be triggered.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
returns : list[float]
|
||||
Historical returns in lookback period
|
||||
timestamps : list[datetime]
|
||||
Timestamps of returns
|
||||
current_equity : float
|
||||
Current equity value
|
||||
peak_equity : float
|
||||
Peak equity value (highest ever)
|
||||
**kwargs
|
||||
Additional context (factor name, portfolio ID, etc.)
|
||||
|
||||
Returns
|
||||
-------
|
||||
ProtectionResult
|
||||
Decision on whether to block trading
|
||||
"""
|
||||
pass
|
||||
|
||||
def calculate_drawdown(self, current: float, peak: float) -> float:
|
||||
"""Calculate drawdown percentage (negative value)."""
|
||||
if peak == 0:
|
||||
return 0.0
|
||||
return (current - peak) / peak
|
||||
|
||||
def calculate_max_consecutive_losses(self, returns: list[float]) -> int:
|
||||
"""Find maximum consecutive losing trades."""
|
||||
max_losses = 0
|
||||
current_losses = 0
|
||||
|
||||
for ret in returns:
|
||||
if ret < 0:
|
||||
current_losses += 1
|
||||
max_losses = max(max_losses, current_losses)
|
||||
else:
|
||||
current_losses = 0
|
||||
|
||||
return max_losses
|
||||
|
||||
def calculate_recent_loss_rate(self, returns: list[float]) -> float:
|
||||
"""Calculate percentage of losing trades."""
|
||||
if not returns:
|
||||
return 0.0
|
||||
losses = sum(1 for r in returns if r < 0)
|
||||
return losses / len(returns)
|
||||
|
||||
def record_check(self, blocked: bool = False):
|
||||
"""Record that a check was performed."""
|
||||
self.total_checks += 1
|
||||
self.last_check = datetime.now()
|
||||
if blocked:
|
||||
self.total_blocks += 1
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Get protection statistics."""
|
||||
return {
|
||||
"type": self.__class__.__name__,
|
||||
"enabled": self.config.enabled,
|
||||
"total_checks": self.total_checks,
|
||||
"total_blocks": self.total_blocks,
|
||||
"block_rate": self.total_blocks / max(1, self.total_checks),
|
||||
"last_check": self.last_check.isoformat() if self.last_check else None
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Cooldown Period Protection
|
||||
|
||||
Enforces mandatory rest periods after losses.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from .base import BaseProtection, ProtectionConfig, ProtectionResult, ProtectionType, ProtectionScope
|
||||
|
||||
|
||||
@dataclass
|
||||
class CooldownConfig(ProtectionConfig):
|
||||
"""Configuration for Cooldown protection."""
|
||||
cooldown_after_loss_pct: float = 0.05 # Cooldown after 5% loss
|
||||
cooldown_duration_hours: int = 4 # How long to wait
|
||||
|
||||
|
||||
class CooldownProtection(BaseProtection):
|
||||
"""
|
||||
Enforces cooling-off period after significant losses.
|
||||
|
||||
Prevents revenge trading and emotional decisions by forcing
|
||||
a mandatory break after losses exceed threshold.
|
||||
"""
|
||||
|
||||
def __init__(self, config: CooldownConfig):
|
||||
super().__init__(config)
|
||||
self.config: CooldownConfig = config
|
||||
self.last_loss_time: Optional[datetime] = None
|
||||
self.last_loss_pct: float = 0.0
|
||||
|
||||
@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 cooldown should be triggered."""
|
||||
self.record_check()
|
||||
|
||||
if not self.config.enabled:
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason="Protection disabled",
|
||||
protection_type=ProtectionType.COOLDOWN
|
||||
)
|
||||
|
||||
# Check most recent return
|
||||
if returns:
|
||||
latest_return = returns[-1]
|
||||
latest_time = timestamps[-1] if timestamps else datetime.now()
|
||||
|
||||
if latest_return < -self.config.cooldown_after_loss_pct:
|
||||
self.last_loss_time = latest_time
|
||||
self.last_loss_pct = latest_return
|
||||
|
||||
# If recently had big loss, enforce cooldown
|
||||
if self.last_loss_time:
|
||||
time_since_loss = datetime.now() - self.last_loss_time
|
||||
if time_since_loss < timedelta(hours=self.config.cooldown_duration_hours):
|
||||
remaining = timedelta(hours=self.config.cooldown_duration_hours) - time_since_loss
|
||||
|
||||
result = ProtectionResult(
|
||||
should_block=True,
|
||||
reason=f"Loss of {abs(self.last_loss_pct)*100:.1f}% - cooling down for {remaining.seconds // 3600}h",
|
||||
until=self.last_loss_time + timedelta(hours=self.config.cooldown_duration_hours),
|
||||
protection_type=ProtectionType.COOLDOWN,
|
||||
severity=abs(self.last_loss_pct) / self.config.cooldown_after_loss_pct
|
||||
)
|
||||
self.record_check(blocked=True)
|
||||
return result
|
||||
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason="No recent significant losses",
|
||||
protection_type=ProtectionType.COOLDOWN,
|
||||
severity=0.0
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Maximum Drawdown Protection
|
||||
|
||||
Blocks trading when portfolio drawdown exceeds threshold.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from .base import BaseProtection, ProtectionConfig, ProtectionResult, ProtectionType, ProtectionScope
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaxDrawdownConfig(ProtectionConfig):
|
||||
"""Configuration for MaxDrawdown protection."""
|
||||
max_drawdown_pct: float = 0.15 # Block if drawdown > 15%
|
||||
|
||||
|
||||
class MaxDrawdownProtection(BaseProtection):
|
||||
"""
|
||||
Blocks trading when drawdown exceeds safe threshold.
|
||||
|
||||
This prevents the system from continuing to trade during a losing streak,
|
||||
giving the market time to stabilize.
|
||||
"""
|
||||
|
||||
def __init__(self, config: MaxDrawdownConfig):
|
||||
super().__init__(config)
|
||||
self.config: MaxDrawdownConfig = config
|
||||
|
||||
@property
|
||||
def scope(self) -> ProtectionScope:
|
||||
return ProtectionScope.GLOBAL
|
||||
|
||||
def check(
|
||||
self,
|
||||
returns: list[float],
|
||||
timestamps: list[datetime],
|
||||
current_equity: float,
|
||||
peak_equity: float,
|
||||
**kwargs
|
||||
) -> ProtectionResult:
|
||||
"""Check if drawdown exceeds threshold."""
|
||||
self.record_check()
|
||||
|
||||
if not self.config.enabled:
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason="Protection disabled",
|
||||
protection_type=ProtectionType.MAX_DRAWDOWN
|
||||
)
|
||||
|
||||
drawdown = self.calculate_drawdown(current_equity, peak_equity)
|
||||
severity = abs(drawdown) / self.config.max_drawdown_pct if self.config.max_drawdown_pct > 0 else 0
|
||||
|
||||
if abs(drawdown) > self.config.max_drawdown_pct:
|
||||
# Calculate block duration based on severity
|
||||
block_hours = int(self.config.lookback_period_hours * severity)
|
||||
block_hours = min(block_hours, 168) # Max 1 week
|
||||
|
||||
result = ProtectionResult(
|
||||
should_block=True,
|
||||
reason=f"Drawdown {abs(drawdown)*100:.1f}% exceeds max {self.config.max_drawdown_pct*100:.1f}%",
|
||||
until=datetime.now() + timedelta(hours=block_hours),
|
||||
protection_type=ProtectionType.MAX_DRAWDOWN,
|
||||
severity=severity
|
||||
)
|
||||
self.record_check(blocked=True)
|
||||
return result
|
||||
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason=f"Drawdown {abs(drawdown)*100:.1f}% within safe range",
|
||||
protection_type=ProtectionType.MAX_DRAWDOWN,
|
||||
severity=severity
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Protection Manager
|
||||
|
||||
Orchestrates multiple trading protections.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from .base import (
|
||||
BaseProtection,
|
||||
ProtectionConfig,
|
||||
ProtectionResult,
|
||||
ProtectionType,
|
||||
ProtectionScope
|
||||
)
|
||||
from .max_drawdown import MaxDrawdownProtection, MaxDrawdownConfig
|
||||
from .cooldown import CooldownProtection, CooldownConfig
|
||||
from .stoploss_guard import StoplossGuardProtection, StoplossGuardConfig
|
||||
from .low_performance import LowPerformanceProtection, LowPerformanceConfig
|
||||
|
||||
|
||||
class ProtectionManager:
|
||||
"""
|
||||
Manages multiple trading protections.
|
||||
|
||||
Run all active protections and aggregate their results.
|
||||
If ANY protection returns should_block=True, trading is blocked.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.protections: List[BaseProtection] = []
|
||||
self.active_blocks: List[ProtectionResult] = []
|
||||
|
||||
def add_protection(self, protection: BaseProtection):
|
||||
"""Add a protection to the manager."""
|
||||
self.protections.append(protection)
|
||||
|
||||
def remove_protection(self, protection_type: ProtectionType):
|
||||
"""Remove a protection by type."""
|
||||
type_to_name = {
|
||||
ProtectionType.MAX_DRAWDOWN: "MaxDrawdownProtection",
|
||||
ProtectionType.COOLDOWN: "CooldownProtection",
|
||||
ProtectionType.STOPLOSS_GUARD: "StoplossGuardProtection",
|
||||
ProtectionType.LOW_PERFORMANCE: "LowPerformanceProtection",
|
||||
}
|
||||
class_name = type_to_name.get(protection_type, protection_type.value)
|
||||
self.protections = [
|
||||
p for p in self.protections
|
||||
if p.__class__.__name__ != class_name
|
||||
]
|
||||
|
||||
def check_all(
|
||||
self,
|
||||
returns: list[float],
|
||||
timestamps: list[datetime],
|
||||
current_equity: float,
|
||||
peak_equity: float,
|
||||
**kwargs
|
||||
) -> ProtectionResult:
|
||||
"""
|
||||
Run all protections and aggregate results.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ProtectionResult
|
||||
Combined result from all protections
|
||||
"""
|
||||
all_results = []
|
||||
|
||||
for protection in self.protections:
|
||||
result = protection.check(
|
||||
returns=returns,
|
||||
timestamps=timestamps,
|
||||
current_equity=current_equity,
|
||||
peak_equity=peak_equity,
|
||||
**kwargs
|
||||
)
|
||||
all_results.append(result)
|
||||
|
||||
# Check if any protection is blocking
|
||||
blocking = [r for r in all_results if r.should_block]
|
||||
|
||||
if blocking:
|
||||
# Find most severe block
|
||||
most_severe = max(blocking, key=lambda r: r.severity)
|
||||
|
||||
# Clean up expired blocks
|
||||
self.active_blocks = [
|
||||
b for b in self.active_blocks
|
||||
if b.until is None or datetime.now() < b.until
|
||||
]
|
||||
|
||||
# Add new block
|
||||
self.active_blocks.append(most_severe)
|
||||
|
||||
# Combine reasons
|
||||
reasons = [r.reason for r in blocking]
|
||||
|
||||
return ProtectionResult(
|
||||
should_block=True,
|
||||
reason=f"Trading blocked: {'; '.join(reasons)}",
|
||||
until=most_severe.until,
|
||||
severity=most_severe.severity
|
||||
)
|
||||
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason="All protections passed",
|
||||
severity=0.0
|
||||
)
|
||||
|
||||
def get_active_blocks(self) -> List[ProtectionResult]:
|
||||
"""Get currently active protection blocks."""
|
||||
# Clean up expired
|
||||
self.active_blocks = [
|
||||
b for b in self.active_blocks
|
||||
if b.until is None or datetime.now() < b.until
|
||||
]
|
||||
return self.active_blocks
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Get statistics for all protections."""
|
||||
return {
|
||||
"total_protections": len(self.protections),
|
||||
"active_blocks": len(self.get_active_blocks()),
|
||||
"protections": [p.get_stats() for p in self.protections]
|
||||
}
|
||||
|
||||
def create_default_protections(self):
|
||||
"""Create standard protection setup."""
|
||||
# Max Drawdown: 15% threshold
|
||||
self.add_protection(
|
||||
MaxDrawdownProtection(
|
||||
MaxDrawdownConfig(
|
||||
enabled=True,
|
||||
max_drawdown_pct=0.15,
|
||||
lookback_period_hours=168 # 1 week
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Cooldown: 4 hours after 5% loss
|
||||
self.add_protection(
|
||||
CooldownProtection(
|
||||
CooldownConfig(
|
||||
enabled=True,
|
||||
cooldown_after_loss_pct=0.05,
|
||||
cooldown_duration_hours=4
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Stoploss Guard: Max 5 stoplosses per day
|
||||
self.add_protection(
|
||||
StoplossGuardProtection(
|
||||
StoplossGuardConfig(
|
||||
enabled=True,
|
||||
max_stoplosses_in_period=5,
|
||||
stoploss_threshold_pct=0.02,
|
||||
lookback_period_hours=24
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Low Performance: Filter bad factors
|
||||
self.add_protection(
|
||||
LowPerformanceProtection(
|
||||
LowPerformanceConfig(
|
||||
enabled=True,
|
||||
min_sharpe_ratio=0.5,
|
||||
min_win_rate=0.40,
|
||||
min_trades=20,
|
||||
lookback_period_hours=720 # 30 days
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Stoploss Guard Protection
|
||||
|
||||
Detects clusters of stoplosses and blocks trading.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from .base import BaseProtection, ProtectionConfig, ProtectionResult, ProtectionType, ProtectionScope
|
||||
|
||||
|
||||
@dataclass
|
||||
class StoplossGuardConfig(ProtectionConfig):
|
||||
"""Configuration for StoplossGuard protection."""
|
||||
max_stoplosses_in_period: int = 5 # Max stoplosses allowed
|
||||
stoploss_threshold_pct: float = 0.02 # What counts as stoploss (2%)
|
||||
|
||||
|
||||
class StoplossGuardProtection(BaseProtection):
|
||||
"""
|
||||
Detects stoploss clusters and blocks trading.
|
||||
|
||||
Multiple stoplosses in short time indicates bad market conditions
|
||||
or strategy no longer working.
|
||||
"""
|
||||
|
||||
def __init__(self, config: StoplossGuardConfig):
|
||||
super().__init__(config)
|
||||
self.config: StoplossGuardConfig = config
|
||||
|
||||
@property
|
||||
def scope(self) -> ProtectionScope:
|
||||
return ProtectionScope.GLOBAL
|
||||
|
||||
def check(
|
||||
self,
|
||||
returns: list[float],
|
||||
timestamps: list[datetime],
|
||||
current_equity: float,
|
||||
peak_equity: float,
|
||||
**kwargs
|
||||
) -> ProtectionResult:
|
||||
"""Check for stoploss clusters."""
|
||||
self.record_check()
|
||||
|
||||
if not self.config.enabled:
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason="Protection disabled",
|
||||
protection_type=ProtectionType.STOPLOSS_GUARD
|
||||
)
|
||||
|
||||
# Count stoplosses (large losses)
|
||||
stoplosses = [
|
||||
r for r in returns
|
||||
if r < -self.config.stoploss_threshold_pct
|
||||
]
|
||||
|
||||
if len(stoplosses) > self.config.max_stoplosses_in_period:
|
||||
severity = len(stoplosses) / self.config.max_stoplosses_in_period
|
||||
|
||||
result = ProtectionResult(
|
||||
should_block=True,
|
||||
reason=f"{len(stoplosses)} stoplosses detected (max {self.config.max_stoplosses_in_period})",
|
||||
protection_type=ProtectionType.STOPLOSS_GUARD,
|
||||
severity=severity
|
||||
)
|
||||
self.record_check(blocked=True)
|
||||
return result
|
||||
|
||||
return ProtectionResult(
|
||||
should_block=False,
|
||||
reason=f"{len(stoplosses)} stoplosses (within limit)",
|
||||
protection_type=ProtectionType.STOPLOSS_GUARD,
|
||||
severity=len(stoplosses) / max(1, self.config.max_stoplosses_in_period)
|
||||
)
|
||||
Reference in New Issue
Block a user