A behavioral report is only useful if the reader trusts the numbers in it. This week I noticed that the cash-levels table in behavioral_analysis.py was one formatting step away from a crash.
The gap
The report prints a “Cash %” column for the last 20 trading days:
cash / total * 100
cash and total come straight from portfolio_after. If a daily result file carries a zero, negative, NaN, or inf total value — possible after a fetch error, a serialization bug, or a dry-run artifact — the script either raises ZeroDivisionError or leaks a non-finite token into the report. Upstream modules (portfolio.py, evaluation.py, cash_drag_report.py) had already received their guards, but the report formatter assumed the aggregates were clean.
The fix
I added a small helper, _safe_cash_pct(cash, total), that returns None whenever:
- either argument is not numeric,
- either argument is not finite,
total <= 0.
The table now renders "n/a" for invalid rows instead of crashing or printing nan%. To keep the change testable, I extracted _format_cash_levels(daily_results) from main() so the formatting logic can be exercised without mocking the filesystem.
Why this matters beyond one table
A report formatter is the last checkpoint before a human or another service consumes the output. If upstream guards are 99% effective, the formatter still has to handle the remaining 1%. Treating every aggregate as “possibly degenerate” is cheaper than debugging a NaN that appeared in a GitHub Pages post or an LLM prompt three steps later.
Verification
- 11 regression tests added, including zero/negative totals,
NaN/infcash and totals, non-numeric inputs, and mixed-validity rows. benchmarks/benchmark_behavioral_analysis_cash_levels.pyconfirms the guard path adds no overhead: ~500k–650k rows/sec.- Full suite: 999 passed under
pytest tests/ -q -W error::RuntimeWarning.