riskgovernor 0.1.0__tar.gz
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.
- riskgovernor-0.1.0/LICENSE +21 -0
- riskgovernor-0.1.0/PKG-INFO +307 -0
- riskgovernor-0.1.0/README.md +264 -0
- riskgovernor-0.1.0/pyproject.toml +40 -0
- riskgovernor-0.1.0/riskgovernor/__init__.py +29 -0
- riskgovernor-0.1.0/riskgovernor/__main__.py +6 -0
- riskgovernor-0.1.0/riskgovernor/cli.py +197 -0
- riskgovernor-0.1.0/riskgovernor/governor.py +237 -0
- riskgovernor-0.1.0/riskgovernor/ledger.py +288 -0
- riskgovernor-0.1.0/riskgovernor/params.py +79 -0
- riskgovernor-0.1.0/riskgovernor/policy.py +251 -0
- riskgovernor-0.1.0/riskgovernor/py.typed +0 -0
- riskgovernor-0.1.0/riskgovernor/quickstart.py +190 -0
- riskgovernor-0.1.0/riskgovernor/report.py +63 -0
- riskgovernor-0.1.0/riskgovernor.egg-info/PKG-INFO +307 -0
- riskgovernor-0.1.0/riskgovernor.egg-info/SOURCES.txt +23 -0
- riskgovernor-0.1.0/riskgovernor.egg-info/dependency_links.txt +1 -0
- riskgovernor-0.1.0/riskgovernor.egg-info/entry_points.txt +2 -0
- riskgovernor-0.1.0/riskgovernor.egg-info/requires.txt +3 -0
- riskgovernor-0.1.0/riskgovernor.egg-info/top_level.txt +1 -0
- riskgovernor-0.1.0/setup.cfg +4 -0
- riskgovernor-0.1.0/tests/test_cli_params.py +333 -0
- riskgovernor-0.1.0/tests/test_governor.py +190 -0
- riskgovernor-0.1.0/tests/test_ledger.py +122 -0
- riskgovernor-0.1.0/tests/test_policy.py +122 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kingkillery
|
|
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.
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: riskgovernor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic bankroll risk governance: hard gates, strategy rotation, loss-sized recovery.
|
|
5
|
+
Author: kingkillery
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 kingkillery
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Keywords: risk,bankroll,drawdown,position-sizing,trading
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
37
|
+
Requires-Python: >=3.9
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# riskgovernor
|
|
45
|
+
|
|
46
|
+
[](https://github.com/kingkillery/riskgovernor/actions/workflows/ci.yml)
|
|
47
|
+
[](https://pypi.org/project/riskgovernor/)
|
|
48
|
+
[](https://pypi.org/project/riskgovernor/)
|
|
49
|
+
|
|
50
|
+
Deterministic bankroll risk governance: hard gates, strategy rotation, and
|
|
51
|
+
loss-sized recovery, expressed as pure data plus a pure decision function.
|
|
52
|
+
|
|
53
|
+
The governor answers exactly one question — *what should the next episode be,
|
|
54
|
+
given what has already happened?* — and it answers it the same way every time.
|
|
55
|
+
It never places an order. It prints a command for a human to approve.
|
|
56
|
+
|
|
57
|
+
## 30-second start
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ pip install riskgovernor
|
|
61
|
+
$ riskgovernor demo
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`demo` needs no files and walks through every verdict in order — STANDARD,
|
|
65
|
+
RECOVERY, both HALTs, and what an override looks like. When you want your
|
|
66
|
+
own setup:
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
$ riskgovernor init # writes policy.json, ledger.json, config stubs
|
|
70
|
+
$ riskgovernor decide --policy policy.json --ledger ledger.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Three commands, zero reading required. The rest of this README is what's
|
|
74
|
+
underneath.
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
$ riskgovernor decide --policy policy.json --ledger ledger.json --equity 0.00015414
|
|
78
|
+
--- RISK GOVERNOR ---
|
|
79
|
+
last episode: 38
|
|
80
|
+
last net: -3.04 uBTC
|
|
81
|
+
last strategy: plan1
|
|
82
|
+
streak: 1
|
|
83
|
+
drawdown: 3.04 uBTC
|
|
84
|
+
cumulative: -2.65 uBTC
|
|
85
|
+
peak: 0.39 uBTC
|
|
86
|
+
equity: 154.14 uBTC
|
|
87
|
+
floor headroom: 79.14 uBTC
|
|
88
|
+
overrides used: 0
|
|
89
|
+
|
|
90
|
+
VERDICT: RECOVERY glacier
|
|
91
|
+
set target = 9800
|
|
92
|
+
set side = UNDER
|
|
93
|
+
set max_loss = 0.00000150
|
|
94
|
+
set max_rounds = 40
|
|
95
|
+
set max_profit = 0.00000200
|
|
96
|
+
set base_stake = 0.00000050
|
|
97
|
+
py -3.13 run_single_round.py 39 s12_glacier.json --note "R39 glacier recovery streak=1"
|
|
98
|
+
wrote s12_glacier.json (recovery spec)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Why this exists
|
|
102
|
+
|
|
103
|
+
Most risk rules live inside the thing they are supposed to constrain. The
|
|
104
|
+
position sizer sits in the strategy, the drawdown limit sits in the bot, and
|
|
105
|
+
the first time the rule is inconvenient somebody comments it out.
|
|
106
|
+
|
|
107
|
+
This package separates the two. The governor is a pure function of
|
|
108
|
+
`(ledger, equity, policy)`, so it can be reviewed, diffed, unit-tested and
|
|
109
|
+
replayed against history without touching a live account. Execution stays in
|
|
110
|
+
whatever runner you already have.
|
|
111
|
+
|
|
112
|
+
## Install
|
|
113
|
+
|
|
114
|
+
Pure standard library, no dependencies.
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
pip install riskgovernor # from PyPI
|
|
118
|
+
pip install -e ".[dev]" # from a checkout, with test deps
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## The verdict table
|
|
122
|
+
|
|
123
|
+
Evaluated in order; the first match wins.
|
|
124
|
+
|
|
125
|
+
| # | Condition | Verdict |
|
|
126
|
+
|---|-----------|---------|
|
|
127
|
+
| 1 | equity below `gates.floor` | `HALT` |
|
|
128
|
+
| 2 | loss streak at `gates.max_consecutive_losses` | `HALT` |
|
|
129
|
+
| 3 | drawdown at `gates.max_drawdown` | `HALT` |
|
|
130
|
+
| 4 | last episode's net was negative | `RECOVERY` — rotate strategy, size to the loss |
|
|
131
|
+
| 5 | otherwise | `STANDARD` — offer every strategy |
|
|
132
|
+
|
|
133
|
+
Gate 1 is skipped when no equity is supplied, so the same code runs offline.
|
|
134
|
+
|
|
135
|
+
`RECOVERY` rotates forward through the declared strategy order and wraps. With
|
|
136
|
+
two or more strategies it never re-selects the one that just lost. A
|
|
137
|
+
single-strategy policy cannot avoid doing so — the governor flags it as a
|
|
138
|
+
degenerate rotation in `notes` rather than pretending otherwise. `STANDARD`
|
|
139
|
+
deliberately chooses nothing: it lists the options and leaves the call to the
|
|
140
|
+
operator.
|
|
141
|
+
|
|
142
|
+
## The ledger
|
|
143
|
+
|
|
144
|
+
An ordered list of episodes. JSON, with the usual file conventions.
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"episodes": [
|
|
149
|
+
{"id": 37, "net": "0.000000389793877560", "strategy": "glacier"},
|
|
150
|
+
{"id": 38, "net": "-0.000003041134348000", "strategy": "plan1",
|
|
151
|
+
"note": "R38 plan1 standard streak=0"}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `id` may be any JSON value, but only **numeric** ids are episodes. Anything
|
|
157
|
+
else (`"RESET"`, `"OVERRIDE"`, `"P1"`) is bookkeeping: it never becomes the
|
|
158
|
+
"last episode", and a zero-net row ends a loss streak by construction.
|
|
159
|
+
- `net` is a decimal string. Unparseable values break the streak rather than
|
|
160
|
+
being silently treated as zero.
|
|
161
|
+
- Unknown keys survive a load/save round trip, and the original rows key
|
|
162
|
+
(`rounds`, `episodes`, `rows`) is preserved.
|
|
163
|
+
- `net`, `result` and `pnl` are all accepted as the result key.
|
|
164
|
+
|
|
165
|
+
`save()` writes the canonical schema: row ids become `id` (a ledger read as
|
|
166
|
+
`n` is re-emitted as `id`) and `note` is materialised on every row. Content is
|
|
167
|
+
preserved; spelling is not. Don't point `save()` at a ledger another tool
|
|
168
|
+
still reads by its own key names.
|
|
169
|
+
|
|
170
|
+
**Record the strategy on every row.** This is the one field the governor
|
|
171
|
+
cannot reconstruct. If it is missing, `RECOVERY` falls back to the first
|
|
172
|
+
strategy in the rotation and says so in `notes` — it does not guess silently.
|
|
173
|
+
|
|
174
|
+
## The policy
|
|
175
|
+
|
|
176
|
+
Plain JSON, so the risk rules can be reviewed on their own.
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"unit": "USD",
|
|
181
|
+
"scale": "1",
|
|
182
|
+
"note_template": "E{episode} {strategy} {mode} streak={streak}",
|
|
183
|
+
"gates": {
|
|
184
|
+
"floor": "5000",
|
|
185
|
+
"max_consecutive_losses": 3,
|
|
186
|
+
"max_drawdown": "2000"
|
|
187
|
+
},
|
|
188
|
+
"strategies": [
|
|
189
|
+
{
|
|
190
|
+
"id": "core",
|
|
191
|
+
"config_files": ["core.json"],
|
|
192
|
+
"command": "python run_strategy.py {episode} {config} {note_arg}",
|
|
193
|
+
"recovery": {
|
|
194
|
+
"params": { "max_loss": "100", "max_rounds": 15 },
|
|
195
|
+
"size_param": "max_profit",
|
|
196
|
+
"cap": "200",
|
|
197
|
+
"stake_param": "base_stake",
|
|
198
|
+
"stake_base": "50",
|
|
199
|
+
"stake_halved": "25",
|
|
200
|
+
"halve_at_streak": 2
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Strategy order **is** the rotation order.
|
|
208
|
+
|
|
209
|
+
`recovery` describes what a recovery episode looks like:
|
|
210
|
+
|
|
211
|
+
- `params` — static overrides, applied verbatim.
|
|
212
|
+
- `size_param` / `cap` — that parameter is set to `min(|loss|, cap)`, so the
|
|
213
|
+
recovery target is sized to the loss that triggered it rather than being a
|
|
214
|
+
fixed constant.
|
|
215
|
+
- `stake_param` / `stake_base` / `stake_halved` / `halve_at_streak` — the stake
|
|
216
|
+
is cut once losses accumulate. De-risk on the way down, don't double up.
|
|
217
|
+
|
|
218
|
+
Command templates may use `{episode}`, `{strategy}`, `{mode}`, `{streak}`,
|
|
219
|
+
`{config}`, `{configs}`, `{note}` and `{note_arg}`.
|
|
220
|
+
|
|
221
|
+
### Units
|
|
222
|
+
|
|
223
|
+
Every number in the policy — gates, caps, recovery params — and every
|
|
224
|
+
`--equity` value is in **ledger units**: the same units as the ledger's `net`
|
|
225
|
+
values. `scale` and `unit` are presentation only and never participate in a
|
|
226
|
+
comparison. Keep them consistent; a `floor` written in display units against a
|
|
227
|
+
ledger in base units will read as a floor 10^6 times too high.
|
|
228
|
+
|
|
229
|
+
## Overrides are recorded, never silent
|
|
230
|
+
|
|
231
|
+
`HALT` is a hard stop, and the honest failure mode is not that the gate is
|
|
232
|
+
wrong — it is that a human overrides it. So overrides get their own row:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
governor.override(ledger, "resuming after review", operator="alice")
|
|
236
|
+
ledger.override_count() # 1
|
|
237
|
+
ledger.overrides() # the full audit trail
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The count is printed on every run. A rising number is the signal that a
|
|
241
|
+
threshold is set wrong or that someone is chasing losses — which is exactly
|
|
242
|
+
the thing the streak gate exists to catch. Record it; never clear it quietly.
|
|
243
|
+
|
|
244
|
+
## Writing recovery parameters
|
|
245
|
+
|
|
246
|
+
`ParamStore` applies resolved overrides to JSON config files, updating only the
|
|
247
|
+
keys it was given and preserving everything else:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from riskgovernor import Governor, ParamStore
|
|
251
|
+
|
|
252
|
+
governor = Governor(policy)
|
|
253
|
+
decision = governor.decide(ledger, equity=equity)
|
|
254
|
+
|
|
255
|
+
updates = governor.recovery_overrides(decision, ledger) # {file: {key: value}}
|
|
256
|
+
ParamStore("configs").apply(updates) # {} for non-recovery verdicts
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Every target file is read and validated **before** anything is written, so a
|
|
260
|
+
missing, corrupt or malformed file leaves all files untouched — a
|
|
261
|
+
partly-applied spec cannot land on disk.
|
|
262
|
+
|
|
263
|
+
## What this is not
|
|
264
|
+
|
|
265
|
+
- **Not an executor.** It prints commands. Approval is a separate step.
|
|
266
|
+
- **Not a strategy.** It has no view on whether your edge is real. Gates bound
|
|
267
|
+
damage; they do not create profit. A negative-expectancy process governed
|
|
268
|
+
perfectly is still negative-expectancy.
|
|
269
|
+
- **Not coupled to any market.** Nothing here knows what an episode is.
|
|
270
|
+
|
|
271
|
+
## Design notes
|
|
272
|
+
|
|
273
|
+
**Strategy identity belongs in the ledger.** An earlier version of the
|
|
274
|
+
predecessor script inferred "which strategy ran last" by globbing output files
|
|
275
|
+
in the working directory. That worked until a runner archived several rounds
|
|
276
|
+
into one directory, at which point the first filename to match a pattern won
|
|
277
|
+
and the governor recommended re-running the strategy that had just lost —
|
|
278
|
+
silently violating its own core rule. Persisted state beats sniffed state.
|
|
279
|
+
The artifact-scanning version in that script has since been made
|
|
280
|
+
recency-ordered, which is the best available repair when the ledger carries no
|
|
281
|
+
tag; in this package, the ledger is simply authoritative.
|
|
282
|
+
|
|
283
|
+
**Pure decision function.** No clock, no filesystem, no randomness. The same
|
|
284
|
+
inputs produce the same verdict, so behaviour can be pinned by tests and
|
|
285
|
+
replayed against history.
|
|
286
|
+
|
|
287
|
+
## Development
|
|
288
|
+
|
|
289
|
+
```console
|
|
290
|
+
python -m pytest tests -q
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
74 tests cover ledger parsing and metric derivation (including NaN and
|
|
294
|
+
non-finite rejection), gate ordering and precedence, rotation and recovery
|
|
295
|
+
sizing, config-file preservation and failure atomicity, policy validation,
|
|
296
|
+
and the CLI end to end.
|
|
297
|
+
|
|
298
|
+
## Origin
|
|
299
|
+
|
|
300
|
+
Extracted from a bankroll protocol built for a dice-betting automation. The
|
|
301
|
+
protocol did its job — floors held, streaks halted, the audit trail was
|
|
302
|
+
complete — and the underlying expectation was still negative, which is why
|
|
303
|
+
that part stayed where it was and this part was generalised instead.
|
|
304
|
+
|
|
305
|
+
## Licence
|
|
306
|
+
|
|
307
|
+
MIT.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# riskgovernor
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kingkillery/riskgovernor/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/riskgovernor/)
|
|
5
|
+
[](https://pypi.org/project/riskgovernor/)
|
|
6
|
+
|
|
7
|
+
Deterministic bankroll risk governance: hard gates, strategy rotation, and
|
|
8
|
+
loss-sized recovery, expressed as pure data plus a pure decision function.
|
|
9
|
+
|
|
10
|
+
The governor answers exactly one question — *what should the next episode be,
|
|
11
|
+
given what has already happened?* — and it answers it the same way every time.
|
|
12
|
+
It never places an order. It prints a command for a human to approve.
|
|
13
|
+
|
|
14
|
+
## 30-second start
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
$ pip install riskgovernor
|
|
18
|
+
$ riskgovernor demo
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`demo` needs no files and walks through every verdict in order — STANDARD,
|
|
22
|
+
RECOVERY, both HALTs, and what an override looks like. When you want your
|
|
23
|
+
own setup:
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ riskgovernor init # writes policy.json, ledger.json, config stubs
|
|
27
|
+
$ riskgovernor decide --policy policy.json --ledger ledger.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Three commands, zero reading required. The rest of this README is what's
|
|
31
|
+
underneath.
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ riskgovernor decide --policy policy.json --ledger ledger.json --equity 0.00015414
|
|
35
|
+
--- RISK GOVERNOR ---
|
|
36
|
+
last episode: 38
|
|
37
|
+
last net: -3.04 uBTC
|
|
38
|
+
last strategy: plan1
|
|
39
|
+
streak: 1
|
|
40
|
+
drawdown: 3.04 uBTC
|
|
41
|
+
cumulative: -2.65 uBTC
|
|
42
|
+
peak: 0.39 uBTC
|
|
43
|
+
equity: 154.14 uBTC
|
|
44
|
+
floor headroom: 79.14 uBTC
|
|
45
|
+
overrides used: 0
|
|
46
|
+
|
|
47
|
+
VERDICT: RECOVERY glacier
|
|
48
|
+
set target = 9800
|
|
49
|
+
set side = UNDER
|
|
50
|
+
set max_loss = 0.00000150
|
|
51
|
+
set max_rounds = 40
|
|
52
|
+
set max_profit = 0.00000200
|
|
53
|
+
set base_stake = 0.00000050
|
|
54
|
+
py -3.13 run_single_round.py 39 s12_glacier.json --note "R39 glacier recovery streak=1"
|
|
55
|
+
wrote s12_glacier.json (recovery spec)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why this exists
|
|
59
|
+
|
|
60
|
+
Most risk rules live inside the thing they are supposed to constrain. The
|
|
61
|
+
position sizer sits in the strategy, the drawdown limit sits in the bot, and
|
|
62
|
+
the first time the rule is inconvenient somebody comments it out.
|
|
63
|
+
|
|
64
|
+
This package separates the two. The governor is a pure function of
|
|
65
|
+
`(ledger, equity, policy)`, so it can be reviewed, diffed, unit-tested and
|
|
66
|
+
replayed against history without touching a live account. Execution stays in
|
|
67
|
+
whatever runner you already have.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
Pure standard library, no dependencies.
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
pip install riskgovernor # from PyPI
|
|
75
|
+
pip install -e ".[dev]" # from a checkout, with test deps
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## The verdict table
|
|
79
|
+
|
|
80
|
+
Evaluated in order; the first match wins.
|
|
81
|
+
|
|
82
|
+
| # | Condition | Verdict |
|
|
83
|
+
|---|-----------|---------|
|
|
84
|
+
| 1 | equity below `gates.floor` | `HALT` |
|
|
85
|
+
| 2 | loss streak at `gates.max_consecutive_losses` | `HALT` |
|
|
86
|
+
| 3 | drawdown at `gates.max_drawdown` | `HALT` |
|
|
87
|
+
| 4 | last episode's net was negative | `RECOVERY` — rotate strategy, size to the loss |
|
|
88
|
+
| 5 | otherwise | `STANDARD` — offer every strategy |
|
|
89
|
+
|
|
90
|
+
Gate 1 is skipped when no equity is supplied, so the same code runs offline.
|
|
91
|
+
|
|
92
|
+
`RECOVERY` rotates forward through the declared strategy order and wraps. With
|
|
93
|
+
two or more strategies it never re-selects the one that just lost. A
|
|
94
|
+
single-strategy policy cannot avoid doing so — the governor flags it as a
|
|
95
|
+
degenerate rotation in `notes` rather than pretending otherwise. `STANDARD`
|
|
96
|
+
deliberately chooses nothing: it lists the options and leaves the call to the
|
|
97
|
+
operator.
|
|
98
|
+
|
|
99
|
+
## The ledger
|
|
100
|
+
|
|
101
|
+
An ordered list of episodes. JSON, with the usual file conventions.
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"episodes": [
|
|
106
|
+
{"id": 37, "net": "0.000000389793877560", "strategy": "glacier"},
|
|
107
|
+
{"id": 38, "net": "-0.000003041134348000", "strategy": "plan1",
|
|
108
|
+
"note": "R38 plan1 standard streak=0"}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `id` may be any JSON value, but only **numeric** ids are episodes. Anything
|
|
114
|
+
else (`"RESET"`, `"OVERRIDE"`, `"P1"`) is bookkeeping: it never becomes the
|
|
115
|
+
"last episode", and a zero-net row ends a loss streak by construction.
|
|
116
|
+
- `net` is a decimal string. Unparseable values break the streak rather than
|
|
117
|
+
being silently treated as zero.
|
|
118
|
+
- Unknown keys survive a load/save round trip, and the original rows key
|
|
119
|
+
(`rounds`, `episodes`, `rows`) is preserved.
|
|
120
|
+
- `net`, `result` and `pnl` are all accepted as the result key.
|
|
121
|
+
|
|
122
|
+
`save()` writes the canonical schema: row ids become `id` (a ledger read as
|
|
123
|
+
`n` is re-emitted as `id`) and `note` is materialised on every row. Content is
|
|
124
|
+
preserved; spelling is not. Don't point `save()` at a ledger another tool
|
|
125
|
+
still reads by its own key names.
|
|
126
|
+
|
|
127
|
+
**Record the strategy on every row.** This is the one field the governor
|
|
128
|
+
cannot reconstruct. If it is missing, `RECOVERY` falls back to the first
|
|
129
|
+
strategy in the rotation and says so in `notes` — it does not guess silently.
|
|
130
|
+
|
|
131
|
+
## The policy
|
|
132
|
+
|
|
133
|
+
Plain JSON, so the risk rules can be reviewed on their own.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"unit": "USD",
|
|
138
|
+
"scale": "1",
|
|
139
|
+
"note_template": "E{episode} {strategy} {mode} streak={streak}",
|
|
140
|
+
"gates": {
|
|
141
|
+
"floor": "5000",
|
|
142
|
+
"max_consecutive_losses": 3,
|
|
143
|
+
"max_drawdown": "2000"
|
|
144
|
+
},
|
|
145
|
+
"strategies": [
|
|
146
|
+
{
|
|
147
|
+
"id": "core",
|
|
148
|
+
"config_files": ["core.json"],
|
|
149
|
+
"command": "python run_strategy.py {episode} {config} {note_arg}",
|
|
150
|
+
"recovery": {
|
|
151
|
+
"params": { "max_loss": "100", "max_rounds": 15 },
|
|
152
|
+
"size_param": "max_profit",
|
|
153
|
+
"cap": "200",
|
|
154
|
+
"stake_param": "base_stake",
|
|
155
|
+
"stake_base": "50",
|
|
156
|
+
"stake_halved": "25",
|
|
157
|
+
"halve_at_streak": 2
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Strategy order **is** the rotation order.
|
|
165
|
+
|
|
166
|
+
`recovery` describes what a recovery episode looks like:
|
|
167
|
+
|
|
168
|
+
- `params` — static overrides, applied verbatim.
|
|
169
|
+
- `size_param` / `cap` — that parameter is set to `min(|loss|, cap)`, so the
|
|
170
|
+
recovery target is sized to the loss that triggered it rather than being a
|
|
171
|
+
fixed constant.
|
|
172
|
+
- `stake_param` / `stake_base` / `stake_halved` / `halve_at_streak` — the stake
|
|
173
|
+
is cut once losses accumulate. De-risk on the way down, don't double up.
|
|
174
|
+
|
|
175
|
+
Command templates may use `{episode}`, `{strategy}`, `{mode}`, `{streak}`,
|
|
176
|
+
`{config}`, `{configs}`, `{note}` and `{note_arg}`.
|
|
177
|
+
|
|
178
|
+
### Units
|
|
179
|
+
|
|
180
|
+
Every number in the policy — gates, caps, recovery params — and every
|
|
181
|
+
`--equity` value is in **ledger units**: the same units as the ledger's `net`
|
|
182
|
+
values. `scale` and `unit` are presentation only and never participate in a
|
|
183
|
+
comparison. Keep them consistent; a `floor` written in display units against a
|
|
184
|
+
ledger in base units will read as a floor 10^6 times too high.
|
|
185
|
+
|
|
186
|
+
## Overrides are recorded, never silent
|
|
187
|
+
|
|
188
|
+
`HALT` is a hard stop, and the honest failure mode is not that the gate is
|
|
189
|
+
wrong — it is that a human overrides it. So overrides get their own row:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
governor.override(ledger, "resuming after review", operator="alice")
|
|
193
|
+
ledger.override_count() # 1
|
|
194
|
+
ledger.overrides() # the full audit trail
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The count is printed on every run. A rising number is the signal that a
|
|
198
|
+
threshold is set wrong or that someone is chasing losses — which is exactly
|
|
199
|
+
the thing the streak gate exists to catch. Record it; never clear it quietly.
|
|
200
|
+
|
|
201
|
+
## Writing recovery parameters
|
|
202
|
+
|
|
203
|
+
`ParamStore` applies resolved overrides to JSON config files, updating only the
|
|
204
|
+
keys it was given and preserving everything else:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from riskgovernor import Governor, ParamStore
|
|
208
|
+
|
|
209
|
+
governor = Governor(policy)
|
|
210
|
+
decision = governor.decide(ledger, equity=equity)
|
|
211
|
+
|
|
212
|
+
updates = governor.recovery_overrides(decision, ledger) # {file: {key: value}}
|
|
213
|
+
ParamStore("configs").apply(updates) # {} for non-recovery verdicts
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Every target file is read and validated **before** anything is written, so a
|
|
217
|
+
missing, corrupt or malformed file leaves all files untouched — a
|
|
218
|
+
partly-applied spec cannot land on disk.
|
|
219
|
+
|
|
220
|
+
## What this is not
|
|
221
|
+
|
|
222
|
+
- **Not an executor.** It prints commands. Approval is a separate step.
|
|
223
|
+
- **Not a strategy.** It has no view on whether your edge is real. Gates bound
|
|
224
|
+
damage; they do not create profit. A negative-expectancy process governed
|
|
225
|
+
perfectly is still negative-expectancy.
|
|
226
|
+
- **Not coupled to any market.** Nothing here knows what an episode is.
|
|
227
|
+
|
|
228
|
+
## Design notes
|
|
229
|
+
|
|
230
|
+
**Strategy identity belongs in the ledger.** An earlier version of the
|
|
231
|
+
predecessor script inferred "which strategy ran last" by globbing output files
|
|
232
|
+
in the working directory. That worked until a runner archived several rounds
|
|
233
|
+
into one directory, at which point the first filename to match a pattern won
|
|
234
|
+
and the governor recommended re-running the strategy that had just lost —
|
|
235
|
+
silently violating its own core rule. Persisted state beats sniffed state.
|
|
236
|
+
The artifact-scanning version in that script has since been made
|
|
237
|
+
recency-ordered, which is the best available repair when the ledger carries no
|
|
238
|
+
tag; in this package, the ledger is simply authoritative.
|
|
239
|
+
|
|
240
|
+
**Pure decision function.** No clock, no filesystem, no randomness. The same
|
|
241
|
+
inputs produce the same verdict, so behaviour can be pinned by tests and
|
|
242
|
+
replayed against history.
|
|
243
|
+
|
|
244
|
+
## Development
|
|
245
|
+
|
|
246
|
+
```console
|
|
247
|
+
python -m pytest tests -q
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
74 tests cover ledger parsing and metric derivation (including NaN and
|
|
251
|
+
non-finite rejection), gate ordering and precedence, rotation and recovery
|
|
252
|
+
sizing, config-file preservation and failure atomicity, policy validation,
|
|
253
|
+
and the CLI end to end.
|
|
254
|
+
|
|
255
|
+
## Origin
|
|
256
|
+
|
|
257
|
+
Extracted from a bankroll protocol built for a dice-betting automation. The
|
|
258
|
+
protocol did its job — floors held, streaks halted, the audit trail was
|
|
259
|
+
complete — and the underlying expectation was still negative, which is why
|
|
260
|
+
that part stayed where it was and this part was generalised instead.
|
|
261
|
+
|
|
262
|
+
## Licence
|
|
263
|
+
|
|
264
|
+
MIT.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "riskgovernor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministic bankroll risk governance: hard gates, strategy rotation, loss-sized recovery."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "kingkillery" }]
|
|
13
|
+
keywords = ["risk", "bankroll", "drawdown", "position-sizing", "trading"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Topic :: Office/Business :: Financial",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=7"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
riskgovernor = "riskgovernor.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["riskgovernor"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
riskgovernor = ["py.typed"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""riskgovernor - deterministic bankroll risk governance.
|
|
2
|
+
|
|
3
|
+
Hard gates, strategy rotation and loss-sized recovery, expressed as pure data
|
|
4
|
+
and a pure decision function. The governor recommends; a human approves.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .governor import Decision, Governor, Metrics, Verdict
|
|
8
|
+
from .ledger import Episode, Ledger, to_decimal
|
|
9
|
+
from .params import MissingConfigError, ParamStore
|
|
10
|
+
from .policy import Gates, RecoverySpec, RiskPolicy, Strategy
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Decision",
|
|
16
|
+
"Episode",
|
|
17
|
+
"Gates",
|
|
18
|
+
"Governor",
|
|
19
|
+
"Ledger",
|
|
20
|
+
"Metrics",
|
|
21
|
+
"MissingConfigError",
|
|
22
|
+
"ParamStore",
|
|
23
|
+
"RecoverySpec",
|
|
24
|
+
"RiskPolicy",
|
|
25
|
+
"Strategy",
|
|
26
|
+
"Verdict",
|
|
27
|
+
"to_decimal",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|