| | """ |
| | risk_engine.py — Adaptive risk management with consecutive-loss scaling, |
| | volatility-percentile-aware position sizing, and Kelly-influenced allocation. |
| | |
| | Key fixes vs prior version: |
| | - Consecutive loss counter drives a risk scale table (never compounds losses) |
| | - ATR stop multiplier is adaptive: widens in high-volatility to avoid noise stops |
| | - Position size caps at a hard notional limit regardless of risk fraction |
| | - Regime confidence feeds directly into risk fraction (low confidence = smaller size) |
| | - Separate max_drawdown_guard: if equity has drawn down >N% from peak, halt sizing |
| | """ |
| |
|
| | from typing import Dict, Any, List |
| |
|
| | import numpy as np |
| |
|
| | from config import ( |
| | MAX_RISK_PER_TRADE, |
| | HIGH_VOL_THRESHOLD, |
| | LOW_VOL_THRESHOLD, |
| | REDUCED_RISK_FACTOR, |
| | ATR_STOP_MULT, |
| | RR_RATIO, |
| | DEFAULT_ACCOUNT_EQUITY, |
| | CONSEC_LOSS_RISK_SCALE, |
| | ) |
| |
|
| | _MAX_NOTIONAL_FRACTION = 0.30 |
| | _MAX_DRAWDOWN_HALT = 0.15 |
| | _ADAPTIVE_STOP_MULT_HIGH = 3.0 |
| | _ADAPTIVE_STOP_MULT_LOW = 2.0 |
| |
|
| |
|
| | def adaptive_stop_multiplier(vol_ratio: float, compressed: bool) -> float: |
| | """ |
| | Widen ATR stop in high volatility to avoid noise-out. |
| | Use tighter stop when entering from a compressed base (cleaner structure). |
| | """ |
| | if vol_ratio > HIGH_VOL_THRESHOLD: |
| | return _ADAPTIVE_STOP_MULT_HIGH |
| | if compressed: |
| | return _ADAPTIVE_STOP_MULT_LOW |
| | return ATR_STOP_MULT |
| |
|
| |
|
| | def consecutive_loss_scale(consec_losses: int) -> float: |
| | """ |
| | Step-down risk table — each loss reduces risk fraction. |
| | Prevents geometric compounding of losses during drawdown streaks. |
| | Table is defined in config.CONSEC_LOSS_RISK_SCALE. |
| | """ |
| | idx = min(consec_losses, len(CONSEC_LOSS_RISK_SCALE) - 1) |
| | return CONSEC_LOSS_RISK_SCALE[idx] |
| |
|
| |
|
| | def compute_dynamic_risk_fraction( |
| | vol_ratio: float, |
| | regime_score: float, |
| | volume_score: float, |
| | regime_confidence: float, |
| | consec_losses: int = 0, |
| | equity_drawdown_pct: float = 0.0, |
| | base_risk: float = MAX_RISK_PER_TRADE, |
| | ) -> float: |
| | """ |
| | Multi-factor risk fraction with hard halt on drawdown breach. |
| | |
| | Priority order (each multiplies, not adds): |
| | 1. Drawdown guard (hard gate) |
| | 2. Consecutive loss scale |
| | 3. Volatility regime adjustment |
| | 4. Regime score quality |
| | 5. Confidence floor |
| | """ |
| | |
| | if equity_drawdown_pct >= _MAX_DRAWDOWN_HALT: |
| | return 0.0 |
| |
|
| | risk = base_risk |
| |
|
| | |
| | risk *= consecutive_loss_scale(consec_losses) |
| |
|
| | |
| | if vol_ratio > HIGH_VOL_THRESHOLD: |
| | risk *= REDUCED_RISK_FACTOR |
| | elif vol_ratio > HIGH_VOL_THRESHOLD * 0.75: |
| | risk *= 0.70 |
| | elif vol_ratio < LOW_VOL_THRESHOLD: |
| | risk *= 0.80 |
| |
|
| | |
| | if regime_score < 0.25: |
| | risk *= REDUCED_RISK_FACTOR |
| | elif regime_score < 0.45: |
| | risk *= 0.65 |
| | elif regime_score < 0.60: |
| | risk *= 0.85 |
| |
|
| | |
| | if regime_confidence < 0.30: |
| | risk *= 0.25 |
| | elif regime_confidence < 0.55: |
| | risk *= regime_confidence |
| |
|
| | return float(np.clip(risk, 0.001, base_risk)) |
| |
|
| |
|
| | def compute_position_size( |
| | account_equity: float, |
| | entry_price: float, |
| | stop_distance: float, |
| | risk_fraction: float, |
| | ) -> float: |
| | if stop_distance <= 0 or entry_price <= 0 or account_equity <= 0: |
| | return 0.0 |
| | dollar_risk = account_equity * risk_fraction |
| | units = dollar_risk / stop_distance |
| | notional = units * entry_price |
| | |
| | max_notional = account_equity * _MAX_NOTIONAL_FRACTION |
| | return float(min(notional, max_notional)) |
| |
|
| |
|
| | def evaluate_risk( |
| | close: float, |
| | atr: float, |
| | atr_pct: float, |
| | regime_score: float, |
| | vol_ratio: float, |
| | volume_score: float = 0.5, |
| | regime_confidence: float = 0.5, |
| | vol_compressed: bool = False, |
| | consec_losses: int = 0, |
| | equity_drawdown_pct: float = 0.0, |
| | account_equity: float = DEFAULT_ACCOUNT_EQUITY, |
| | rr_ratio: float = RR_RATIO, |
| | ) -> Dict[str, Any]: |
| | stop_mult = adaptive_stop_multiplier(vol_ratio, vol_compressed) |
| | stop_distance = atr * stop_mult |
| |
|
| | risk_fraction = compute_dynamic_risk_fraction( |
| | vol_ratio=vol_ratio, |
| | regime_score=regime_score, |
| | volume_score=volume_score, |
| | regime_confidence=regime_confidence, |
| | consec_losses=consec_losses, |
| | equity_drawdown_pct=equity_drawdown_pct, |
| | base_risk=MAX_RISK_PER_TRADE, |
| | ) |
| |
|
| | position_notional = compute_position_size( |
| | account_equity=account_equity, |
| | entry_price=close, |
| | stop_distance=stop_distance, |
| | risk_fraction=risk_fraction, |
| | ) |
| |
|
| | dollar_at_risk = account_equity * risk_fraction |
| | reward_distance = stop_distance * rr_ratio |
| | leverage_implied = position_notional / account_equity if account_equity > 0 else 0.0 |
| |
|
| | |
| | quality = 1.0 |
| | if vol_ratio > HIGH_VOL_THRESHOLD: |
| | quality -= 0.25 |
| | if regime_score < 0.40: |
| | quality -= 0.20 |
| | if regime_confidence < 0.55: |
| | quality -= 0.15 |
| | if consec_losses >= 2: |
| | quality -= 0.15 |
| | risk_quality = float(np.clip(quality, 0.0, 1.0)) |
| |
|
| | halted = equity_drawdown_pct >= _MAX_DRAWDOWN_HALT |
| |
|
| | return { |
| | "entry_price": close, |
| | "atr": round(atr, 8), |
| | "atr_pct": round(atr_pct * 100, 3), |
| | "stop_mult": round(stop_mult, 2), |
| | "stop_distance": round(stop_distance, 8), |
| | "stop_long": round(close - stop_distance, 8), |
| | "stop_short": round(close + stop_distance, 8), |
| | "target_long": round(close + reward_distance, 8), |
| | "target_short": round(close - reward_distance, 8), |
| | "reward_distance": round(reward_distance, 8), |
| | "rr_ratio": rr_ratio, |
| | "risk_fraction": round(risk_fraction * 100, 4), |
| | "dollar_at_risk": round(dollar_at_risk, 2), |
| | "position_notional": round(position_notional, 2), |
| | "leverage_implied": round(leverage_implied, 3), |
| | "vol_ratio": round(vol_ratio, 3), |
| | "regime_score": round(regime_score, 4), |
| | "regime_confidence": round(regime_confidence, 4), |
| | "consec_losses": consec_losses, |
| | "equity_drawdown_pct": round(equity_drawdown_pct * 100, 2), |
| | "risk_quality": round(risk_quality, 3), |
| | "sizing_halted": halted, |
| | } |
| |
|