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,
}
|