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/validator.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Pre-trade validator — the final gate before an order goes out.
|
|
2
|
+
|
|
3
|
+
A composable checklist that runs every rule against a proposed trade and vetoes
|
|
4
|
+
it if *any* rule fails. The point is to make "should I take this trade?" a
|
|
5
|
+
single, auditable function call whose output records exactly which rules passed
|
|
6
|
+
and which failed.
|
|
7
|
+
|
|
8
|
+
The checks span market quality, position sizing, risk limits, signal quality,
|
|
9
|
+
and timing. The validator is framework-agnostic: you assemble a
|
|
10
|
+
:class:`TradeProposal` from whatever your system knows, and you get back a
|
|
11
|
+
:class:`ValidationResult`.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Mapping
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CheckResult:
|
|
21
|
+
name: str
|
|
22
|
+
ok: bool
|
|
23
|
+
details: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ValidationResult:
|
|
28
|
+
passed: bool
|
|
29
|
+
failures: list[CheckResult]
|
|
30
|
+
details: list[CheckResult] = field(default_factory=list)
|
|
31
|
+
market_quality_failed: bool = False # if True, the caller may retry shortly
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class TradeProposal:
|
|
36
|
+
symbol: str
|
|
37
|
+
side: str # "long" | "short"
|
|
38
|
+
entry_price: float
|
|
39
|
+
stop_price: float
|
|
40
|
+
target_price: float
|
|
41
|
+
size_units: float
|
|
42
|
+
notional: float
|
|
43
|
+
strategy: str
|
|
44
|
+
score: int # signal-quality score (0-100)
|
|
45
|
+
regime: str = ""
|
|
46
|
+
|
|
47
|
+
# Market-quality snapshot
|
|
48
|
+
spread_pct: float = 0.0
|
|
49
|
+
orderbook_depth: float = float("inf") # quote-currency depth near touch
|
|
50
|
+
recent_atr_spike_x: float = 1.0 # current_atr / baseline_atr
|
|
51
|
+
last_quote_age_sec: float = 0.0
|
|
52
|
+
|
|
53
|
+
# Portfolio state
|
|
54
|
+
equity: float = 0.0
|
|
55
|
+
free_balance: float = float("inf")
|
|
56
|
+
current_total_exposure_pct: float = 0.0
|
|
57
|
+
current_portfolio_heat_pct: float = 0.0 # open risk-at-stop, excl. this trade
|
|
58
|
+
sector: str = "" # sector / asset-class tag (for per-sector cap)
|
|
59
|
+
current_sector_exposure_pct: float = 0.0 # this sector's open notional %, excl. this trade
|
|
60
|
+
open_concurrent_positions: int = 0
|
|
61
|
+
daily_loss_pct: float = 0.0
|
|
62
|
+
daily_trade_count: int = 0
|
|
63
|
+
drawdown_halted: bool = False
|
|
64
|
+
cooldown_active: bool = False
|
|
65
|
+
correlation_blocked: bool = False
|
|
66
|
+
at_max_concurrent: bool = False
|
|
67
|
+
seconds_since_last_trade: float = float("inf")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PreTradeValidator:
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
max_spread_pct_tight: float = 0.05,
|
|
74
|
+
max_spread_pct_default: float = 0.1,
|
|
75
|
+
tight_spread_symbols: set[str] | None = None,
|
|
76
|
+
depth_multiplier: float = 2.0,
|
|
77
|
+
max_recent_atr_spike: float = 3.0,
|
|
78
|
+
max_quote_age_sec: float = 120.0,
|
|
79
|
+
max_notional_pct: float = 4.0,
|
|
80
|
+
max_total_exposure_pct: float = 10.0,
|
|
81
|
+
max_portfolio_heat_pct: float = float("inf"),
|
|
82
|
+
max_exposure_per_sector_pct: float = float("inf"),
|
|
83
|
+
max_daily_loss_pct: float = 1.5,
|
|
84
|
+
max_daily_trades: int = 5,
|
|
85
|
+
min_score: int = 70,
|
|
86
|
+
min_rr_ratio: float = 2.0,
|
|
87
|
+
min_seconds_between_trades: float = 15 * 60,
|
|
88
|
+
regime_strategies: Mapping[str, set[str]] | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
self.max_spread_tight = max_spread_pct_tight
|
|
91
|
+
self.max_spread_default = max_spread_pct_default
|
|
92
|
+
self.tight_spread_symbols = tight_spread_symbols or set()
|
|
93
|
+
self.depth_mult = depth_multiplier
|
|
94
|
+
self.max_atr_spike = max_recent_atr_spike
|
|
95
|
+
self.max_quote_age = max_quote_age_sec
|
|
96
|
+
self.max_notional_pct = max_notional_pct
|
|
97
|
+
self.max_total_exposure_pct = max_total_exposure_pct
|
|
98
|
+
self.max_portfolio_heat_pct = max_portfolio_heat_pct
|
|
99
|
+
self.max_sector_exposure_pct = max_exposure_per_sector_pct
|
|
100
|
+
self.max_daily_loss_pct = max_daily_loss_pct
|
|
101
|
+
self.max_daily_trades = max_daily_trades
|
|
102
|
+
self.min_score = min_score
|
|
103
|
+
self.min_rr = min_rr_ratio
|
|
104
|
+
self.min_secs_between = min_seconds_between_trades
|
|
105
|
+
self.regime_strategies = regime_strategies
|
|
106
|
+
|
|
107
|
+
def validate(self, p: TradeProposal, *, min_score_override: int | None = None) -> ValidationResult:
|
|
108
|
+
results: list[CheckResult] = []
|
|
109
|
+
min_score = max(min_score_override or 0, self.min_score)
|
|
110
|
+
|
|
111
|
+
# ── MARKET QUALITY ──
|
|
112
|
+
max_spread = (
|
|
113
|
+
self.max_spread_tight if p.symbol in self.tight_spread_symbols
|
|
114
|
+
else self.max_spread_default
|
|
115
|
+
)
|
|
116
|
+
results.append(CheckResult("spread_ok", p.spread_pct < max_spread,
|
|
117
|
+
f"spread={p.spread_pct:.4f}% < {max_spread}%"))
|
|
118
|
+
results.append(CheckResult("orderbook_depth_ok",
|
|
119
|
+
p.orderbook_depth >= self.depth_mult * p.notional,
|
|
120
|
+
f"depth={p.orderbook_depth:.0f} vs {self.depth_mult}x notional {p.notional:.0f}"))
|
|
121
|
+
results.append(CheckResult("no_recent_volatility_spike",
|
|
122
|
+
p.recent_atr_spike_x < self.max_atr_spike,
|
|
123
|
+
f"atr_spike_x={p.recent_atr_spike_x:.2f} < {self.max_atr_spike}"))
|
|
124
|
+
results.append(CheckResult("data_fresh", p.last_quote_age_sec < self.max_quote_age,
|
|
125
|
+
f"quote_age={p.last_quote_age_sec:.0f}s"))
|
|
126
|
+
|
|
127
|
+
# ── POSITION SIZING ──
|
|
128
|
+
results.append(CheckResult("size_positive", p.size_units > 0 and p.notional > 0,
|
|
129
|
+
"size must be > 0"))
|
|
130
|
+
notional_pct = (p.notional / p.equity * 100.0) if p.equity else 100.0
|
|
131
|
+
results.append(CheckResult("notional_cap", notional_pct <= self.max_notional_pct,
|
|
132
|
+
f"notional {notional_pct:.2f}% of equity vs cap {self.max_notional_pct}%"))
|
|
133
|
+
projected = p.current_total_exposure_pct + notional_pct
|
|
134
|
+
results.append(CheckResult("total_exposure_cap", projected <= self.max_total_exposure_pct,
|
|
135
|
+
f"projected exposure {projected:.2f}% vs cap {self.max_total_exposure_pct}%"))
|
|
136
|
+
# Per-sector exposure: keep any single sector / asset-class from dominating the
|
|
137
|
+
# book. Only checked when a cap is configured and the trade carries a sector tag.
|
|
138
|
+
if self.max_sector_exposure_pct != float("inf") and p.sector:
|
|
139
|
+
projected_sector = p.current_sector_exposure_pct + notional_pct
|
|
140
|
+
results.append(CheckResult(
|
|
141
|
+
"sector_exposure_ok",
|
|
142
|
+
projected_sector <= self.max_sector_exposure_pct,
|
|
143
|
+
f"sector '{p.sector}' projected {projected_sector:.2f}% vs cap {self.max_sector_exposure_pct}%"))
|
|
144
|
+
# Portfolio heat: total risk-at-stop across open positions plus this one.
|
|
145
|
+
# Only checked when a cap is configured (off by default).
|
|
146
|
+
if self.max_portfolio_heat_pct != float("inf"):
|
|
147
|
+
trade_risk = abs(p.entry_price - p.stop_price) * p.size_units
|
|
148
|
+
trade_risk_pct = (trade_risk / p.equity * 100.0) if p.equity else 0.0
|
|
149
|
+
projected_heat = p.current_portfolio_heat_pct + trade_risk_pct
|
|
150
|
+
results.append(CheckResult("portfolio_heat_ok",
|
|
151
|
+
projected_heat <= self.max_portfolio_heat_pct,
|
|
152
|
+
f"projected heat {projected_heat:.2f}% vs cap {self.max_portfolio_heat_pct}%"))
|
|
153
|
+
results.append(CheckResult("sufficient_balance", p.free_balance >= p.notional * 1.01,
|
|
154
|
+
f"free={p.free_balance:.2f} need={p.notional * 1.01:.2f}"))
|
|
155
|
+
|
|
156
|
+
# ── RISK LIMITS ──
|
|
157
|
+
results.append(CheckResult("daily_loss_ok", p.daily_loss_pct < self.max_daily_loss_pct,
|
|
158
|
+
f"daily_loss={p.daily_loss_pct:.2f}% < {self.max_daily_loss_pct}%"))
|
|
159
|
+
results.append(CheckResult("daily_trade_count_ok", p.daily_trade_count < self.max_daily_trades,
|
|
160
|
+
f"trades_today={p.daily_trade_count} < {self.max_daily_trades}"))
|
|
161
|
+
results.append(CheckResult("not_in_drawdown_halt", not p.drawdown_halted,
|
|
162
|
+
"drawdown halt active" if p.drawdown_halted else "ok"))
|
|
163
|
+
results.append(CheckResult("not_in_cooldown", not p.cooldown_active,
|
|
164
|
+
"in cooldown" if p.cooldown_active else "ok"))
|
|
165
|
+
results.append(CheckResult("correlation_ok", not p.correlation_blocked,
|
|
166
|
+
"correlated position open" if p.correlation_blocked else "ok"))
|
|
167
|
+
results.append(CheckResult("max_concurrent_ok", not p.at_max_concurrent,
|
|
168
|
+
f"open={p.open_concurrent_positions} at cap" if p.at_max_concurrent else "ok"))
|
|
169
|
+
|
|
170
|
+
# ── SIGNAL QUALITY ──
|
|
171
|
+
results.append(CheckResult("score_ok", p.score >= min_score,
|
|
172
|
+
f"score={p.score} >= {min_score}"))
|
|
173
|
+
risk = abs(p.entry_price - p.stop_price)
|
|
174
|
+
reward = abs(p.target_price - p.entry_price)
|
|
175
|
+
rr = reward / risk if risk else 0.0
|
|
176
|
+
results.append(CheckResult("rr_ratio_ok", rr >= self.min_rr,
|
|
177
|
+
f"R:R {rr:.2f} >= {self.min_rr}"))
|
|
178
|
+
if self.regime_strategies is not None:
|
|
179
|
+
allowed = p.strategy in self.regime_strategies.get(p.regime, set())
|
|
180
|
+
results.append(CheckResult("regime_allows_strategy", allowed,
|
|
181
|
+
f"strategy={p.strategy} regime={p.regime}"))
|
|
182
|
+
|
|
183
|
+
# ── TIMING ──
|
|
184
|
+
results.append(CheckResult("min_time_between_trades",
|
|
185
|
+
p.seconds_since_last_trade >= self.min_secs_between,
|
|
186
|
+
f"{p.seconds_since_last_trade:.0f}s >= {self.min_secs_between}s"))
|
|
187
|
+
|
|
188
|
+
failures = [r for r in results if not r.ok]
|
|
189
|
+
market_quality_names = {
|
|
190
|
+
"spread_ok", "orderbook_depth_ok", "no_recent_volatility_spike", "data_fresh",
|
|
191
|
+
}
|
|
192
|
+
return ValidationResult(
|
|
193
|
+
passed=len(failures) == 0,
|
|
194
|
+
failures=failures,
|
|
195
|
+
details=results,
|
|
196
|
+
market_quality_failed=any(r.name in market_quality_names for r in failures),
|
|
197
|
+
)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: riskkit-quant
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A framework-agnostic risk-management toolkit for systematic traders: position sizing, drawdown laddering, and more.
|
|
5
|
+
Project-URL: Homepage, https://github.com/HasibDaddy/riskkit
|
|
6
|
+
Project-URL: Repository, https://github.com/HasibDaddy/riskkit
|
|
7
|
+
Project-URL: Issues, https://github.com/HasibDaddy/riskkit/issues
|
|
8
|
+
Author: Hasib
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: algotrading,backtesting,drawdown,kelly-criterion,position-sizing,quant,risk-management,trading
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: backtesting
|
|
25
|
+
Requires-Dist: backtesting>=0.3; extra == 'backtesting'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: backtesting>=0.3; extra == 'dev'
|
|
28
|
+
Requires-Dist: hypothesis>=6.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pandas>=1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pyyaml>=5.0; extra == 'dev'
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
34
|
+
Provides-Extra: pandas
|
|
35
|
+
Requires-Dist: pandas>=1.0; extra == 'pandas'
|
|
36
|
+
Provides-Extra: yaml
|
|
37
|
+
Requires-Dist: pyyaml>=5.0; extra == 'yaml'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# riskkit
|
|
41
|
+
|
|
42
|
+
[](https://github.com/HasibDaddy/riskkit/actions/workflows/ci.yml)
|
|
43
|
+
[](https://hasibdaddy.github.io/riskkit/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+

|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
**A framework-agnostic risk-management toolkit for systematic traders.**
|
|
49
|
+
|
|
50
|
+
Most open-source trading tools focus on the fun part — signals, indicators,
|
|
51
|
+
backtesting engines. They leave the part that actually decides whether you
|
|
52
|
+
survive thin or absent: *how big a position to take, when to cut size, and when
|
|
53
|
+
to stop trading altogether.* That's what blows up retail algo traders, not a
|
|
54
|
+
bad entry signal.
|
|
55
|
+
|
|
56
|
+
`riskkit` is that missing layer. The components are pure Python with **no
|
|
57
|
+
dependency on any exchange, data provider, or backtesting framework**. They
|
|
58
|
+
don't know what CCXT is. You feed them numbers; they hand back decisions you can
|
|
59
|
+
audit. Drop them into [backtesting.py](https://github.com/kernc/backtesting.py),
|
|
60
|
+
[vectorbt](https://github.com/polakowo/vectorbt), [backtrader](https://github.com/mementum/backtrader),
|
|
61
|
+
[freqtrade](https://github.com/freqtrade/freqtrade), or your own loop.
|
|
62
|
+
|
|
63
|
+
> ⚠️ **Not financial advice.** `riskkit` helps you *implement* a risk policy you
|
|
64
|
+
> have chosen. It does not choose one for you, and it cannot make a losing
|
|
65
|
+
> strategy profitable. Test everything on paper first.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "git+https://github.com/HasibDaddy/riskkit.git"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Zero runtime dependencies. Python 3.9+. *(A PyPI release is on the way — until
|
|
76
|
+
then, install straight from GitHub with the line above.)*
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## What's in the box
|
|
81
|
+
|
|
82
|
+
| Component | What it does |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `PositionSizer` | Volatility-adjusted fixed-fractional sizing with an optional half-Kelly ceiling, a reduction ladder for losing streaks / drawdowns, and a hard notional cap. |
|
|
85
|
+
| `DrawdownManager` | Tracks high-water-mark drawdown, maps it onto a tier ladder (cut size → raise the bar → halt), with a recovery ramp and a rolling weekly-loss pause. |
|
|
86
|
+
| `StopEngine` | A composable stop *stack* per position — initial, break-even, ATR/EMA trailing, chandelier, structure (swing), PSAR, time, and volatility stops. The tightest one wins; stops only ever move closer. |
|
|
87
|
+
| `CorrelationGuard` | At most one open position per correlation group. Groups can be static (you define them) or computed dynamically from a rolling return-correlation matrix. |
|
|
88
|
+
| `SessionManager` | Daily trade/loss caps, profit-taking stops, minimum spacing, escalating cooldowns after losing streaks, and tilt detection. |
|
|
89
|
+
| `PreTradeValidator` | The composable final gate: runs every rule against a proposed trade and vetoes it if any fails — returning exactly which checks passed and failed. |
|
|
90
|
+
|
|
91
|
+
Every decision is **auditable** — the sizer returns which multipliers fired,
|
|
92
|
+
the stop engine logs each adjustment, and the validator returns a pass/fail line
|
|
93
|
+
for every single check.
|
|
94
|
+
|
|
95
|
+
Beyond the six components, riskkit ships a few **portfolio-level controls** and
|
|
96
|
+
**standalone sizers** you can reach for on their own:
|
|
97
|
+
|
|
98
|
+
- **Portfolio caps** (enforced by `RiskManager` as the book fills): total open
|
|
99
|
+
notional, total **heat** (risk-at-stop), and **per-sector / asset-class**
|
|
100
|
+
exposure — so no single sector can dominate the book.
|
|
101
|
+
- **Composable sizers** (pure functions): `volatility_target_size`,
|
|
102
|
+
`inverse_vol_weights` (naive risk parity), and `kelly_fraction`.
|
|
103
|
+
- **Risk metrics**: historical `value_at_risk` and `conditional_value_at_risk`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
### The whole stack, one call
|
|
110
|
+
|
|
111
|
+
`RiskManager` is the façade: wire all six components from a single config, push
|
|
112
|
+
equity in as your account moves, and ask one question per trade. It keeps the
|
|
113
|
+
drawdown, session, and open-position state in sync for you.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from riskkit import RiskManager, RiskConfig, TradeIntent
|
|
117
|
+
|
|
118
|
+
risk = RiskManager(RiskConfig(
|
|
119
|
+
base_risk_pct=1.0, max_notional_pct=4.0,
|
|
120
|
+
drawdown=dict(tier1_pct=3, halt_pct=10),
|
|
121
|
+
session=dict(max_trades_per_day=5),
|
|
122
|
+
correlation=dict(static_groups={"majors": {"BTC/USDT", "ETH/USDT"}}),
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
risk.on_equity(10_000) # refresh drawdown/session state
|
|
126
|
+
decision = risk.evaluate(TradeIntent(
|
|
127
|
+
symbol="BTC/USDT", side="long",
|
|
128
|
+
entry_price=100.0, stop_price=98.0, target_price=104.0,
|
|
129
|
+
score=82, atr=2.0, atr_baseline=2.0,
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
if decision.ok:
|
|
133
|
+
place(decision.units, decision.stop) # your execution layer
|
|
134
|
+
risk.on_fill(decision) # tell riskkit it filled
|
|
135
|
+
else:
|
|
136
|
+
print("skip:", *decision.reasons, sep="\n ") # every gate that vetoed it
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Reach past the façade to any single component when you need to — they're all
|
|
140
|
+
exposed (`risk.sizer`, `risk.drawdown`, `risk.stops`, …) and usable standalone.
|
|
141
|
+
|
|
142
|
+
Don't want to tune every knob? Start from a preset, or load policy from YAML:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
risk = RiskManager(RiskConfig.conservative()) # or .balanced() / .aggressive()
|
|
146
|
+
cfg = RiskConfig.from_yaml("risk.yaml") # needs riskkit[yaml]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Sizing a trade
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from riskkit import PositionSizer, SizingInputs
|
|
153
|
+
|
|
154
|
+
sizer = PositionSizer(
|
|
155
|
+
base_risk_pct=1.0, # risk 1% of equity per trade, before adjustments
|
|
156
|
+
max_risk_pct=1.5, # never risk more than 1.5%
|
|
157
|
+
max_notional_pct=4.0, # never let a position exceed 4% of equity
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
result = sizer.size(SizingInputs(
|
|
161
|
+
equity=10_000,
|
|
162
|
+
entry_price=100.0,
|
|
163
|
+
stop_price=98.0, # the stop distance defines your risk per unit
|
|
164
|
+
atr=2.5, # current volatility
|
|
165
|
+
atr_baseline=2.0, # "normal" volatility -> scales risk down when choppy
|
|
166
|
+
consecutive_losses=2, # reduction ladder kicks in
|
|
167
|
+
drawdown_pct=4.0,
|
|
168
|
+
))
|
|
169
|
+
|
|
170
|
+
if result.units > 0:
|
|
171
|
+
print(f"Buy {result.units:.4f} units (risk {result.risk_pct:.2%})")
|
|
172
|
+
print("adjustments:", result.multipliers_applied)
|
|
173
|
+
else:
|
|
174
|
+
print("Skip:", result.reason_for_zero)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Adapting to drawdown
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from riskkit import DrawdownManager
|
|
181
|
+
|
|
182
|
+
dm = DrawdownManager(tier1_pct=3, tier2_pct=5, tier3_pct=7, halt_pct=10)
|
|
183
|
+
|
|
184
|
+
state = dm.update(current_equity) # call once per equity refresh
|
|
185
|
+
if state.halted:
|
|
186
|
+
print("No new trades:", state.reason)
|
|
187
|
+
else:
|
|
188
|
+
size = base_size * state.size_multiplier # scale every position by the tier
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The two compose naturally: feed `DrawdownManager`'s `drawdown_pct` and
|
|
192
|
+
`size_multiplier` straight into the sizer.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Design principles
|
|
197
|
+
|
|
198
|
+
- **Framework-agnostic.** No exchange SDK, no pandas requirement in the core,
|
|
199
|
+
no global state. Just dataclasses in, dataclasses out.
|
|
200
|
+
- **Auditable, not magic.** Every adjustment is named and returned. You can log
|
|
201
|
+
the exact reason a trade was sized down or skipped.
|
|
202
|
+
- **Conservative by default.** Floors, ceilings, and hard caps bound every knob.
|
|
203
|
+
The math can recommend; it can never exceed the limits you set.
|
|
204
|
+
- **Anti-martingale.** Size goes *down* after losses and during drawdowns, never
|
|
205
|
+
up. There is no "average down" path anywhere in this library.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Integrations
|
|
210
|
+
|
|
211
|
+
riskkit slots into whatever you already use — the [examples](examples/) are
|
|
212
|
+
runnable:
|
|
213
|
+
|
|
214
|
+
- **backtesting.py** — subclass the `RiskkitStrategy` adapter
|
|
215
|
+
(`from riskkit.adapters.backtesting import RiskkitStrategy`) and call
|
|
216
|
+
`risk_long()` / `risk_short()`; every entry is sized **and** validated by one
|
|
217
|
+
`RiskConfig`, with closed trades fed back to the session manager. See
|
|
218
|
+
[`examples/backtesting_riskmanager.py`](examples/backtesting_riskmanager.py)
|
|
219
|
+
(full façade) or [`examples/backtesting_py_strategy.py`](examples/backtesting_py_strategy.py)
|
|
220
|
+
(just `PositionSizer`, by hand).
|
|
221
|
+
- **freqtrade** — `FreqtradeRiskManager`
|
|
222
|
+
(`from riskkit.adapters.freqtrade import FreqtradeRiskManager`) drives
|
|
223
|
+
`custom_stake_amount` + `confirm_trade_entry` from one `RiskConfig`; see
|
|
224
|
+
[`examples/freqtrade_callbacks.py`](examples/freqtrade_callbacks.py).
|
|
225
|
+
- **vectorbt** — `size_signals`
|
|
226
|
+
(`from riskkit.adapters.vectorbt import size_signals`) turns entry signals into
|
|
227
|
+
a riskkit-sized array for `Portfolio.from_signals`; see
|
|
228
|
+
[`examples/vectorbt_sizing.py`](examples/vectorbt_sizing.py).
|
|
229
|
+
- **your own loop** — [`examples/risk_manager.py`](examples/risk_manager.py)
|
|
230
|
+
drives the full `RiskManager` façade end-to-end;
|
|
231
|
+
[`examples/multi_asset_book.py`](examples/multi_asset_book.py) allocates,
|
|
232
|
+
vol-targets, and vets a cross-sector book through the portfolio caps;
|
|
233
|
+
[`examples/pipeline.py`](examples/pipeline.py) shows the same flow wired by hand.
|
|
234
|
+
|
|
235
|
+
📖 **Full docs: https://hasibdaddy.github.io/riskkit/**
|
|
236
|
+
|
|
237
|
+
## Roadmap
|
|
238
|
+
|
|
239
|
+
`riskkit` is extracted and generalized from a working risk-first trading bot.
|
|
240
|
+
The core six components are in place; next up is making them effortless to drop
|
|
241
|
+
into the popular frameworks:
|
|
242
|
+
|
|
243
|
+
- [x] `PositionSizer`, `DrawdownManager`, `StopEngine`
|
|
244
|
+
- [x] `CorrelationGuard`, `SessionManager`, `PreTradeValidator`
|
|
245
|
+
- [x] A single `RiskManager` façade that wires all six together with one config
|
|
246
|
+
- [x] Config presets (conservative / balanced / aggressive) + dict/YAML loading
|
|
247
|
+
- [x] First-class adapters for backtesting.py, freqtrade, and vectorbt
|
|
248
|
+
- [x] A [hosted docs site](https://hasibdaddy.github.io/riskkit/) with component recipes
|
|
249
|
+
- [x] Portfolio caps (total-exposure, heat, per-sector) + standalone sizers (vol-target, risk-parity, Kelly) + VaR/CVaR
|
|
250
|
+
- [ ] A PyPI release (`pip install riskkit-quant`, then `import riskkit`)
|
|
251
|
+
|
|
252
|
+
Feedback on the API is genuinely welcome — open an issue. See the full
|
|
253
|
+
[ROADMAP.md](ROADMAP.md), [CONTRIBUTING.md](CONTRIBUTING.md), and the
|
|
254
|
+
[examples](examples/).
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT © 2026 Hasib. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
riskkit/__init__.py,sha256=9gsMtPovsgwKT25gyGnBVbPy2lIrb_KK5Z05qnP-3mo,2651
|
|
2
|
+
riskkit/correlation.py,sha256=FHmCYuyhdmZzCT695cIZOTSgdPV9W3Rve3YkE9kU-UQ,4230
|
|
3
|
+
riskkit/drawdown.py,sha256=kfmeQord_vl9rHjikEvvWrQa7LHFCbUISX74pyayJxI,5977
|
|
4
|
+
riskkit/manager.py,sha256=649JeReCiIgxaFllhx0kX5g6yJXtM29Vq4-vMVVItvA,25677
|
|
5
|
+
riskkit/metrics.py,sha256=D9EOwwCEHKgWIEVRnJAbx-kxnFfbr532cwZTy7kIUNU,1694
|
|
6
|
+
riskkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
riskkit/session.py,sha256=mAqcOQybTl6zkk4cbZ1o9aDe9SV4mr0xALCIZbP4Bd8,6877
|
|
8
|
+
riskkit/sizing.py,sha256=0y02WR0itWQyJAdfhUOFAV8B_5pqGhnpFpxOWBzSb5U,10157
|
|
9
|
+
riskkit/stops.py,sha256=im_vObTohLqJiQfYxatdTZWcrTNib-8ChHp5t7z8AVI,10192
|
|
10
|
+
riskkit/validator.py,sha256=MmjIkebfcIA6I18GuIGNi6jZ-7ycX5vPY9M67v0okdo,9923
|
|
11
|
+
riskkit/adapters/__init__.py,sha256=-a58ATahRvWrRRNb8WLv12s_Se_RbXeXpisTeQKljkg,295
|
|
12
|
+
riskkit/adapters/backtesting.py,sha256=uJRthhAJDLFvbmd6bobBT9XPZ1cHOl4X-Tl6Epg0Hlc,6837
|
|
13
|
+
riskkit/adapters/freqtrade.py,sha256=1rDBz4aGuT_Bfc_2y3pzCY3hjFGL5emPjulBYHZ5smY,6188
|
|
14
|
+
riskkit/adapters/vectorbt.py,sha256=Ii1ip2yL0QrrMxPzBtf3L8Y5Av0jamhHnqZ5RY5IhLY,4711
|
|
15
|
+
riskkit_quant-0.4.0.dist-info/METADATA,sha256=dkBA_wEqtgOVAPo0Rbb4WIDQSxOIDZ6Zv6uoza8t13U,11441
|
|
16
|
+
riskkit_quant-0.4.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
17
|
+
riskkit_quant-0.4.0.dist-info/licenses/LICENSE,sha256=yNlpPgqTSHw3GbzxIlF5ndbtwb1eEJmTlyet0zxILH4,1062
|
|
18
|
+
riskkit_quant-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hasib
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|