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.
@@ -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
+ [![CI](https://github.com/kingkillery/riskgovernor/actions/workflows/ci.yml/badge.svg)](https://github.com/kingkillery/riskgovernor/actions/workflows/ci.yml)
47
+ [![PyPI](https://img.shields.io/pypi/v/riskgovernor.svg)](https://pypi.org/project/riskgovernor/)
48
+ [![Python](https://img.shields.io/pypi/pyversions/riskgovernor.svg)](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
+ [![CI](https://github.com/kingkillery/riskgovernor/actions/workflows/ci.yml/badge.svg)](https://github.com/kingkillery/riskgovernor/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/riskgovernor.svg)](https://pypi.org/project/riskgovernor/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/riskgovernor.svg)](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
+ ]
@@ -0,0 +1,6 @@
1
+ """``python -m riskgovernor`` entry point."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())