File size: 6,673 Bytes
13a6601
 
 
 
 
 
 
 
 
 
 
 
 
ce9d8fb
 
 
9ff4b7f
 
ce9d8fb
 
9ff4b7f
ce9d8fb
 
 
13a6601
9ff4b7f
 
13a6601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ff4b7f
ce9d8fb
 
 
 
13a6601
 
 
ce9d8fb
 
13a6601
 
 
 
 
 
 
 
 
 
 
 
 
 
ce9d8fb
 
13a6601
 
 
 
ce9d8fb
 
 
13a6601
 
 
ce9d8fb
13a6601
 
ce9d8fb
13a6601
 
 
 
ce9d8fb
13a6601
 
 
 
 
ce9d8fb
13a6601
9ff4b7f
 
 
 
 
 
ce9d8fb
9ff4b7f
13a6601
9ff4b7f
 
 
13a6601
 
 
 
9ff4b7f
 
 
ce9d8fb
9ff4b7f
 
 
 
ce9d8fb
13a6601
 
 
 
ce9d8fb
 
9ff4b7f
13a6601
ce9d8fb
 
 
 
 
 
13a6601
 
 
 
ce9d8fb
 
9ff4b7f
ce9d8fb
 
 
 
9ff4b7f
 
ce9d8fb
 
13a6601
ce9d8fb
13a6601
 
ce9d8fb
13a6601
 
 
 
 
 
 
 
 
 
9ff4b7f
 
ce9d8fb
 
 
13a6601
ce9d8fb
13a6601
 
 
 
ce9d8fb
 
13a6601
ce9d8fb
 
13a6601
ce9d8fb
 
13a6601
 
 
ce9d8fb
13a6601
9ff4b7f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""
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    # never put more than 30% of equity in one trade
_MAX_DRAWDOWN_HALT = 0.15        # halt new positions if equity is down 15% from peak
_ADAPTIVE_STOP_MULT_HIGH = 3.0   # wider stop when vol ratio > HIGH_VOL_THRESHOLD
_ADAPTIVE_STOP_MULT_LOW = 2.0    # tighter stop when vol is compressed


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
    """
    # Hard halt: equity drawn down too far from peak
    if equity_drawdown_pct >= _MAX_DRAWDOWN_HALT:
        return 0.0

    risk = base_risk

    # Consecutive loss scaling
    risk *= consecutive_loss_scale(consec_losses)

    # Volatility adjustment
    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  # also reduce in extreme low vol (thin market)

    # Regime quality
    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

    # Confidence gate: confidence below threshold scales linearly to zero
    if regime_confidence < 0.30:
        risk *= 0.25
    elif regime_confidence < 0.55:
        risk *= regime_confidence  # proportional scaling

    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
    # Hard cap: never exceed _MAX_NOTIONAL_FRACTION of equity in one trade
    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

    # Risk quality: composite readiness score
    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,
    }