GoshawkVortexAI commited on
Commit
13a6601
·
verified ·
1 Parent(s): a461698

Update risk_engine.py

Browse files
Files changed (1) hide show
  1. risk_engine.py +115 -29
risk_engine.py CHANGED
@@ -1,4 +1,16 @@
1
- from typing import Dict, Any
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import numpy as np
4
 
@@ -10,31 +22,88 @@ from config import (
10
  ATR_STOP_MULT,
11
  RR_RATIO,
12
  DEFAULT_ACCOUNT_EQUITY,
 
13
  )
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  def compute_dynamic_risk_fraction(
17
  vol_ratio: float,
18
  regime_score: float,
19
  volume_score: float,
 
 
 
20
  base_risk: float = MAX_RISK_PER_TRADE,
21
  ) -> float:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  risk = base_risk
23
 
 
 
 
 
24
  if vol_ratio > HIGH_VOL_THRESHOLD:
25
  risk *= REDUCED_RISK_FACTOR
26
  elif vol_ratio > HIGH_VOL_THRESHOLD * 0.75:
27
- risk *= 0.75
 
 
28
 
29
- if regime_score < 0.3:
 
30
  risk *= REDUCED_RISK_FACTOR
31
- elif regime_score < 0.5:
32
- risk *= 0.7
 
 
33
 
34
- if volume_score < 0.3:
35
- risk *= 0.8
 
 
 
36
 
37
- return float(np.clip(risk, 0.002, base_risk))
38
 
39
 
40
  def compute_position_size(
@@ -43,11 +112,14 @@ def compute_position_size(
43
  stop_distance: float,
44
  risk_fraction: float,
45
  ) -> float:
46
- if stop_distance <= 0 or entry_price <= 0:
47
  return 0.0
48
  dollar_risk = account_equity * risk_fraction
49
  units = dollar_risk / stop_distance
50
- return units * entry_price
 
 
 
51
 
52
 
53
  def evaluate_risk(
@@ -57,16 +129,24 @@ def evaluate_risk(
57
  regime_score: float,
58
  vol_ratio: float,
59
  volume_score: float = 0.5,
 
 
 
 
60
  account_equity: float = DEFAULT_ACCOUNT_EQUITY,
61
- stop_mult: float = ATR_STOP_MULT,
62
  rr_ratio: float = RR_RATIO,
63
  ) -> Dict[str, Any]:
 
64
  stop_distance = atr * stop_mult
65
 
66
  risk_fraction = compute_dynamic_risk_fraction(
67
  vol_ratio=vol_ratio,
68
  regime_score=regime_score,
69
  volume_score=volume_score,
 
 
 
 
70
  )
71
 
72
  position_notional = compute_position_size(
@@ -78,37 +158,43 @@ def evaluate_risk(
78
 
79
  dollar_at_risk = account_equity * risk_fraction
80
  reward_distance = stop_distance * rr_ratio
 
81
 
82
- stop_long = close - stop_distance
83
- stop_short = close + stop_distance
84
- target_long = close + reward_distance
85
- target_short = close - reward_distance
86
-
87
- leverage_implied = position_notional / account_equity if account_equity > 0 else 1.0
88
-
89
- quality_deduction = 0.0
90
  if vol_ratio > HIGH_VOL_THRESHOLD:
91
- quality_deduction += 0.2
92
- if regime_score < 0.4:
93
- quality_deduction += 0.15
94
- risk_quality = float(np.clip(1.0 - quality_deduction, 0.0, 1.0))
 
 
 
 
 
 
95
 
96
  return {
97
  "entry_price": close,
98
  "atr": round(atr, 8),
99
  "atr_pct": round(atr_pct * 100, 3),
 
100
  "stop_distance": round(stop_distance, 8),
101
- "stop_long": round(stop_long, 8),
102
- "stop_short": round(stop_short, 8),
103
- "target_long": round(target_long, 8),
104
- "target_short": round(target_short, 8),
105
  "reward_distance": round(reward_distance, 8),
106
  "rr_ratio": rr_ratio,
107
- "risk_fraction": round(risk_fraction * 100, 3),
108
  "dollar_at_risk": round(dollar_at_risk, 2),
109
  "position_notional": round(position_notional, 2),
110
- "leverage_implied": round(leverage_implied, 2),
111
  "vol_ratio": round(vol_ratio, 3),
112
  "regime_score": round(regime_score, 4),
 
 
 
113
  "risk_quality": round(risk_quality, 3),
 
114
  }
 
1
+ """
2
+ risk_engine.py — Adaptive risk management with consecutive-loss scaling,
3
+ volatility-percentile-aware position sizing, and Kelly-influenced allocation.
4
+
5
+ Key fixes vs prior version:
6
+ - Consecutive loss counter drives a risk scale table (never compounds losses)
7
+ - ATR stop multiplier is adaptive: widens in high-volatility to avoid noise stops
8
+ - Position size caps at a hard notional limit regardless of risk fraction
9
+ - Regime confidence feeds directly into risk fraction (low confidence = smaller size)
10
+ - Separate max_drawdown_guard: if equity has drawn down >N% from peak, halt sizing
11
+ """
12
+
13
+ from typing import Dict, Any, List
14
 
15
  import numpy as np
16
 
 
22
  ATR_STOP_MULT,
23
  RR_RATIO,
24
  DEFAULT_ACCOUNT_EQUITY,
25
+ CONSEC_LOSS_RISK_SCALE,
26
  )
27
 
28
+ _MAX_NOTIONAL_FRACTION = 0.30 # never put more than 30% of equity in one trade
29
+ _MAX_DRAWDOWN_HALT = 0.15 # halt new positions if equity is down 15% from peak
30
+ _ADAPTIVE_STOP_MULT_HIGH = 3.0 # wider stop when vol ratio > HIGH_VOL_THRESHOLD
31
+ _ADAPTIVE_STOP_MULT_LOW = 2.0 # tighter stop when vol is compressed
32
+
33
+
34
+ def adaptive_stop_multiplier(vol_ratio: float, compressed: bool) -> float:
35
+ """
36
+ Widen ATR stop in high volatility to avoid noise-out.
37
+ Use tighter stop when entering from a compressed base (cleaner structure).
38
+ """
39
+ if vol_ratio > HIGH_VOL_THRESHOLD:
40
+ return _ADAPTIVE_STOP_MULT_HIGH
41
+ if compressed:
42
+ return _ADAPTIVE_STOP_MULT_LOW
43
+ return ATR_STOP_MULT
44
+
45
+
46
+ def consecutive_loss_scale(consec_losses: int) -> float:
47
+ """
48
+ Step-down risk table — each loss reduces risk fraction.
49
+ Prevents geometric compounding of losses during drawdown streaks.
50
+ Table is defined in config.CONSEC_LOSS_RISK_SCALE.
51
+ """
52
+ idx = min(consec_losses, len(CONSEC_LOSS_RISK_SCALE) - 1)
53
+ return CONSEC_LOSS_RISK_SCALE[idx]
54
+
55
 
56
  def compute_dynamic_risk_fraction(
57
  vol_ratio: float,
58
  regime_score: float,
59
  volume_score: float,
60
+ regime_confidence: float,
61
+ consec_losses: int = 0,
62
+ equity_drawdown_pct: float = 0.0,
63
  base_risk: float = MAX_RISK_PER_TRADE,
64
  ) -> float:
65
+ """
66
+ Multi-factor risk fraction with hard halt on drawdown breach.
67
+
68
+ Priority order (each multiplies, not adds):
69
+ 1. Drawdown guard (hard gate)
70
+ 2. Consecutive loss scale
71
+ 3. Volatility regime adjustment
72
+ 4. Regime score quality
73
+ 5. Confidence floor
74
+ """
75
+ # Hard halt: equity drawn down too far from peak
76
+ if equity_drawdown_pct >= _MAX_DRAWDOWN_HALT:
77
+ return 0.0
78
+
79
  risk = base_risk
80
 
81
+ # Consecutive loss scaling
82
+ risk *= consecutive_loss_scale(consec_losses)
83
+
84
+ # Volatility adjustment
85
  if vol_ratio > HIGH_VOL_THRESHOLD:
86
  risk *= REDUCED_RISK_FACTOR
87
  elif vol_ratio > HIGH_VOL_THRESHOLD * 0.75:
88
+ risk *= 0.70
89
+ elif vol_ratio < LOW_VOL_THRESHOLD:
90
+ risk *= 0.80 # also reduce in extreme low vol (thin market)
91
 
92
+ # Regime quality
93
+ if regime_score < 0.25:
94
  risk *= REDUCED_RISK_FACTOR
95
+ elif regime_score < 0.45:
96
+ risk *= 0.65
97
+ elif regime_score < 0.60:
98
+ risk *= 0.85
99
 
100
+ # Confidence gate: confidence below threshold scales linearly to zero
101
+ if regime_confidence < 0.30:
102
+ risk *= 0.25
103
+ elif regime_confidence < 0.55:
104
+ risk *= regime_confidence # proportional scaling
105
 
106
+ return float(np.clip(risk, 0.001, base_risk))
107
 
108
 
109
  def compute_position_size(
 
112
  stop_distance: float,
113
  risk_fraction: float,
114
  ) -> float:
115
+ if stop_distance <= 0 or entry_price <= 0 or account_equity <= 0:
116
  return 0.0
117
  dollar_risk = account_equity * risk_fraction
118
  units = dollar_risk / stop_distance
119
+ notional = units * entry_price
120
+ # Hard cap: never exceed _MAX_NOTIONAL_FRACTION of equity in one trade
121
+ max_notional = account_equity * _MAX_NOTIONAL_FRACTION
122
+ return float(min(notional, max_notional))
123
 
124
 
125
  def evaluate_risk(
 
129
  regime_score: float,
130
  vol_ratio: float,
131
  volume_score: float = 0.5,
132
+ regime_confidence: float = 0.5,
133
+ vol_compressed: bool = False,
134
+ consec_losses: int = 0,
135
+ equity_drawdown_pct: float = 0.0,
136
  account_equity: float = DEFAULT_ACCOUNT_EQUITY,
 
137
  rr_ratio: float = RR_RATIO,
138
  ) -> Dict[str, Any]:
139
+ stop_mult = adaptive_stop_multiplier(vol_ratio, vol_compressed)
140
  stop_distance = atr * stop_mult
141
 
142
  risk_fraction = compute_dynamic_risk_fraction(
143
  vol_ratio=vol_ratio,
144
  regime_score=regime_score,
145
  volume_score=volume_score,
146
+ regime_confidence=regime_confidence,
147
+ consec_losses=consec_losses,
148
+ equity_drawdown_pct=equity_drawdown_pct,
149
+ base_risk=MAX_RISK_PER_TRADE,
150
  )
151
 
152
  position_notional = compute_position_size(
 
158
 
159
  dollar_at_risk = account_equity * risk_fraction
160
  reward_distance = stop_distance * rr_ratio
161
+ leverage_implied = position_notional / account_equity if account_equity > 0 else 0.0
162
 
163
+ # Risk quality: composite readiness score
164
+ quality = 1.0
 
 
 
 
 
 
165
  if vol_ratio > HIGH_VOL_THRESHOLD:
166
+ quality -= 0.25
167
+ if regime_score < 0.40:
168
+ quality -= 0.20
169
+ if regime_confidence < 0.55:
170
+ quality -= 0.15
171
+ if consec_losses >= 2:
172
+ quality -= 0.15
173
+ risk_quality = float(np.clip(quality, 0.0, 1.0))
174
+
175
+ halted = equity_drawdown_pct >= _MAX_DRAWDOWN_HALT
176
 
177
  return {
178
  "entry_price": close,
179
  "atr": round(atr, 8),
180
  "atr_pct": round(atr_pct * 100, 3),
181
+ "stop_mult": round(stop_mult, 2),
182
  "stop_distance": round(stop_distance, 8),
183
+ "stop_long": round(close - stop_distance, 8),
184
+ "stop_short": round(close + stop_distance, 8),
185
+ "target_long": round(close + reward_distance, 8),
186
+ "target_short": round(close - reward_distance, 8),
187
  "reward_distance": round(reward_distance, 8),
188
  "rr_ratio": rr_ratio,
189
+ "risk_fraction": round(risk_fraction * 100, 4),
190
  "dollar_at_risk": round(dollar_at_risk, 2),
191
  "position_notional": round(position_notional, 2),
192
+ "leverage_implied": round(leverage_implied, 3),
193
  "vol_ratio": round(vol_ratio, 3),
194
  "regime_score": round(regime_score, 4),
195
+ "regime_confidence": round(regime_confidence, 4),
196
+ "consec_losses": consec_losses,
197
+ "equity_drawdown_pct": round(equity_drawdown_pct * 100, 2),
198
  "risk_quality": round(risk_quality, 3),
199
+ "sizing_halted": halted,
200
  }