Skip to content

fix(sim): two rules of one kind on one asset get a row each (#829) - #832

Merged
eaitbrahim merged 1 commit into
mainfrom
fix/rule-key-collision
Sep 27, 2026
Merged

eaitbrahim merged 1 commit into
mainfrom
fix/rule-key-collision

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

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.db has two paper BTC DCA rules, rule 19 at $50/week and rule 20 at $5/week. keel simulate printed 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_class looked 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 rules list, which simulate already does. The docstrings say so.

Verification

  • tests/sim/test_rule_keys.py has 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_id 3
  • Real data, on a copy of the paper database over the 5-year window:

    Row Buys Cost Value
    dca#19:BTC 261 $13,213.14 ($50.63/buy) $25,715.00
    dca#20:BTC 261 $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 check and ruff format --check are clean.

🤖 Generated with Claude Code

edge_table and accumulation_table keyed rows "{rule.name}:{asset}", so a
second rule of the same kind on the same asset overwrote the first: the
deployment's $50/week BTC DCA sleeve disappeared behind a $5 one.
group_trades_by_class looked both rules up by the shared key, so G2's
class pool counted the survivor twice and the other rule not at all.

One key function, rule_keys, now serves all three: a unique key is
unchanged; colliding rules are told apart by rules.id ("dca#19:BTC"), or
by position when a hand-built rule has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eaitbrahim eaitbrahim added fix Bug fix (groups under Fixes) research Measurement, validation or KB work (Research & validation) labels Sep 26, 2026
@eaitbrahim
eaitbrahim merged commit 759a9a7 into main Sep 27, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the fix/rule-key-collision branch September 27, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix (groups under Fixes) research Measurement, validation or KB work (Research & validation)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

simulate: two rules of one kind on one asset collide in the edge and accumulation tables (a DCA sleeve vanishes)

1 participant