Problem
There is no supported way to change a stored rule's parameters. keel rules offers
add, seed, promote, demote, disable, enable, list, backtest, lookahead --
all lifecycle, none editing. add only inserts a fresh candidate.
So correcting a parameter on a live rule means hand-editing JSON inside SQLite:
.venv/bin/python -c "import json,sqlite3;d=sqlite3.connect('keel-live.db');\
p=json.loads(d.execute('select params from rules where id=6').fetchone()[0]);\
p['budget_usd']='50';d.execute('update rules set params=? where id=6',(json.dumps(p),));d.commit()"
That bypasses every validation the codebase has: the Rule.decimal_params /
granularity_param / tuple_params coercion tables (#447), parse_products_option's
product validation (which rules seed runs and names as the reason a bad id is refused
before anything is written), and build_rule_from_params, described in keel/agent.py:156
as "THE single boundary".
How this bit, concretely
keel-live.db rule 6 (dca, BTC-USD) carried budget_usd: "25" while
config.live-sandbox.yaml and deploy/live-rules.json both said "50". The divergence is
already documented at keel/execution/executor.py:673:
CAREFUL: the live path sizes DCA from the CONFIG's dca.budget_usd, and ignores the
RULE's own budget_usd [...] a rule row saying 25 while the config says 50 spends 50.
It surprised us once; do not assume the rule's number is what moves.
The consequence named two lines later is the real cost: sim/portfolio_sim.py:593 prefers
context["size_usd"], so the simulator and the live path modelled different position
sizes -- the backtest was not measuring what the account was doing. Reconciling it
required the raw SQL above, on a live trading database, with no schema check and no audit
trail.
keel/commands/rules.py:1513 already documents a related failure in the same area: a
budget_usd stored as the string "Infinity" yields size_usd=Decimal('Infinity') "with
nothing raising anywhere".
Proposed
keel rules update-params <id> --param key=value [--param ...], which:
- routes the merged params through
build_rule_from_params and refuses if it raises,
so the coercion tables and product validation apply exactly as they do on seed/add
- refuses
Infinity/NaN for any decimal_params member (the #1513 hole)
- warns when the edited key is one the live path ignores in favour of
config --
dca.budget_usd being the known case -- naming both values, so an operator cannot
"fix" a number that does not move
- requires confirmation for a rule at
status=live, like the other dangerous verbs
- records the before/after in the audit chain
Also worth considering
A doctor check for the divergence itself: config dca.budget_usd, the stored rule row,
and deploy/live-rules.json disagreeing is exactly the class of silent skew doctor
exists to surface, and it would have caught this without anyone looking.
Found while diagnosing why the live loop had placed no order since 2026-08-21.
Problem
There is no supported way to change a stored rule's parameters.
keel rulesoffersadd,seed,promote,demote,disable,enable,list,backtest,lookahead--all lifecycle, none editing.
addonly inserts a freshcandidate.So correcting a parameter on a live rule means hand-editing JSON inside SQLite:
That bypasses every validation the codebase has: the
Rule.decimal_params/granularity_param/tuple_paramscoercion tables (#447),parse_products_option'sproduct validation (which
rules seedruns and names as the reason a bad id is refusedbefore anything is written), and
build_rule_from_params, described inkeel/agent.py:156as "THE single boundary".
How this bit, concretely
keel-live.dbrule 6 (dca, BTC-USD) carriedbudget_usd: "25"whileconfig.live-sandbox.yamlanddeploy/live-rules.jsonboth said"50". The divergence isalready documented at
keel/execution/executor.py:673:The consequence named two lines later is the real cost:
sim/portfolio_sim.py:593preferscontext["size_usd"], so the simulator and the live path modelled different positionsizes -- the backtest was not measuring what the account was doing. Reconciling it
required the raw SQL above, on a live trading database, with no schema check and no audit
trail.
keel/commands/rules.py:1513already documents a related failure in the same area: abudget_usdstored as the string"Infinity"yieldssize_usd=Decimal('Infinity')"withnothing raising anywhere".
Proposed
keel rules update-params <id> --param key=value [--param ...], which:build_rule_from_paramsand refuses if it raises,so the coercion tables and product validation apply exactly as they do on
seed/addInfinity/NaNfor anydecimal_paramsmember (the #1513 hole)config--dca.budget_usdbeing the known case -- naming both values, so an operator cannot"fix" a number that does not move
status=live, like the other dangerous verbsAlso worth considering
A
doctorcheck for the divergence itself: configdca.budget_usd, the stored rule row,and
deploy/live-rules.jsondisagreeing is exactly the class of silent skewdoctorexists to surface, and it would have caught this without anyone looking.
Found while diagnosing why the live loop had placed no order since 2026-08-21.