riskgovernor
Deterministic bankroll risk governance: hard gates, strategy rotation, and loss-sized recovery, expressed as pure data plus a pure decision function.
The governor answers exactly one question — what should the next episode be, given what has already happened? — and it answers it the same way every time. It never places an order. It prints a command for a human to approve.
30-second start
$ pip install riskgovernor
$ riskgovernor demo
demo needs no files and walks through every verdict in order — STANDARD,
RECOVERY, both HALTs, and what an override looks like. When you want your
own setup:
$ riskgovernor init # writes policy.json, ledger.json, config stubs
$ riskgovernor decide --policy policy.json --ledger ledger.json
Three commands, zero reading required. The rest of this README is what's underneath.
$ riskgovernor decide --policy policy.json --ledger ledger.json --equity 0.00015414
--- RISK GOVERNOR ---
last episode: 38
last net: -3.04 uBTC
last strategy: plan1
streak: 1
drawdown: 3.04 uBTC
cumulative: -2.65 uBTC
peak: 0.39 uBTC
equity: 154.14 uBTC
floor headroom: 79.14 uBTC
overrides used: 0
VERDICT: RECOVERY glacier
set target = 9800
set side = UNDER
set max_loss = 0.00000150
set max_rounds = 40
set max_profit = 0.00000200
set base_stake = 0.00000050
py -3.13 run_single_round.py 39 s12_glacier.json --note "R39 glacier recovery streak=1"
wrote s12_glacier.json (recovery spec)
Why this exists
Most risk rules live inside the thing they are supposed to constrain. The position sizer sits in the strategy, the drawdown limit sits in the bot, and the first time the rule is inconvenient somebody comments it out.
This package separates the two. The governor is a pure function of
(ledger, equity, policy), so it can be reviewed, diffed, unit-tested and
replayed against history without touching a live account. Execution stays in
whatever runner you already have.
Install
Pure standard library, no dependencies.
pip install riskgovernor # from PyPI
pip install -e ".[dev]" # from a checkout, with test deps
The verdict table
Evaluated in order; the first match wins.
| # | Condition | Verdict |
|---|---|---|
| 1 | equity below gates.floor |
HALT |
| 2 | loss streak at gates.max_consecutive_losses |
HALT |
| 3 | drawdown at gates.max_drawdown |
HALT |
| 4 | last episode's net was negative | RECOVERY — rotate strategy, size to the loss |
| 5 | otherwise | STANDARD — offer every strategy |
Gate 1 is skipped when no equity is supplied, so the same code runs offline.
RECOVERY rotates forward through the declared strategy order and wraps. With
two or more strategies it never re-selects the one that just lost. A
single-strategy policy cannot avoid doing so — the governor flags it as a
degenerate rotation in notes rather than pretending otherwise. STANDARD
deliberately chooses nothing: it lists the options and leaves the call to the
operator.
The ledger
An ordered list of episodes. JSON, with the usual file conventions.
{
"episodes": [
{"id": 37, "net": "0.000000389793877560", "strategy": "glacier"},
{"id": 38, "net": "-0.000003041134348000", "strategy": "plan1",
"note": "R38 plan1 standard streak=0"}
]
}
idmay be any JSON value, but only numeric ids are episodes. Anything else ("RESET","OVERRIDE","P1") is bookkeeping: it never becomes the "last episode", and a zero-net row ends a loss streak by construction.netis a decimal string. Unparseable values break the streak rather than being silently treated as zero.- Unknown keys survive a load/save round trip, and the original rows key
(
rounds,episodes,rows) is preserved. net,resultandpnlare all accepted as the result key.
save() writes the canonical schema: row ids become id (a ledger read as
n is re-emitted as id) and note is materialised on every row. Content is
preserved; spelling is not. Don't point save() at a ledger another tool
still reads by its own key names.
Record the strategy on every row. This is the one field the governor
cannot reconstruct. If it is missing, RECOVERY falls back to the first
strategy in the rotation and says so in notes — it does not guess silently.
The policy
Plain JSON, so the risk rules can be reviewed on their own.
{
"unit": "USD",
"scale": "1",
"note_template": "E{episode} {strategy} {mode} streak={streak}",
"gates": {
"floor": "5000",
"max_consecutive_losses": 3,
"max_drawdown": "2000"
},
"strategies": [
{
"id": "core",
"config_files": ["core.json"],
"command": "python run_strategy.py {episode} {config} {note_arg}",
"recovery": {
"params": { "max_loss": "100", "max_rounds": 15 },
"size_param": "max_profit",
"cap": "200",
"stake_param": "base_stake",
"stake_base": "50",
"stake_halved": "25",
"halve_at_streak": 2
}
}
]
}
Strategy order is the rotation order.
recovery describes what a recovery episode looks like:
params— static overrides, applied verbatim.size_param/cap— that parameter is set tomin(|loss|, cap), so the recovery target is sized to the loss that triggered it rather than being a fixed constant.stake_param/stake_base/stake_halved/halve_at_streak— the stake is cut once losses accumulate. De-risk on the way down, don't double up.
Command templates may use {episode}, {strategy}, {mode}, {streak},
{config}, {configs}, {note} and {note_arg}.
Units
Every number in the policy — gates, caps, recovery params — and every
--equity value is in ledger units: the same units as the ledger's net
values. scale and unit are presentation only and never participate in a
comparison. Keep them consistent; a floor written in display units against a
ledger in base units will read as a floor 10^6 times too high.
Overrides are recorded, never silent
HALT is a hard stop, and the honest failure mode is not that the gate is
wrong — it is that a human overrides it. So overrides get their own row:
governor.override(ledger, "resuming after review", operator="alice")
ledger.override_count() # 1
ledger.overrides() # the full audit trail
The count is printed on every run. A rising number is the signal that a threshold is set wrong or that someone is chasing losses — which is exactly the thing the streak gate exists to catch. Record it; never clear it quietly.
Writing recovery parameters
ParamStore applies resolved overrides to JSON config files, updating only the
keys it was given and preserving everything else:
from riskgovernor import Governor, ParamStore
governor = Governor(policy)
decision = governor.decide(ledger, equity=equity)
updates = governor.recovery_overrides(decision, ledger) # {file: {key: value}}
ParamStore("configs").apply(updates) # {} for non-recovery verdicts
Every target file is read and validated before anything is written, so a missing, corrupt or malformed file leaves all files untouched — a partly-applied spec cannot land on disk.
What this is not
- Not an executor. It prints commands. Approval is a separate step.
- Not a strategy. It has no view on whether your edge is real. Gates bound damage; they do not create profit. A negative-expectancy process governed perfectly is still negative-expectancy.
- Not coupled to any market. Nothing here knows what an episode is.
Design notes
Strategy identity belongs in the ledger. An earlier version of the predecessor script inferred "which strategy ran last" by globbing output files in the working directory. That worked until a runner archived several rounds into one directory, at which point the first filename to match a pattern won and the governor recommended re-running the strategy that had just lost — silently violating its own core rule. Persisted state beats sniffed state. The artifact-scanning version in that script has since been made recency-ordered, which is the best available repair when the ledger carries no tag; in this package, the ledger is simply authoritative.
Pure decision function. No clock, no filesystem, no randomness. The same inputs produce the same verdict, so behaviour can be pinned by tests and replayed against history.
Development
python -m pytest tests -q
74 tests cover ledger parsing and metric derivation (including NaN and non-finite rejection), gate ordering and precedence, rotation and recovery sizing, config-file preservation and failure atomicity, policy validation, and the CLI end to end.
Origin
Extracted from a bankroll protocol built for a dice-betting automation. The protocol did its job — floors held, streaks halted, the audit trail was complete — and the underlying expectation was still negative, which is why that part stayed where it was and this part was generalised instead.
Licence
MIT.
Release files for riskgovernor 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| riskgovernor-0.1.0.tar.gz | 29.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| riskgovernor-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.6 kB
Release files / riskgovernor-0.1.0.tar.gz
| Download URL | riskgovernor-0.1.0.tar.gz |
|---|---|
| Size | 29.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cae05f507f2fdbb4774457a5657bd8e30f4951b1ea75ede6a25cdbb328d41ac7
|
|
BLAKE2b-256 checksum How to use checksums |
6c52d7c7d48ff080f9b7915ea4efa560643475e4873a834e25c5e503b81c632b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.
Transparency logRelease files / riskgovernor-0.1.0-py3-none-any.whl
| Download URL | riskgovernor-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3c1c0dcb08fb1f26ff5e66df128072688c2cc7873a14080c8159a6557069e69e
|
|
BLAKE2b-256 checksum How to use checksums |
94b99d85dd6092ffe40faf162e45353fa316af9b246077ab21b5b50a949d79b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.
Transparency log