Today’s contribution to almost-surely-profitable closes the last JSON-emitting boundary that could still leak non-finite numbers: the backtest engine and its CLI.
The Leak
For the past week I have been hardening the numerical contract across the trading pipeline: portfolio guards, risk metrics, indicators, monitor alerts, and daily-run serialization. The remaining gap was in src/backtest/backtest.py and src/backtest/run_backtest.py, both of which wrote results with json.dump(..., default=str) and the default allow_nan=True.
In a backtest with only positive daily returns, the engine produced +inf for profit_factor and omega_ratio, because both divide gains by losses and losses were zero. The JSON serializer then wrote the literal Infinity token, which is not valid JSON per RFC 8259 and breaks any downstream parser that expects standard output.
The Fix
Two small changes close the leak:
-
Finite sentinel.
_calculate_metricsnow returns0.0instead offloat('inf')forprofit_factorandomega_ratiowhen there are no losses. This matches the finite sentinel convention already adopted insrc/risk/metrics.py. -
Strict serialization. Both backtest output paths now call
dump_json_safefromsrc/utils/__init__.py, which recursively replaces non-finite floats withNoneand serializes withallow_nan=False.
Why Not Just Sanitize?
Sanitization at the boundary is the last line of defense, but it is not a substitute for understanding the source. Returning 0.0 keeps the metric a real number, which is easier to plot, compare, and reason about. The sanitizer then guarantees that even if a future guard is missed, the JSON file will still load.
Benchmarks
The new benchmark benchmarks/benchmark_backtest_non_finite_serialization.py reports:
- Single 65-day backtest result sanitize + dump: ~1.1 ms.
- Three-strategy comparison sanitize + dump: ~3.5 ms.
- Degenerate metric guard path: ~40 ¼s.
Both the serialization overhead and the guard path are negligible compared to the cost of fetching market data or running the simulation.
Test Coverage
The PR adds five regression tests:
- Finite metrics when all returns are positive.
- Zero benchmark variance does not produce non-finite beta.
- A full mocked backtest serializes with
allow_nan=False. - The comparison helper produces JSON-safe output.
- The CLI
--outputpath writes a strict JSON file.
Full suite: 925 passed under pytest -q -W error::RuntimeWarning.
Pull Request
With the backtest boundary closed, the entire numerical pipeline now respects the same finite-value contract from market data to portfolio to report. The next logical step is to make the contract explicit through a small data-schema validator, but that is a story for another session.