riskkit-quant 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- riskkit/__init__.py +86 -0
- riskkit/adapters/__init__.py +7 -0
- riskkit/adapters/backtesting.py +165 -0
- riskkit/adapters/freqtrade.py +144 -0
- riskkit/adapters/vectorbt.py +129 -0
- riskkit/correlation.py +119 -0
- riskkit/drawdown.py +171 -0
- riskkit/manager.py +619 -0
- riskkit/metrics.py +41 -0
- riskkit/py.typed +0 -0
- riskkit/session.py +173 -0
- riskkit/sizing.py +263 -0
- riskkit/stops.py +228 -0
- riskkit/validator.py +197 -0
- riskkit_quant-0.4.0.dist-info/METADATA +260 -0
- riskkit_quant-0.4.0.dist-info/RECORD +18 -0
- riskkit_quant-0.4.0.dist-info/WHEEL +4 -0
- riskkit_quant-0.4.0.dist-info/licenses/LICENSE +21 -0
riskkit/drawdown.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Drawdown manager.
|
|
2
|
+
|
|
3
|
+
Tracks high-water-mark equity and the resulting drawdown, then maps it onto a
|
|
4
|
+
tier ladder. Each tier dials position size down, raises the bar for taking new
|
|
5
|
+
trades, and eventually halts entirely. A recovery ramp prevents the manager
|
|
6
|
+
from snapping straight back to full size the moment equity ticks up — it has to
|
|
7
|
+
earn its way back one tier at a time.
|
|
8
|
+
|
|
9
|
+
Default tiers::
|
|
10
|
+
|
|
11
|
+
0 dd <= tier1 normal
|
|
12
|
+
1 tier1 < dd <= tier2 0.75x size
|
|
13
|
+
2 tier2 < dd <= tier3 0.50x size, min_score 80
|
|
14
|
+
3 tier3 < dd <= halt 0.25x size, min_score 85, max 1 concurrent
|
|
15
|
+
4 dd > halt HALT — no new entries
|
|
16
|
+
|
|
17
|
+
A separate weekly-loss guard pauses new entries for 24h if equity falls more
|
|
18
|
+
than ``weekly_loss_pause_pct`` within a rolling 7-day window.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from datetime import datetime, timedelta, timezone
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class DrawdownState:
|
|
28
|
+
"""The current risk posture implied by drawdown.
|
|
29
|
+
|
|
30
|
+
``size_multiplier`` scales position size; ``min_score_override`` and
|
|
31
|
+
``max_concurrent_override`` are ``None`` when the tier imposes no override.
|
|
32
|
+
When ``halted`` is True, no new entries should be opened.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
tier: int
|
|
36
|
+
drawdown_pct: float
|
|
37
|
+
peak_equity: float
|
|
38
|
+
halted: bool
|
|
39
|
+
size_multiplier: float
|
|
40
|
+
min_score_override: int | None
|
|
41
|
+
max_concurrent_override: int | None
|
|
42
|
+
reason: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class _WeeklyWindow:
|
|
47
|
+
start_equity: float
|
|
48
|
+
start_ts: datetime
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DrawdownManager:
|
|
52
|
+
"""Stateful drawdown tracker. Call :meth:`update` once per equity refresh.
|
|
53
|
+
|
|
54
|
+
All threshold arguments are human percentages (``3.0`` == 3% drawdown).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
tier1_pct: float = 3.0,
|
|
60
|
+
tier2_pct: float = 5.0,
|
|
61
|
+
tier3_pct: float = 7.0,
|
|
62
|
+
halt_pct: float = 10.0,
|
|
63
|
+
weekly_loss_pause_pct: float = 3.0,
|
|
64
|
+
recovery_ramp: bool = True,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.tier1 = tier1_pct
|
|
67
|
+
self.tier2 = tier2_pct
|
|
68
|
+
self.tier3 = tier3_pct
|
|
69
|
+
self.halt = halt_pct
|
|
70
|
+
self.weekly_loss = weekly_loss_pause_pct
|
|
71
|
+
self.recovery_ramp = recovery_ramp
|
|
72
|
+
|
|
73
|
+
self.peak_equity: float = 0.0
|
|
74
|
+
self.current_tier: int = 0
|
|
75
|
+
self.weekly_window: _WeeklyWindow | None = None
|
|
76
|
+
self.weekly_pause_until: datetime | None = None
|
|
77
|
+
|
|
78
|
+
def update(self, equity: float, now: datetime | None = None) -> DrawdownState:
|
|
79
|
+
"""Feed the latest equity and get back the current :class:`DrawdownState`."""
|
|
80
|
+
now = now or datetime.now(timezone.utc)
|
|
81
|
+
if equity <= 0:
|
|
82
|
+
return DrawdownState(
|
|
83
|
+
4, 100.0, self.peak_equity, True, 0.0, None, None,
|
|
84
|
+
reason="non-positive equity",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if equity > self.peak_equity:
|
|
88
|
+
self.peak_equity = equity
|
|
89
|
+
|
|
90
|
+
dd_pct = (
|
|
91
|
+
(self.peak_equity - equity) / self.peak_equity * 100.0
|
|
92
|
+
if self.peak_equity else 0.0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
target = self._target_tier(dd_pct)
|
|
96
|
+
|
|
97
|
+
# Recovery ramp: a tier can only step DOWN one level at a time, and only
|
|
98
|
+
# once drawdown has recovered to half of the lower tier's threshold.
|
|
99
|
+
if self.recovery_ramp and target < self.current_tier:
|
|
100
|
+
lower_threshold = [self.tier1, self.tier2, self.tier3, self.halt][
|
|
101
|
+
max(0, self.current_tier - 1)
|
|
102
|
+
]
|
|
103
|
+
if dd_pct < lower_threshold * 0.5:
|
|
104
|
+
self.current_tier = max(target, self.current_tier - 1)
|
|
105
|
+
# otherwise hold at the current tier
|
|
106
|
+
else:
|
|
107
|
+
self.current_tier = target
|
|
108
|
+
|
|
109
|
+
weekly_loss = self._update_weekly_window(equity, now)
|
|
110
|
+
|
|
111
|
+
size_mult, min_score, max_conc, halted, reason = self._tier_actions(
|
|
112
|
+
self.current_tier, weekly_pause=self.weekly_pause_until is not None
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return DrawdownState(
|
|
116
|
+
tier=self.current_tier,
|
|
117
|
+
drawdown_pct=dd_pct,
|
|
118
|
+
peak_equity=self.peak_equity,
|
|
119
|
+
halted=halted,
|
|
120
|
+
size_multiplier=size_mult,
|
|
121
|
+
min_score_override=min_score,
|
|
122
|
+
max_concurrent_override=max_conc,
|
|
123
|
+
reason=reason,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------ helpers
|
|
127
|
+
|
|
128
|
+
def _target_tier(self, dd_pct: float) -> int:
|
|
129
|
+
if dd_pct > self.halt:
|
|
130
|
+
return 4
|
|
131
|
+
if dd_pct > self.tier3:
|
|
132
|
+
return 3
|
|
133
|
+
if dd_pct > self.tier2:
|
|
134
|
+
return 2
|
|
135
|
+
if dd_pct > self.tier1:
|
|
136
|
+
return 1
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
def _update_weekly_window(self, equity: float, now: datetime) -> float:
|
|
140
|
+
"""Maintain the rolling 7-day window and arm/clear the weekly pause."""
|
|
141
|
+
if self.weekly_window is None or (now - self.weekly_window.start_ts).days >= 7:
|
|
142
|
+
self.weekly_window = _WeeklyWindow(start_equity=equity, start_ts=now)
|
|
143
|
+
|
|
144
|
+
weekly_loss = (
|
|
145
|
+
(self.weekly_window.start_equity - equity)
|
|
146
|
+
/ self.weekly_window.start_equity * 100.0
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if weekly_loss > self.weekly_loss and self.weekly_pause_until is None:
|
|
150
|
+
self.weekly_pause_until = now + timedelta(hours=24)
|
|
151
|
+
|
|
152
|
+
if self.weekly_pause_until and now >= self.weekly_pause_until:
|
|
153
|
+
self.weekly_pause_until = None
|
|
154
|
+
|
|
155
|
+
return weekly_loss
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _tier_actions(
|
|
159
|
+
tier: int, weekly_pause: bool
|
|
160
|
+
) -> tuple[float, int | None, int | None, bool, str]:
|
|
161
|
+
if weekly_pause:
|
|
162
|
+
return 0.0, None, 0, True, "weekly loss limit — 24h pause"
|
|
163
|
+
if tier == 0:
|
|
164
|
+
return 1.0, None, None, False, "normal"
|
|
165
|
+
if tier == 1:
|
|
166
|
+
return 0.75, None, None, False, "tier1 — size 0.75x"
|
|
167
|
+
if tier == 2:
|
|
168
|
+
return 0.5, 80, None, False, "tier2 — size 0.5x, min_score 80"
|
|
169
|
+
if tier == 3:
|
|
170
|
+
return 0.25, 85, 1, False, "tier3 — size 0.25x, min_score 85, max 1 position"
|
|
171
|
+
return 0.0, None, 0, True, "HALT — drawdown exceeded halt threshold"
|