Summary
Surface signal rejections as first-class citizens in the standard BacktestReport summary and the default plotting helpers, instead of requiring users to dig through BacktestRun.signal_events.
Motivation
The framework already records every signal rejection in signal_events with a reason field (already_in_position, open_short_position, cooldown, insufficient_capital, etc. — see backtest_run.py lines 83 / 653). But:
BacktestReport.pretty_print() doesn't show the counts.
- The standard plot helpers don't render them.
- Users notice rejections only via accidental discovery when they hand-build charts (recent example in
examples/tutorial/notebooks/02_strategy_visualization.ipynb).
Silent rejections are a real UX gap — they make it look like the strategy "isn't firing" when actually it's firing fine but the engine is dropping signals on the floor.
Proposed design
1. New aggregation method on BacktestRun
def get_rejection_summary(self) -> dict[str, int]:
"""Return {reason: count} for all rejected signal events."""
Located in backtest_run.py.
2. Include counts in BacktestReport.pretty_print()
New section:
Signal rejections:
already_in_position : 42
open_short_position : 17
cooldown : 88
insufficient_capital : 3
Total rejected : 150
3. Render suppressed signals in default plotting helpers
In any default chart helper that already plots fills, also plot suppressed signals as grey x-thin-open markers (same pattern as the notebook). Add a legend entry per reason.
Acceptance criteria
Estimated scope
~2 hours. Purely additive.
Release target
v9.1.0 (SemVer MINOR — additive).
Summary
Surface signal rejections as first-class citizens in the standard
BacktestReportsummary and the default plotting helpers, instead of requiring users to dig throughBacktestRun.signal_events.Motivation
The framework already records every signal rejection in
signal_eventswith areasonfield (already_in_position,open_short_position,cooldown,insufficient_capital, etc. — see backtest_run.py lines 83 / 653). But:BacktestReport.pretty_print()doesn't show the counts.examples/tutorial/notebooks/02_strategy_visualization.ipynb).Silent rejections are a real UX gap — they make it look like the strategy "isn't firing" when actually it's firing fine but the engine is dropping signals on the floor.
Proposed design
1. New aggregation method on
BacktestRunLocated in backtest_run.py.
2. Include counts in
BacktestReport.pretty_print()New section:
3. Render suppressed signals in default plotting helpers
In any default chart helper that already plots fills, also plot suppressed signals as grey
x-thin-openmarkers (same pattern as the notebook). Add a legend entry per reason.Acceptance criteria
BacktestRun.get_rejection_summary()exists and returns aggregated counts.BacktestReport.pretty_print()includes a "Signal rejections" section when the count is > 0.get_rejection_summary()covering: empty, single reason, multiple reasons.pretty_print()output.signal_eventsschema or any public API.Estimated scope
~2 hours. Purely additive.
Release target
v9.1.0 (SemVer MINOR — additive).