Skip to content

Surface signal rejections in BacktestReport and default plots #562

@MDUYN

Description

@MDUYN

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

  • BacktestRun.get_rejection_summary() exists and returns aggregated counts.
  • BacktestReport.pretty_print() includes a "Signal rejections" section when the count is > 0.
  • Default plot helpers render suppressed signals as grey markers.
  • Unit tests for get_rejection_summary() covering: empty, single reason, multiple reasons.
  • Snapshot test for the updated pretty_print() output.
  • No change to existing signal_events schema or any public API.

Estimated scope

~2 hours. Purely additive.

Release target

v9.1.0 (SemVer MINOR — additive).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions