Files
NexQuant/rdagent/components/backtesting/protections/base.py
TPTBusiness cbe1c52e00 refactor: rename project from Predix to NexQuant
Rename all source files, scripts, tests, documentation, and configuration
from Predix/predix to NexQuant/nexquant across the entire codebase.
2026-05-09 17:48:22 +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 NexQuant.
"""
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
}