fix(sim): DCA buys once per completed day, and the report shows its sleeve (#821) - #825
Merged
Merged
Conversation
…leeve (#821) The account sim evaluated the real Dca rule on every hourly bar, and its daily window included the still-forming day, so every cadence day bought 24 times at a close that had not happened yet (9 cadence days at $50 spent ~$10,768). The sleeve was then invisible: the edge table gave DCA N 0 (it never saw daily candles) and per_asset_pnl counted closed trades only. - Dca decides on completed days only, via the forming-day guard TurtleBreakout already used, now shared as rules.base.completed_days. Live input (closed candles only) is kept unchanged. - portfolio_sim takes at most one DCA decision per (asset, rule, UTC day), logs each buy (SimResult.dca_buys) and records final prices. - edge_table skips accumulating rules (Rule.accumulates); the new accumulation_table reports buys, cost, mark-to-market value and unrealized P&L, outside __pooled__ and G2. - The account report and the HTML artifact show the DCA sleeve marked to market; per-asset P&L is labelled as realized rule P&L. Closes #821 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Sep 26, 2026
…oled evaluation as policy (#828) MINOR: #826 changes what promotion decides (G2 and #338's pooled path judge in R; min_expectancy is an R threshold; a sample with no R is refused), and #825 adds report sections. No schema change since 0.17.0. What lands: #826 (#820) -- edge table, G2 and promotion in R; negative-risk flip fixed; pooled rows in exit-time order. #825 (#821) -- simulate buys DCA once per completed day (was 24x per cadence day), no forming-day look-ahead, DCA sleeve reported. #824 (#822) -- ADR 0006: daily strategies evaluated on the pooled sample. #827 (#823) -- experiment record: hourly turtle, 4,871 trades. #819 -- live per-asset ceiling config mirrored (0.50 -> 0.75). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
keel simulategot the DCA rule wrong in four ways, and the report hid all of them.keel simulaterun with a DCA rule has an oversized DCA sleeve. That inflates or distorts its ending value, return, drawdown, caps headroom and rail-14 trading volume.Dca.detectpriced its buy at that day's close and measured the dip against that day's high. Live only has closed candles, so live DCA sees yesterday, and the simulator's cadence day was one day ahead of live.TurtleBreakoutalready guarded against this with_completed_days;Dcahad no guard.Dcadeclares no timeframe, sobacktest()gave it only hourly candles. It needs daily candles, so it never fired. Declaring a daily timeframe would not have fixed it. Itstarget=entry/stop=0sentinels would turn each buy into a fee-only round trip closed on its fill bar, which is a fake loss and not an edge.per_asset_pnlandtrade_countcome from closed trades only, so the sleeve was insideending_valueand appeared nowhere else in the Markdown report or the HTML artifact.What changed
keel/strategy/rules/base.pyascompleted_days, with its docstring.TurtleBreakoutimports it under its old private name, so every existing reference still resolves.Dca.detectnow uses it. Live behaviour is unchanged: when the last daily bar is closed and the hourly series is past it, that bar is kept. A test pins this.portfolio_sim. The day is marked when the decision is taken, whether the buy fills or is vetoed. That matches live, where the agent trades once per UTC day. Each buy is logged inSimResult.dca_buys(decision time, fill time, qty, notional, and cash cost including fees).SimResult.final_pricesrecords each asset's last close.Rule.accumulatesflag, which today meansDca, are left out ofedge_table. So they are not in__pooled__orgroup_trades_by_class, and G2 gets neither fake round trips nor a zero-trade sample. The newreport.accumulation_tableruns them over daily candles, using the same completed-day and once-per-day rules. It reports buys, qty, cost basis including fees, last close, marked value and unrealized P&L. The report renders it as its own section, "DCA accumulation (not round trips)", right after the edge table. The edge table's own columns are untouched, so simulate: the Edge table's "unit-less R-multiples" are price units, so the pooled row is BTC #820's R-multiple change should merge cleanly.build_account_metricsnow returnsdca_sleeve, built byportfolio_sim.dca_sleeve: per asset, the buys, qty, cost basis, value at the last close, and unrealized P&L. The Markdown report shows it under "DCA sleeve (accumulation, marked to market)". The per-asset list is now labelled "Per-asset realized rule P&L (closed round trips; the DCA sleeve is below)". The HTML artifact gets the same sleeve table, and its chart is retitled "Per-asset realized rule P&L".keel rules promote: nothing changes. Its docstring says a DCA rule produces no backtest trades, so--forceis its only path to paper. That is still true.backtest()is unchanged, a test pins N 0, and the promotion path never reads the accumulation row.Before and after (synthetic repro: 60 days of flat-ish hourly and daily candles, cadence 7, $50)
dca:BTCrow with N 0per_asset_pnl{}The window has 9 cadence days, but the ninth is its last day. That day's decision falls on the next day, outside the window, so the result is 8 buys, not 9.
Verification
dca:BTCround-trip key; the other tests failed on missing names or keys.tests/sim/test_dca_sleeve.py, all using the realDca: (a) total spend equals budget × cadence days; (d) accumulation row plus a pooled result byte-identical with or without DCA, andgroup_trades_by_classis empty; (e) sleeve value equals qty × last close and unrealized equals value − cost; (f) the Markdown tables are parsed row by row. Also: the realized-P&L label, and N 0 frombacktest().tests/strategy/test_dca.py: (b) at hour 0 the forming day's close and high are ignored, and a forming cadence day does not fire; (c) live-shaped input still fires on the closed day. (c) is a regression pin, so it passes both before and after the change.tests/sim/test_artifact.py: the sleeve table header and cells, and no table when there is no sleeve.Dca(5 failed)edge_tableand the pool (2 failed)completed_daysdrops the last day whenever hourly data exists, the live regression (1 failed)uv run pytest -q: 6735 passed, 3 skipped.uv run ruff check keel testsis clean,uv run ruff format --check keel testsis clean, anduv run mypyreports no issues. No baseline fixtures changed.Closes #821
🤖 Generated with Claude Code