Repository navigation
rules backtest: word an empty-side profit factor as insights does (#988) - #998
Conversation
The `keel rules backtest` line printed the engine's profit_factor raw: `Infinity` for all wins and a measured-looking `0` for all scratches or no closed trade. It now prints `undefined_reading` over the reason #986 gave the track record -- `n/a (no losses yet)`, `n/a (no wins or losses yet)`, `n/a (no trades yet)` -- through one shared `insights.profit_factor_reason_of(stats)`, whose sides are decided by counting trades like `build_rule_track_record`. An all-losses 0 is a measurement and keeps its number. The returned BacktestResult and the gates are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
left a comment
There was a problem hiding this comment.
Round 1 review (#988).
Checked: the helper reuses _empty_side, _side_average and undefined_reading, and adds no wording of its own. RuleTrackRecord.profit_factor_reason now goes through the same _profit_factor_reason, so it cannot drift. Open trades are fine, because _side_average counts only win/loss outcomes and n_trades counts only closed trades. A win with pnl 0 reads n/a (no losses yet) over an engine 0, which matches insights. backtest_resolved always returns summarize output, so a hand-built BacktestResult (trades=[], n>0, which would read as no wins or losses yet) cannot reach this line. Mutation check: reverting the f-string to {stats.profit_factor} fails 3 of the 5 parametrized cases (all-wins, all-scratch, no-trades). Other surfaces: the web Rules page reads record.profit_factor_reason, the strategy console renders this same line, and research, trials and walk-forward print no profit factor.
[minor] Outside this diff: the simulate report's edge table (keel/sim/report.py:1008, _r_cell) still prints inf for an all-wins profit_factor_r and 0.000 for an all-scratch R sample. _r_cell's docstring says inf is deliberate, and the field is in R rather than money, so this is follow-up material and does not block this PR. It is the last surface that prints the engine's empty-side convention raw.
…ves the line The rules backtest line prints bare n/a for win_rate, profit_factor, expectancy_px and max_drawdown_px when no trade closed, as insights summary does. The parity test now runs all-wins, all-scratch and a zero-pnl win through the real line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
left a comment
There was a problem hiding this comment.
Round 2. Both round-1 fixes are real. At n=0 the line prints bare n/a for win_rate, profit_factor, expectancy_px and max_drawdown_px, matching render_summary. The parity test now drives _backtest_line_over. I ran six mutants on a scratch copy and each one failed at least one new test: raw profit factor, no n=0 guard, deciding sides by avg or None instead of counting, no n=0 guard on win_rate, no n=0 guard on max_drawdown_px, and dropping the no-wins filter. The tests assert parsed fields, not substrings.
[minor] The PR body is stale after 52f0f99:
- The table's "no closed trade" row still says the line now prints
profit_factor=n/a (no trades yet). It prints bareprofit_factor=n/a. - The body never says that win_rate, expectancy_px and max_drawdown_px also become
n/aat n=0. That is a visible change to the line. - The Tests section lists five cases. It omits the zero-pnl-win case and the separate n=0 every-figure test.
- It still describes the parity test as comparing the backtest reason with the record's reason. It now compares the rendered line.
The reviewer and the squash commit read this body, so please update it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
left a comment
There was a problem hiding this comment.
Round 3: no findings
Closes #988
What
keel rules backtestused to printstats.profit_factorraw. That meantInfinityfor an all-wins sample, and a0that looks measured for an all-scratch sample (0/0) or for a sample with no closed trade. Since #986,keel insights summaryand the Rules page word these cases instead. The backtest line now uses the same words:Infinity/0profit_factor=Infinity/0profit_factor=n/a (no losses yet)0profit_factor=0profit_factor=n/a (no wins or losses yet)0win_rate=0.00% ... profit_factor=0 expectancy_px=0 max_drawdown_px=0win_rate=n/a ... profit_factor=n/a expectancy_px=n/a max_drawdown_px=n/a(baren/afor every figure, asrender_summaryprints at n=0)0profit_factor=0profit_factor=0(a measurement, unchanged)How
keel/commands/insights.py:_profit_factor_reason(n_trades, avg_win, avg_loss)is now the single rule. It finds the empty side and drops "no wins yet", because an all-losses 0 is measured.RuleTrackRecord.profit_factor_reasoncalls it, and the newprofit_factor_reason_of(stats)applies it to aBacktestResult. Each side is decided by counting trades through_side_average, asbuild_rule_track_recorddoes.keel/commands/rules.py: at n=0 every unmeasured figure prints baren/a. Otherwise the line printsundefined_reading(reason)when there is a reason, and the engine's own value when there isn't.This changes the display only.
BacktestResult, the gates and the console's returned stats are unchanged, and tests pin that.rules backtesthas no--json.Tests (TDD)
All new tests are in
tests/commands/test_rules_services.py. They drive the realrun_rule_backtestand replace only the engine run:undefined_reading(RuleTrackRecord.profit_factor_reason)for the same stats.Six mutations were each killed by these tests: raw line, no n=0 guard, zero-average sides, no win_rate guard, no px guard, and no no-wins filter.
Follow-up filed: #1000. The
keel simulateedge table's R profit factor still printsinf/0.000; its docstring calls that deliberate, so it was left out of scope.🤖 Generated with Claude Code