Files
NexQuant/rdagent/components/backtesting/protections/base.py
T
TPTBusiness 421a3889fa 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.
2026-04-03 13:01:56 +02:00

146 lines
4.3 KiB
Python

"""
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
}