Premchan369 commited on
Commit
69bae84
·
verified ·
1 Parent(s): 1567e40

Add drawdown control - CPPI, Kelly criterion, dynamic position sizing

Browse files
Files changed (1) hide show
  1. drawdown_control.py +118 -0
drawdown_control.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """drawdown_control.py — Dynamic Drawdown Control & Position Sizing
2
+
3
+ Implements CPPI (Constant Proportion Portfolio Insurance), fractional Kelly
4
+ criterion, dynamic leverage based on current drawdown, and volatility targeting.
5
+ Essential for survival-first quant strategies.
6
+
7
+ References:
8
+ - Perold & Sharpe 1988: "Dynamic Strategies for Asset Allocation" (CPPI)
9
+ - Thorp 2006: "The Kelly Criterion in Blackjack, Sports Betting, and the Stock Market"
10
+ - Grossman & Zhou 1993: "Optimal Investment Strategies for Controlling Drawdowns"
11
+ """
12
+ import numpy as np, pandas as pd
13
+
14
+ class DrawdownController:
15
+ """Controls drawdown via CPPI, Kelly, and dynamic leverage."""
16
+
17
+ def __init__(self, max_dd=0.15, target_vol=0.10, kelly_fraction=0.5):
18
+ self.max_dd = max_dd
19
+ self.target_vol = target_vol
20
+ self.kelly_frac = kelly_fraction
21
+
22
+ def cppi_weights(self, prices, floor_ratio=0.8, multiplier=3.0):
23
+ """CPPI: allocate to risky asset based on cushion above floor.
24
+
25
+ Returns equity exposure fraction [0, 1].
26
+ """
27
+ nav = prices / prices.iloc[0]
28
+ peak = nav.expanding().max()
29
+ floor = peak * floor_ratio
30
+ cushion = nav - floor
31
+ exposure = multiplier * cushion / nav
32
+ return np.clip(exposure, 0, 1.0)
33
+
34
+ def kelly_leverage(self, returns, window=252):
35
+ """Fractional Kelly optimal leverage.
36
+
37
+ f* = mu / sigma^2 (full Kelly)
38
+ f = fraction * f* (half Kelly = safer)
39
+ """
40
+ r = returns.dropna()
41
+ if len(r) < 30: return 0.5
42
+ mu = r.tail(window).mean() * 252
43
+ sigma2 = (r.tail(window).std() * np.sqrt(252)) ** 2
44
+ if sigma2 < 1e-10: return 0.5
45
+ f_star = mu / sigma2
46
+ return np.clip(self.kelly_frac * f_star, 0.0, 2.0)
47
+
48
+ def dynamic_leverage(self, prices, returns, current_dd=None):
49
+ """Dynamic leverage: reduce when in drawdown, increase when at peak."""
50
+ nav = prices / prices.iloc[0]
51
+ peak = nav.expanding().max()
52
+ dd = (nav - peak) / peak
53
+ if current_dd is None:
54
+ current_dd = dd.iloc[-1]
55
+ # Vol targeting
56
+ vol = returns.tail(63).std() * np.sqrt(252)
57
+ vol_scalar = self.target_vol / (vol + 1e-10)
58
+ # Drawdown guard
59
+ dd_scalar = max(0, 1 - abs(current_dd) / self.max_dd)
60
+ return np.clip(vol_scalar * dd_scalar, 0.0, 2.0)
61
+
62
+ def position_size(self, capital, atr, risk_per_trade=0.01):
63
+ """ATR-based position sizing (Turtle-style).
64
+
65
+ Position = (Capital * Risk%) / ATR
66
+ """
67
+ if atr <= 0: return 0
68
+ return capital * risk_per_trade / atr
69
+
70
+ def optimal_f(self, returns, window=100):
71
+ """Optimal-F (Ralph Vince): geometric growth maximizing fraction.
72
+
73
+ f = argmax G(f) where G(f) = product(1 + f * (-W/R))
74
+ """
75
+ r = returns.tail(window).dropna()
76
+ if len(r) < 20: return 0.25
77
+ # Simplified: use Kelly as proxy
78
+ mu = r.mean(); sigma = r.std()
79
+ if sigma < 1e-10: return 0.25
80
+ return np.clip(mu / (sigma ** 2 + 1e-10), 0.0, 1.0)
81
+
82
+ def report(self, prices, returns):
83
+ """Generate position sizing recommendations."""
84
+ nav = prices / prices.iloc[0]
85
+ current_dd = (nav.iloc[-1] - nav.expanding().max().iloc[-1]) / nav.expanding().max().iloc[-1]
86
+ cppi = self.cppi_weights(prices)
87
+ kelly = self.kelly_leverage(returns)
88
+ dyn = self.dynamic_leverage(prices, returns, current_dd)
89
+ optf = self.optimal_f(returns)
90
+ vol = returns.tail(63).std() * np.sqrt(252)
91
+ return f"""## Position Sizing & Drawdown Control
92
+
93
+ | Metric | Value |
94
+ |--------|-------|
95
+ | Current Drawdown | {current_dd*100:.1f}% |
96
+ | Target Max Drawdown | {self.max_dd*100:.1f}% |
97
+ | Current Volatility (3M ann) | {vol*100:.1f}% |
98
+ | Target Volatility | {self.target_vol*100:.1f}% |
99
+
100
+ **Recommended Leverage / Exposure:**
101
+
102
+ | Strategy | Exposure | Rationale |
103
+ |----------|----------|-----------|
104
+ | CPPI (multiplier=3) | {cppi.iloc[-1]*100:.0f}% | Cushion-based insurance |
105
+ | Half-Kelly | {kelly*100:.0f}% | Growth-optimal, halved for safety |
106
+ | Vol-Target + DD-Guard | {dyn*100:.0f}% | Scales with vol and drawdown |
107
+ | Optimal-F | {optf*100:.0f}% | Geometric growth maximizing |
108
+
109
+ **Composite Recommendation:** {(cppi.iloc[-1] * 0.3 + kelly * 0.3 + dyn * 0.4)*100:.0f}% equity exposure
110
+ """
111
+
112
+ if __name__ == '__main__':
113
+ np.random.seed(42)
114
+ returns = pd.Series(np.random.normal(0.0005, 0.015, 500),
115
+ index=pd.date_range('2022-01-01', periods=500, freq='B'))
116
+ prices = (1 + returns).cumprod()
117
+ ctrl = DrawdownController(max_dd=0.15, target_vol=0.10, kelly_fraction=0.5)
118
+ print(ctrl.report(prices, returns))