Skip to content

fix(orchestrate): the firing-rate channel was dead through the seam - #17

Merged
mspinola merged 1 commit into
mainfrom
claude/edge-decay-firing-rate
Aug 3, 2026
Merged

fix(orchestrate): the firing-rate channel was dead through the seam#17
mspinola merged 1 commit into
mainfrom
claude/edge-decay-firing-rate

Conversation

@mspinola

@mspinola mspinola commented Aug 3, 2026

Copy link
Copy Markdown
Owner

check_decay built its TradeLog from bare floats, TradeLog.from_arrays(trade_r), while crucible derives the live firing rate from the log's entry_date. So live_trades_per_year and frequency_ratio came back None on every cycle, for every book, however carefully the baseline's own rate had been frozen at promotion.

Two of the monitor's three channels ran. The third reported itself off, in a reason line that read as a fact about the book ("live log carries no entry_date") when it was a fact about the seam. npf had just gone to some trouble to compute a trades_per_year explicitly and freeze it into every baseline, and nothing could ever consume it.

It is the channel that matters most on its own, because neither of the others can see the failure it covers: a signal that stops firing while the trades it still takes keep their per-trade edge. Expectancy is unchanged, so the CUSUM stays quiet and the rolling window reads full size, and annual R falls anyway because the opportunity set shrank.

What changed

check_decay takes trade_dates, TriggerContext carries them, run_cycle forwards them, EdgeDecayTrigger passes them through, and the CLI sources them from an optional trade_dates_since(since, params) on the book.

Three choices worth recording:

  • TriggerContext refuses a trade_dates/trade_r length mismatch. Misaligned dates do not fail downstream, they date the wrong trades and yield a rate that is wrong in a direction nothing else in the verdict would reveal.
  • The dates are optional where trade_r is not, and their absence warns rather than refusing. A book that cannot date its trades is still worth monitoring on expectancy. What is not acceptable is the previous state, where supplying R alone turned the channel off with no way to tell that apart from a book that genuinely has no dates.
  • No crucible change was needed. TradeLog.from_arrays has accepted entry_date all along, so this was the seam throwing the dates away rather than a gap in the judge.

A wrong premise, recorded

I assumed a live book at exactly baseline expectancy reads HOLDING, so the frequency test could isolate its channel. At n=300, sigma=1.0, the 200-trade rolling window has SE 0.071 against a 0.10 soft line, so it dips below on roughly 1 seed in 7 from noise alone (2 of the first 12). The test now uses a seed sitting at 192% of baseline, so the isolation is a property of the fixture rather than an assumption.

Verification

399 tests pass, ruff clean. Eight new tests, including the direct regression: undated reports the channel off, dated reports a ratio, and a book whose rate collapses to 20% while per-trade expectancy is untouched goes HOLDING to SLIPPING only once dated.

Merge this before npf#184, which adds the trade_dates_since this consumes. npf CI checks out crucible-stack's default branch.

🤖 Generated with Claude Code

check_decay built its TradeLog from bare floats, TradeLog.from_arrays(trade_r),
while crucible derives the live firing rate from the log's entry_date. So
live_trades_per_year and frequency_ratio came back None on every cycle, for every
book, however carefully the baseline's own rate had been frozen at promotion.

Two of the monitor's three channels ran. The third reported itself off, in a
reason line that read as a fact about the book ("live log carries no entry_date")
when it was a fact about the seam. npf had just gone to some trouble to compute a
trades_per_year explicitly and freeze it into every baseline, and nothing could
ever consume it.

It is the channel that matters most on its own, because neither of the others can
see the failure it covers: a signal that stops firing while the trades it still
takes keep their per-trade edge. Expectancy is unchanged, so the CUSUM stays quiet
and the rolling window reads full size, and annual R falls anyway because the
opportunity set shrank.

check_decay now takes trade_dates, TriggerContext carries them, run_cycle forwards
them, EdgeDecayTrigger passes them through, and the CLI sources them from an
optional trade_dates_since(since, params) on the book.

Three choices worth recording:

  * TriggerContext refuses a trade_dates/trade_r length mismatch. Misaligned dates
    do not fail downstream, they date the wrong trades and yield a rate that is
    wrong in a direction nothing else in the verdict would reveal.
  * The dates are optional where trade_r is not, and their absence warns rather
    than refusing. A book that cannot date its trades is still worth monitoring on
    expectancy. What is not acceptable is the previous state, where supplying R
    alone turned the channel off with no way to tell that apart from a book that
    genuinely has no dates.
  * No crucible change was needed. TradeLog.from_arrays has accepted entry_date all
    along, so this was the seam throwing the dates away rather than a gap in the
    judge.

One test premise of mine was wrong and is recorded rather than quietly fixed: I
assumed a live book at exactly baseline expectancy reads HOLDING, so the frequency
test could isolate its channel. At n=300, sigma=1.0, the 200-trade rolling window
has SE 0.071 against a 0.10 soft line, so it dips below on roughly 1 seed in 7 from
noise alone (2 of the first 12). The test now uses a seed sitting at 192% of
baseline, and the isolation is a property of the fixture rather than an assumption.

399 tests pass, ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mspinola
mspinola merged commit c4c6597 into main Aug 3, 2026
1 check passed
@mspinola
mspinola deleted the claude/edge-decay-firing-rate branch August 3, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant