Repository navigation
keel v0.19.0
Built from 71ddba2. Version binds to this hash:
keel --version reports keel 0.19.0+71ddba24c1f1 [release].
Install
Download all wheels from this release into one directory, then install the
keel_trader wheel by path:
pip install --find-links . ./keel_trader-0.19.0-py3-none-any.whl
keel versions
keel versions — not keel --version — is the check: it reports every
keel distribution in the venv and exits non-zero if a sibling was left behind at
an older version, which --version cannot see. Upgrading an existing
deployment: see "Deploying a new version" in the README.
keel-trader; the name
keel on PyPI belongs to an unrelated project, so pip install keel fetches
someone else's package. A build reporting DIRTY or [checkout] is not this
release and must not be run against live funds.
Configure
config.yaml is attached to this release: the production config, in
auto_trade.mode: confirm — keel previews every order and waits for your
approval. Drop it beside the install (or run keel init-config --live), put
your CDP key in a git-ignored .env, then:
keel migrate # existing database: apply schema migrations
keel init # fresh deployment: write config + seed candidate rules
Seeded rules start as candidate and trade nothing until you promote them.
Features
research: a 200-day SMA filter on the daily turtle -- no arm distinguishable from the baseline (#830) (#833)
Closes #830.
Four commits, in the order that matters:
-
d5e1485feat(strategy): an optionaltrend_filteronturtle_breakout(off|above|slope|both,trend_sma_period200,trend_slope_lookback5).- Default
off, so every existing rule behaves exactly as before. - Too little history to compute the average declines the entry (
trend_filter_history) rather than letting it through. - 8 tests, each failing first, using fixtures that assert their own premise. 5 of 5 mutants killed.
- Default
-
696bc5bThe pre-registration. The driver's docstring holds the arms, data, window, statistics, decision rule and my expectation. It was pushed at 00:39 UTC before any arm ran. -
37cc6acfix(research): the trials ledger now refuses two kinds of summary value it could never read back:- A non-numeric string raises on read, making the append-only ledger unreadable from that row on.
- A float breaks the row's hash, so
verify_chainreports it as tampered forever.
Both were found when the first run's four rows failed to read back. Those rows were never committed. One test each.
-
8604e80The record, data and four ledger rows (ablation,a_priori,diagnostic_only, with R series).
Result (1.20% taker plus per-product slippage, in R, common window)
| Arm | N | Win % | E[R] | Δ vs off, 95% CI (cluster bootstrap by entry day) |
|---|---|---|---|---|
off |
202 | 26.7% | +0.270 | — |
above |
181 | 25.4% | +0.218 | [−0.641, +0.540] |
slope |
121 | 29.8% | +0.419 | [−0.515, +0.829] |
both |
121 | 28.9% | +0.409 | [−0.524, +0.824] |
Pre-registered verdict: no arm is promising. Every arm clears the 100-trade floor, and no lower bound is above zero, so nothing goes to walk-forward.
slopeandbothpoint the hypothesised way, but inside an interval about 1.3 R wide.abovebarely binds and does slightly worse.- The largest effect in the data is the excluded first ~205 days (the 2022 bear-market onset): 18 trades averaging about −1.0 R.
- The baseline itself remains descriptive under ADR 0006: +5 win-rate points over break-even against a 14-point detectable edge.
Honesty notes
- Edits since the pre-registration: the driver changed only in how figures are stored (numeric strings; words moved to
params).git diff 696bc5b -- docs/experiments/2026-09-27-turtle-sma200-filter.pyshows every line. - Reproducibility: the run is deterministic (seed 830), and the committed figures equal the first run's.
- Prediction miss: 90% of entries were retained for
above, not the 40–60% I predicted.
Verification
- Full suite: 6785 passed, 3 skipped.
mypy,ruff checkandruff format --checkare clean.verify_chainon the tracked ledger: 98 rows, no problems.
research: accumulation policy after fees -- no arm beats static DCA (#831) (#835)
Closes #831. Implements the approved design (#834, revision 2).
Three commits, in the order that matters:
7a1e900feat(sim):keel/sim/accumulation_policy.py. Four allocators; an account with allowance-aware fees and average-cost lots; a joint stationary bootstrap; the D6 decision rule.- 47 tests, each seen failing first. 15 of 15 mutants killed; a 16th was equivalent, and its redundant code was removed.
- Arm A reproduces
dca_into_allowlistto the cent under zero fees on gapless candles. It fills at the next open where the benchmark fills at the close, as the design requires, and a second test pins that difference.
4113ed0The pre-registration. The driver's docstring is the design's §3–§8, frozen. It was pushed at 02:04 UTC before the run.git diff 4113ed0 -- docs/experiments/2026-09-27-accumulation-policy.pyis empty: the driver hasn't changed since.bd18caeThe record, data and 16 ledger rows (ablation,a_priori,diagnostic_only; chain verified, 114 rows).
Result (headline: 5 years without PAXG, allowance-aware fees, $30,500 deposited)
| Arm | Terminal | Sortino | Max DD | P(ΔSortino > 0), 20 / 60-day blocks | Verdict |
|---|---|---|---|---|---|
| A static DCA | $46,629 | 0.523 | 79.5% | — | baseline |
| B value averaging | $48,840 | 0.542 | 78.5% | 0.583 / 0.599 | not better |
| C steered deposits | $47,967 | 0.564 | 79.1% | 0.365 / 0.407 | not better |
| D selective trimming | $52,919 | 0.611 | 79.3% | 0.363 / 0.434 | not better |
No arm is better than static DCA after fees, in either run or fee model.
- Trimming (D) wins the historical path by 13.5% but improves Sortino in only 36–43% of 2,000 resampled paths. That's a path-specific result, and the record declines to call it a win.
- Value averaging (B) is the most consistent arm: 58–61% of paths, and about 2 points less drawdown. It is still far from the 0.95 bar.
- Prediction misses are recorded. D was expected to do best on the with-PAXG run and was instead the weakest alternative there. PAXG's slippage is about 100 bp per leg.
Verification
- Full suite: 6832 passed, 3 skipped.
mypyandruffare clean.verify_chainon the ledger: no problems.- The record's computed claims were re-checked against the data. Three were corrected before committing, and a causal story I hadn't tested was removed.
Fixes
fix(sim): two rules of one kind on one asset get a row each (#829) (#832)
Closes #829.
What was wrong
report.edge_table and report.accumulation_table (#825) keyed each row "{rule.name}:{asset}". When two rules of the same kind sit on the same asset, the later rule's row overwrote the earlier one's.
- On the deployment:
~/keel/keel.dbhas two paper BTC DCA rules, rule 19 at $50/week and rule 20 at $5/week.keel simulateprinted a single row, the $5 rule's, which looked like a ~$5/week DCA. The $50 sleeve was invisible. - A second, quieter effect:
group_trades_by_classlooked both rules up by that shared key. G2's per-class pool counted the surviving rule's trades twice and the other rule's not at all. The test below shows a class pool of 0 trades where the pooled row has 1. The pooled row itself was always right, because it extends every rule's trades before keying.
The fix
One function, report.rule_keys(rules), now keys all three tables:
- A key that is already unique is unchanged (
"turtle_breakout:BTC"), so existing readers and tests are untouched. - Rules that share a key are told apart by their
rules.id("dca#19:BTC","dca#20:BTC"), or by position when a hand-built rule has no id. No row is ever lost. - All three tables must be given the same
ruleslist, whichsimulatealready does. The docstrings say so.
Verification
-
tests/sim/test_rule_keys.pyhas 6 tests. Five failed first on the old code; the "unique key unchanged" test is a regression pin that passes throughout. They cover:- two DCA rules, each at its own budget
- two edge rules on one asset, with correct pooled N
- the class pool counting each rule once (it was 0, now 1)
- unique keys unchanged
- colliding rules without ids
- the rendered section showing both rows
-
Five mutants were each checked to have changed the source, and each was killed:
Mutant Tests failed never disambiguate 5 class pool uses the old key 1 accumulation uses the old key 2 always disambiguate 12 ignore rule_id3 -
Real data, on a copy of the paper database over the 5-year window:
Row Buys Cost Value dca#19:BTC261 $13,213.14 ($50.63/buy) $25,715.00 dca#20:BTC261 $1,321.31 ($5.06/buy) $2,571.50 Before the fix only the second row printed.
-
Full suite: 6775 passed, 3 skipped.
mypy,ruff checkandruff format --checkare clean.
fix(rails): rail 14 is a monthly buy cap, not a fee-free allowance (#836) (#837)
Closes #836.
What was wrong
keel described rail 14's cap as the venue's fee-free allowance, and its research treated "inside the allowance" as a fee-free regime. keel's orders go through Coinbase Advanced Trade, which has no fee-waiver tier:
- On 2026-09-27 the account reported the Intro tier: 0.9% taker, 0.5% maker,
has_promo_fee: false. - Every live fill had paid a fee: 1.19%, 1.14%, 1.19% and 0.89%.
- Coinbase One's "zero trading fees up to $500/month" applies on the main Coinbase platform, not Advanced Trade.
What changed (wording only; enforcement untouched)
keel/execution/guards.pyhas zero changed lines. The 29 rail 14 guard tests pass unchanged, which proves the cap, its fail-closed behaviour and every number it enforces are the same.- No schema rename. The
free_volume_usdfield keeps its historical name. A rename would need a migration and would change no behaviour. - Operator-facing text:
keel subscription set --helpnow says "Monthly BUY cap … not a fee waiver".- The throughput report says "route WITHIN each venue's attested buy cap … trades inside it still pay the venue's fee".
- The significance verdict's zero-fee phrase reads "a zero fee (hypothetical: Advanced Trade orders are never fee-free, #836)".
- The simulator's tier section adds a paragraph saying its within-cap rows are hypothetical for keel's orders.
- Docs:
docs/rails/rail-14-subscription-allowance.mdis retitled "the monthly buy cap". Its "What it bounds" section no longer claims a fee waiver or a "profitability boundary", and it carries a dated correction. The enforcement, veto and inspection sections are unchanged.- The runbook and the research toolkit are corrected in place.
- Records are appended to, never rewritten. Five records that described a fee-free regime each gain a dated Fee note under the title: the 2026-08-21 significance, 2026-08-22 trailing exits and 2026-09-05 restatement records, the quant-lab note cross-verification, and the 2026-09-27 accumulation-policy record. The accumulation note says its flat-taker run is the one matching live, and the verdict is unchanged.
- Code comments and docstrings now call the fee-free tier model hypothetical for keel's orders.
Tests
tests/test_rail14_is_a_buy_cap.py has 6 tests, all seen failing first:
- the help text,
- the throughput report,
- the significance label,
- the tier section,
- the rail 14 doc making no fee-waiver claim,
- every record that describes a fee-free regime carrying the #836 note.
One existing test that pinned the old "fee-free" wording is rewritten to pin the corrected label.
Not in scope, deliberately
- The
keel doctorfee-rate readout (#836 item 4, marked optional there) is left for a follow-up. - Switching the DCA to limit orders was decided against on 2026-09-27.
Verification
- Full suite: 6838 passed, 3 skipped.
mypy,ruff checkandruff format --checkare clean.
fix(rails): exempt DCA buys from the total-exposure rail; rail 14 bounds DCA (#841) (#842)
Closes #841
The decision (the operator's, 2026-09-27, final)
DCA is bounded by the venue plan's cap (rail 14, the attested monthly buy cap), not by
caps.max_exposure_usd.
- Exempt DCA BUYs from the total-exposure rail (
total_exposure_cap) only.- Per-asset concentration (
per_asset_concentration_cap) still applies to DCA.- Rail 14 (
monthly_subscription_allowance) still applies to DCA, unchanged. It is now the binding DCA limit.- Every other rail's treatment of DCA is unchanged.
The reason: the live account accumulates through 7 DCA rules. About $213 is held against the $400 max_exposure_usd cap, so rail 4 would veto the sleeve after about one buy on Oct 9.
Which rails bind DCA now
| Rail | Rule-trading BUY | DCA BUY |
|---|---|---|
4: total_exposure_cap |
binds | exempt (this PR) |
6: per_asset_concentration_cap |
binds | binds |
14: monthly_subscription_allowance |
binds | binds. It is the limit that bounds DCA. |
| 8: no averaging into losers | binds | exempt (unchanged) |
| 11: drawdown breaker | binds | exempt (unchanged) |
| 16: consecutive-loss breaker | binds | exempt (unchanged) |
| every other rail | binds | binds (unchanged) |
- DCA holdings still count in the total exposure that a rule-trading entry sees, so a growing DCA sleeve shrinks the room left for rule trades.
- Rail 6's per-asset limit is still
max_per_asset_pct × max_exposure_usd.max_exposure_usdstill bounds DCA per asset, but no longer in total. - SELLs are unaffected. Rail 4 never read a SELL.
What changed
keel/execution/guards.py: rail 4 now gates onis_buy and not intent.is_dca. I updated the module docstring's "DCA exemptions" paragraph, rail 4's comment and rail 14's comment. The docstring now also names rail 16, which was already exempt but was missing from that paragraph.keel/sim/account.py:SimAccount.can_openapplies the same exemption, sokeel simulatemodels what live does. The sim's DCA path (portfolio_sim._process_dca_signals) goes throughcan_open, which vetoes and does not clamp. Before this PR it vetoed DCA overmax_exposure_usd; now it does not.max_affordable_notionalis unchanged because only the rule-slot path calls it.docs/rails/rail-4-total-exposure.mdis new and holds the exemption table.docs/rails/rail-14-subscription-allowance.mdnow says rail 14 is what bounds DCA.- The
caps:comment inkeel/templates/config.yamlandconfig.live.yamlsays the same. The repo-rootconfig.yamlis re-synced with its template, astest_the_template_stays_in_sync_with_the_repo_configrequires. keel doctorhas no text that lists rails or their DCA exemptions, so nothing changed there.executor.pyis not touched.
This reaches live only after a release and deploy. Until then, the deployed rail 4 still vetoes DCA over max_exposure_usd.
Tests (written first; each was seen to fail for the stated reason, or is a pin)
| Test | Before the fix |
|---|---|
(a) test_rail4_dca_buy_over_max_exposure_is_not_vetoed_by_total_exposure |
FAIL: total_exposure_cap: open exposure 960.0 + 45 = 1005.0 exceeds max_exposure_usd 1000 |
(b) test_rail4_the_same_buy_without_is_dca_is_still_vetoed_by_total_exposure |
pass (regression pin; kills M3) |
(c) test_rail6_dca_buy_over_the_per_asset_cap_is_still_vetoed_by_concentration |
FAIL: extra total_exposure_cap beside per_asset_concentration_cap |
(d) test_rail14_dca_buy_over_the_monthly_cap_is_still_vetoed_when_exposure_is_also_over |
FAIL: extra total_exposure_cap beside monthly_subscription_allowance |
(e) test_a_dca_buy_over_max_exposure_is_not_vetoed_by_the_exposure_cap (sim) |
FAIL: total_exposure_cap … 1005 exceeds … 1000 |
(e) test_a_dca_buy_over_the_per_asset_cap_is_vetoed_by_concentration_only (sim) |
FAIL: total_exposure_cap listed before per_asset_concentration_cap |
(e) test_a_rule_buy_over_max_exposure_is_still_vetoed_by_the_exposure_cap (sim) |
pass (regression pin; kills M6) |
(e) test_parity_with_guards_check_when_total_exposure_is_the_tightest_cap |
FAIL: sim vetoed a 100 DCA buy that the fixed guards allow |
(f) test_rail4_never_gates_a_sell_whatever_the_order_class[False/True] |
pass (pin) |
Two existing sim tests, test_exposure_cap_rejects_over_cap and test_close_frees_up_exposure_for_a_subsequent_open, probed the exposure cap with the sim helper's default is_dca=True intent. They now probe with a rule-trading intent, which is what they test.
Mutants (each proven applied: the source differed from the saved copy; restored from that copy, never git checkout)
| Mutant | Result |
|---|---|
| M1: drop the guards exemption | killed by (a), (c), (d) and the parity test |
| M2: exempt DCA from rail 6 instead of rail 4 | killed by (a), (c), (d) and the parity test |
| M3: exempt non-DCA buys too (guards) | killed by (b), test_rail4_total_exposure_cap_rejects_over_cap and test_a_malformed_history_row_still_counts_toward_the_exposure_cap |
| M4: remove the sim exemption | killed by both (e) DCA tests and the parity test |
| M5: sim exempts DCA from concentration instead | killed by 5 sim tests |
| M6: sim exempts rule entries too | killed by the (e) rule pin and 2 existing sim exposure tests |
Checks
uv run pytest -q: 6850 passed, 3 skipped, exit 0uv run ruff check keel tests: exit 0uv run ruff format --check keel tests: exit 0uv run mypy: no issues in 473 files, exit 0
fix(executor): size each DCA buy from the rule's own amount (#840) (#843)
Closes #840
Milestone: DCA plan — proposal CLI and read-only card
What changed
keel/execution/executor.py _build_intent now sizes a DCA ENTER from the rule's amount, setup.context["size_usd"] (what Dca.detect computes: budget_usd × (1 + dip bonus)), instead of the config's single dca.budget_usd.
- A new helper,
_dca_budget(context, fallback), returns(amount, "rule" | "config"). It falls back toconfig.dca.budget_usdonly whensize_usdis absent or is not a positive, finite number. Aboolis refused too.Decimal('Infinity') > 0is true, which is why the finite check is there:commands/rules.pydocuments how an infinitebudget_usdgets through. - The source is logged as
executor.dca_sized, at INFO forruleand WARNING forconfig, with product, rule, rule_id and amount. - Paper:
agent._paper_entersizes through the sameexecutor._build_intentand fillsintent.qty, so paper is fixed by the same change. A test pins it. The account sim (sim/portfolio_sim.py) already preferredsize_usd, so live, paper and the sim now agree. - No rail, cap, guard or veto changed, and
max_exposure_usdis untouched.intent.notionalis stillspend(qty, entry)from the new qty, so rail 14 and every other guard see the real order, which is now smaller. - Docs: the executor's "CAREFUL" comment is rewritten. It records that the old "config is the operator-facing dial" rationale was reversed by the operator on 2026-09-27. Also updated:
- the executor module docstring and the
_order_specdocstring; - a new
DcaConfigdocstring (inpackages/keel-core); - the
dca:comment inconfig.yaml,keel/templates/config.yamlandkeel/templates/config.live.yaml; equity.sizing_equity's docstring;docs/operator-runbook.mdanddocs/RELEASING.md.
- the executor module docstring and the
Before and after
| rule | asset | amount | cadence | buys/month (30.44 d) | before ($50 each) | after (rule's amount) |
|---|---|---|---|---|---|---|
| 6 | BTC | $40 | 7d | 4.35 | $217.4 | $173.9 |
| 8 | ETH | $25 | 7d | 4.35 | $217.4 | $108.7 |
| 9 | PAXG | $25 | 14d | 2.17 | $108.7 | $54.4 |
| 10–13 | ADA/XLM/DOGE/FET | $15 each | 14d | 2.17 each | $434.9 (4 × 108.7) | $130.5 (4 × 32.6) |
| ≈ $978 | ≈ $467 |
Before, the book tried to spend about $978 a month against rail 14's $500 cap, so rail 14 vetoed the tail of each month. After, it spends the rules' ~$467, which fits under the cap. Rail 14 is unchanged and still vetoes any buy that would cross it.
This changes live behaviour only after a release and deploy. The live deployment runs installed wheels, not this repo.
Tests (each one seen failing before the implementation)
| test | what it pins | red before the fix |
|---|---|---|
(a) test_live_dca_sizes_from_the_rules_size_usd_not_the_config_budget |
through the real execute() path, size_usd=25 at 50,000 places qty 0.0005 with quote_size 25, and logs source=rule |
0.001 != 0.0005 |
(b) test_live_dca_falls_back_to_the_config_budget_without_a_usable_size_usd ×8 |
absent, None, 0, −25, Infinity, NaN, "25" and True all size from the config's 50 and log source=config |
no executor.dca_sized record (fallback sizing already held; recording the source is new) |
(c) test_a_dca_rules_detect_feeds_the_live_order_size_end_to_end |
Dca(budget_usd=15).detect(...) → execute() → qty 0.0005 at 30,000 |
0.001666… != 0.0005 |
(c′) test_a_dip_scaled_dca_buy_spends_size_usd_not_the_rules_base_budget |
with a dip, size_usd 18.75 ≠ budget_usd 15, and the order spends 18.75 |
added after the fix; red against the old code via mutant M1 below |
(d) test_paper_dca_fill_is_sized_from_the_rules_amount_not_the_config_budget |
_paper_enter with a real Dca(budget_usd=15) fills 0.5 at 30 |
1.666… != 0.5 |
(e) test_rail_14_sees_the_rules_true_notional_and_admits_a_25_dollar_buy_that_fits |
$460 spent + $25 = $485 against a $485 cap is placed | intent notional 50.000 != 25 |
(e) test_rail_14_still_vetoes_a_25_dollar_dca_buy_that_would_cross_the_cap |
$460 + $25 = $485 against a $480 cap is vetoed, and the veto names 460 + 25 = 485 |
the veto named 50.000 |
Mutants (each proven applied by a changed sha256, restored from a saved copy, never git checkout)
| mutant | result | killed by |
|---|---|---|
M1: revert to config.dca.budget_usd |
KILLED (6 fail) | (a), (c), (c′), (d), both (e) |
M2: drop the fallback (use size_usd unconditionally) |
KILLED (10 fail) | all 8 (b) cases plus 2 existing DCA tests |
M3: read budget_usd instead of size_usd |
KILLED (4 fail) | (a), (c′), both (e) |
M4: accept size_usd <= 0 (< 0) |
KILLED (1 fail) | (b)[zero] |
| M5: drop the finite check | KILLED (2 fail) | (b)[infinity], (b)[nan] |
| M6: drop the bool guard | KILLED (1 fail) | (b)[bool] |
Checks
uv run pytest -q: 6854 passed, 3 skipped, exit 0uv run ruff check keel tests: exit 0uv run ruff format --check keel tests: exit 0uv run mypy(bare): no issues in 473 source files, exit 0
Not done here
deploy/live-rules.jsonstill records a single BTC DCA rule atbudget_usd: "50". The brief lists seven live DCA rules, with BTC at $40. Now that the rule's amount is what spends, that manifest is worth syncing from the deployment in a separate change.- The sim's own fallback (
size_usd or config.dca.budget_usd) still accepts a negativesize_usd. It is not touched here, andDcarefuses a non-positivebudget_usdat construction. - The root
config.*.yamlprofiles (config.live-sandbox.yaml, the paper profiles) keep their uncommenteddca:blocks.
Research & validation
docs(spec): accumulation policy -- value averaging and band rebalancing vs static DCA (#831) (#834)
Refs #831. A draft for review. No code and no simulation until it's approved. Merging this PR is the approval; comments or edits here are the review.
docs/superpowers/specs/2026-09-27-accumulation-policy-design.md covers:
- The formulas.
- Bounded value averaging:
ΔC = clamp(w·D·t − H, C_min·w·D, C_max·w·D), with a zero-growth target path, buy-only, and C_max = 3. - Contribution steering: the monthly deposit goes to underweight assets first; it never sells.
- Band rebalancing: a ±15% band relative to each target weight, checked monthly, trimming and topping up back to target.
- Bounded value averaging:
- The account model every arm shares.
- Deposits: identical $500 monthly deposits. Idle cash earns nothing, consistent with the fiqh constraints.
- Fees: modelled the way this account is actually billed. Buys are fee-free up to the $500/month Basic allowance; sells always pay the 1.20% taker fee. A flat-taker sensitivity run sits beside it.
- Harness requirements.
- A new pure allocator module, separate from
portfolio_sim. - Static DCA must reproduce
dca_into_allowlistexactly, as the oracle for the whole harness. - Tests come first.
- A new pure allocator module, separate from
- Rails. This phase is simulation only. The spec records what a live version would hit. Most notably,
sell_only_on_rulerequires arule_kindon every sale, so rebalancing trims need a policy decision. The two buy-only arms avoid that question entirely. - The prior it has to answer. The knowledge base (§51) calls fixed-weight rebalancing the wrong paradigm here, because correlated crypto offers little dispersion to harvest. The spec sharpens the hypothesis: any rebalancing benefit should come through PAXG, the one low-correlation leg.
Revision 2 (from review): selective trimming in arm D, a ±1.5-point band floor for the small sleeves, and 20- and 60-day bootstrap blocks reported side by side, with the decision rule required to hold under both.
Decisions for you (§9)
| # | Decision | Proposed |
|---|---|---|
| D1 | Universe and window | PAXG's Coinbase history starts in May 2025, so: a 5-year PAXG-free primary run plus a 1.4-year with-PAXG secondary run |
| D2 | Monthly deposit | $500 |
| D3 | Fee model | allowance-aware, plus a flat-taker sensitivity run |
| D4 | Value averaging | C_min 0, C_max 3, g = 0 |
| D5 | Band and trimming | max(0.15·w_i, 0.015), monthly; selective: trim only upper-band breaches, proceeds to underweights |
| D6 | Decision rule | P(ΔSortino > 0) ≥ 0.95 and median Δmax-drawdown not worse, under both 20- and 60-day bootstrap blocks |
Once D1–D6 are settled, §3–§8 become the driver's pre-registered docstring, committed before any run, exactly as for #830.
Other changes
chore(release): 0.19.0 -- DCA sized per rule, bounded by the venue plan's cap (#845)
Version bump only: all 8 pyprojects, their pins, and uv.lock go from 0.18.0 to 0.19.0. Same shape as #828.
MINOR because live DCA sizing changes (#843) and a rail changes scope (#842). No schema change.
- #843 (#840): each DCA buy is sized from its rule's
size_usd - #842 (#841): DCA buys are exempt from rail 4; rails 14 and 6 still bind them
- #837 (#836): rail 14 relabelled as a monthly buy cap
Local: pytest 6882 passed / 3 skipped, ruff and mypy clean.