File size: 14,077 Bytes
fc115d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
"""
Advanced Risk Management Module

Features:
- Adaptive SL/TP based on ATR volatility
- Kelly Criterion position sizing
- Trailing stops
- Dynamic risk per trade
"""

import numpy as np
import pandas as pd
from typing import Dict, Tuple, Optional
from dataclasses import dataclass
import logging

logger = logging.getLogger(__name__)


@dataclass
class RiskParameters:
    """Dynamic risk parameters."""
    stop_loss_pct: float
    take_profit_pct: float
    position_size: float
    trailing_stop_pct: Optional[float]
    risk_reward_ratio: float


class AdaptiveRiskManager:
    """
    Advanced risk management with dynamic SL/TP and position sizing.
    
    Key features:
    - ATR-based stop loss and take profit
    - Kelly Criterion for optimal position sizing
    - Trailing stops for maximizing winners
    """
    
    def __init__(
        self,
        base_sl_pct: float = 0.05,   # 5.0% base stop loss (increased from 2%)
        base_tp_pct: float = 0.10,   # 10.0% base take profit (increased from 5%)
        base_position_size: float = 0.5,
        min_sl_pct: float = 0.02,    # Minimum 2% SL
        max_sl_pct: float = 0.10,    # Maximum 10% SL
        min_tp_pct: float = 0.04,    # Minimum 4% TP
        max_tp_pct: float = 0.20,    # Maximum 20% TP
        min_position_size: float = 0.1,
        max_position_size: float = 0.75,
        atr_period: int = 14,
        use_kelly: bool = True,
        use_trailing: bool = True,
    ):
        self.base_sl_pct = base_sl_pct
        self.base_tp_pct = base_tp_pct
        self.base_position_size = base_position_size
        self.min_sl_pct = min_sl_pct
        self.max_sl_pct = max_sl_pct
        self.min_tp_pct = min_tp_pct
        self.max_tp_pct = max_tp_pct
        self.min_position_size = min_position_size
        self.max_position_size = max_position_size
        self.atr_period = atr_period
        self.use_kelly = use_kelly
        self.use_trailing = use_trailing
        
        # Trade history for Kelly calculation
        self.trade_history: list = []

        logger.info(f"πŸ“Š AdaptiveRiskManager initialized (Kelly={use_kelly}, Trailing={use_trailing})")

    def get_asset_specific_params(self, symbol: str) -> Tuple[float, float]:
        """
        Get asset-specific SL/TP base parameters based on typical volatility.

        BTC: Lower volatility β†’ Tighter stops (1.5% SL, 3.5% TP)
        ETH: Medium volatility β†’ Standard stops (2.0% SL, 5.0% TP)
        SOL: Higher volatility β†’ Wider stops (2.5% SL, 6.0% TP)
        XRP: Medium-high volatility β†’ Medium-wide stops (2.0% SL, 5.5% TP)
        """
        symbol_upper = symbol.upper().replace('USDT', '')

        asset_params = {
            'BTC': (0.015, 0.035),  # 1.5% SL, 3.5% TP (lowest volatility)
            'ETH': (0.020, 0.050),  # 2.0% SL, 5.0% TP (medium volatility)
            'SOL': (0.025, 0.060),  # 2.5% SL, 6.0% TP (highest volatility)
            'XRP': (0.020, 0.055),  # 2.0% SL, 5.5% TP (medium-high volatility)
        }

        sl_pct, tp_pct = asset_params.get(symbol_upper, (self.base_sl_pct, self.base_tp_pct))

        logger.info(f"πŸ“Š Asset-specific params for {symbol_upper}: SL={sl_pct:.2%}, TP={tp_pct:.2%}")
        return sl_pct, tp_pct

    def calculate_atr(self, df: pd.DataFrame, period: int = None) -> float:
        """Calculate Average True Range."""
        period = period or self.atr_period
        
        high = df['high'].values
        low = df['low'].values
        close = df['close'].values
        
        tr1 = high - low
        tr2 = np.abs(high - np.roll(close, 1))
        tr3 = np.abs(low - np.roll(close, 1))
        
        tr = np.maximum(np.maximum(tr1, tr2), tr3)
        atr = np.mean(tr[-period:])
        
        return atr
    
    def get_adaptive_sl_tp(self, df: pd.DataFrame, trade_type: str = "long") -> Tuple[float, float]:
        """
        Calculate adaptive SL/TP using direct ATR multipliers.
        
        Provides robust stops that sit outside normal market noise.
        SL: 2.5x ATR
        TP: 4.0x ATR
        """
        current_price = df['close'].iloc[-1]
        
        try:
            atr = self.calculate_atr(df)
            atr_pct = atr / current_price
            
            # Use direct ATR multipliers for robust crypto stops
            sl_pct = atr_pct * 2.5
            tp_pct = atr_pct * 4.0
            
            # Clamp to min/max safety rails
            sl_pct = np.clip(sl_pct, self.min_sl_pct, self.max_sl_pct)
            tp_pct = np.clip(tp_pct, self.min_tp_pct, self.max_tp_pct)
            
            logger.info(
                f"πŸ“Š Adaptive SL/TP: ATR={atr:.2f} ({atr_pct:.2%}) "
                f"β†’ SL={sl_pct:.2%}, TP={tp_pct:.2%}"
            )
            return sl_pct, tp_pct
            
        except Exception as e:
            logger.warning(f"Failed to calc ATR-based SL/TP: {e}. Using base defaults.")
            return self.base_sl_pct, self.base_tp_pct

    def get_structural_sl_tp(self, df: pd.DataFrame, trade_type: str = "long", symbol: str = "") -> Tuple[float, float]:
        """
        Calculate Structural SL/TP using VWAP and recent swing highs/lows.
        Places the stop loss just beyond the nearest structural support/resistance
        to prevent being wicked out by noise.

        Uses asset-specific base parameters for min/max clamping.
        """
        if len(df) < 24:
            return self.get_adaptive_sl_tp(df, trade_type)

        # Get asset-specific base parameters for appropriate min/max clamping
        asset_sl_base, asset_tp_base = self.get_asset_specific_params(symbol) if symbol else (self.base_sl_pct, self.base_tp_pct)
            
        current_price = df['close'].iloc[-1]
        
        try:
            # 1. Calculate VWAP (approximate support/resistance)
            # typical price = (H+L+C)/3
            tp = (df['high'] + df['low'] + df['close']) / 3
            vwap = (tp * df['volume']).sum() / df['volume'].sum() if df['volume'].sum() > 0 else current_price
            
            # 2. Get local swings (last 24 periods)
            recent_low = df['low'].tail(24).min()
            recent_high = df['high'].tail(24).max()
            
            # 3. Calculate ATR for a small buffer (0.5x ATR buffer past structure)
            atr = self.calculate_atr(df)
            buffer = atr * 0.5
            
            if trade_type == "long":
                # For LONG: SL should be slightly below the nearest structure (VWAP or Swing Low)
                # Pick whichever is closer to the current price, but below it.
                structures_below = [p for p in [vwap, recent_low] if p < current_price]
                
                if structures_below:
                    nearest_support = max(structures_below) # highest support below us
                    sl_price = nearest_support - buffer
                else:
                    sl_price = current_price - (atr * 2.5) # fallback
                
                # Convert price to percentage Drop
                sl_pct = (current_price - sl_price) / current_price
                
                # TP: Target the recent high, or default 4x ATR
                tp_price = max(recent_high, current_price + (atr * 4))
                tp_pct = (tp_price - current_price) / current_price
                
            else: # SHORT
                # For SHORT: SL slightly above nearest structural resistance
                structures_above = [p for p in [vwap, recent_high] if p > current_price]
                
                if structures_above:
                    nearest_resistance = min(structures_above) # lowest resistance above us
                    sl_price = nearest_resistance + buffer
                else:
                    sl_price = current_price + (atr * 2.5) # fallback
                    
                # Convert price to percentage Rise
                sl_pct = (sl_price - current_price) / current_price
                
                # TP: Target recent low, or default 4x ATR
                tp_price = min(recent_low, current_price - (atr * 4))
                tp_pct = (current_price - tp_price) / current_price
                
            # Asset-specific soft clamping (allow some flexibility around base params)
            # Use base params as anchor, but allow 50% flexibility
            if symbol:
                min_sl = asset_sl_base * 0.5  # e.g., BTC 1.5% β†’ min 0.75%
                max_sl = asset_sl_base * 2.0  # e.g., BTC 1.5% β†’ max 3.0%
                min_tp = asset_tp_base * 0.6  # e.g., BTC 3.5% β†’ min 2.1%
                max_tp = asset_tp_base * 2.0  # e.g., BTC 3.5% β†’ max 7.0%

                sl_pct = np.clip(sl_pct, min_sl, max_sl)
                tp_pct = np.clip(tp_pct, min_tp, max_tp)
            else:
                # Fallback to global limits if no symbol provided
                sl_pct = np.clip(sl_pct, self.min_sl_pct, self.max_sl_pct)
                tp_pct = np.clip(tp_pct, self.min_tp_pct, self.max_tp_pct)

            logger.info(
                f"πŸ›οΈ Structural SL/TP [{symbol or 'UNKNOWN'}] (VWAP: ${vwap:.2f}): "
                f"SL={sl_pct:.2%} (${sl_price:.2f}), TP={tp_pct:.2%} (${tp_price:.2f})"
            )
            return sl_pct, tp_pct
            
        except Exception as e:
            logger.warning(f"Failed to calc Structural SL/TP: {e}. Falling back to ATR.")
            return self.get_adaptive_sl_tp(df, trade_type)
    
    def calculate_kelly_fraction(self) -> float:
        """
        Calculate optimal position size using Kelly Criterion.
        
        Kelly = (Win% * Avg_Win / Avg_Loss) - (1 - Win%) / (Avg_Win / Avg_Loss)
        Or simplified: Kelly = Win% - (Loss% / Win/Loss Ratio)
        """
        if len(self.trade_history) < 10:
            # Not enough history, use base position size
            return self.base_position_size
        
        # Calculate statistics
        wins = [t for t in self.trade_history if t > 0]
        losses = [t for t in self.trade_history if t < 0]
        
        if not wins or not losses:
            return self.base_position_size
        
        win_rate = len(wins) / len(self.trade_history)
        avg_win = np.mean(wins)
        avg_loss = abs(np.mean(losses))
        
        # Kelly formula
        if avg_loss == 0:
            return self.max_position_size
        
        win_loss_ratio = avg_win / avg_loss
        kelly = win_rate - ((1 - win_rate) / win_loss_ratio)
        
        # Use half-Kelly for safety (more conservative)
        half_kelly = kelly * 0.5
        
        # Clamp to reasonable range
        position_size = np.clip(half_kelly, self.min_position_size, self.max_position_size)
        
        logger.info(
            f"πŸ“Š Kelly Sizing: Win%={win_rate:.1%}, W/L Ratio={win_loss_ratio:.2f}, "
            f"Kelly={kelly:.2%}, Half-Kelly={position_size:.2%}"
        )
        
        return position_size
    
    def record_trade(self, pnl_pct: float):
        """Record trade result for Kelly calculation."""
        self.trade_history.append(pnl_pct)
        # Keep only last 50 trades for recency
        if len(self.trade_history) > 50:
            self.trade_history = self.trade_history[-50:]
    
    def get_trailing_stop(
        self, 
        entry_price: float, 
        current_price: float, 
        highest_price: float,
        trade_type: str = "long",
        base_trailing_pct: float = 0.015
    ) -> Tuple[float, bool]:
        """
        Calculate trailing stop price and whether it's triggered.
        
        Args:
            entry_price: Position entry price
            current_price: Current market price
            highest_price: Highest price since entry (for longs)
            trade_type: "long" or "short"
            base_trailing_pct: Trailing stop percentage
            
        Returns:
            Tuple of (trailing_stop_price, is_triggered)
        """
        if trade_type == "long":
            # For longs, trail from the highest price
            trailing_stop = highest_price * (1 - base_trailing_pct)
            triggered = current_price <= trailing_stop
        else:
            # For shorts, trail from the lowest price
            trailing_stop = highest_price * (1 + base_trailing_pct)  # highest_price is actually lowest for shorts
            triggered = current_price >= trailing_stop
        
        return trailing_stop, triggered
    
    def get_risk_parameters(self, df: pd.DataFrame, trade_type: str = "long") -> RiskParameters:
        """
        Get all risk parameters for a trade.
        
        Returns complete RiskParameters with adaptive values.
        """
        # Adaptive SL/TP
        sl_pct, tp_pct = self.get_adaptive_sl_tp(df, trade_type)
        
        # Kelly position sizing
        if self.use_kelly:
            position_size = self.calculate_kelly_fraction()
        else:
            position_size = self.base_position_size
        
        # Trailing stop
        trailing_pct = sl_pct if self.use_trailing else None
        
        return RiskParameters(
            stop_loss_pct=sl_pct,
            take_profit_pct=tp_pct,
            position_size=position_size,
            trailing_stop_pct=trailing_pct,
            risk_reward_ratio=tp_pct / sl_pct
        )
    
    def get_summary(self) -> Dict:
        """Get summary of current risk parameters."""
        if len(self.trade_history) >= 10:
            wins = [t for t in self.trade_history if t > 0]
            losses = [t for t in self.trade_history if t < 0]
            win_rate = len(wins) / len(self.trade_history) if self.trade_history else 0
            kelly = self.calculate_kelly_fraction()
        else:
            win_rate = 0
            kelly = self.base_position_size
        
        return {
            'trades_recorded': len(self.trade_history),
            'win_rate': win_rate,
            'kelly_fraction': kelly,
            'use_kelly': self.use_kelly,
            'use_trailing': self.use_trailing,
        }