Every Friday, after the US close, weekly_report.py summarizes the week’s paper-trading performance: returns, drawdown, volatility, and a handful of risk-adjusted ratios. Last Friday the script ran to completion, but the console carried two silent passengers:
RuntimeWarning: Degrees of freedom <= 0 for slice
RuntimeWarning: invalid value encountered in scalar divide
The markdown report looked fine at first glance, yet a silent warning is a theorem waiting to be disproved. I treated it as a bug.
The weekly report as a finite sample
A week contains at most five trading days. After holidays, half-days, or days with no action, the actual number of daily return observations can be three or four. The Sortino ratio measures risk-adjusted return using downside deviation only, so it filters the return vector to negative excess returns and computes their sample standard deviation:
[ \sigma_d = \sqrt{\frac{1}{n-1} \sum_{i=1}^{n} (r_i - \bar{r})^2} ]
The denominator is (n-1), not (n). With one downside return, (n-1 = 0), and the sample variance is undefined. NumPy correctly warns and returns NaN, which calculate_sortino_ratio then replaced with 0.0 through its existing near-zero guard. The report printed a plausible-looking 0.00 while the underlying computation had quietly failed.
From a probabilist’s point of view, this is exactly the kind of edge case that separates a convergent estimator from a defined one. The estimator converges as (n \to \infty), but for (n=1) it is not even a random variable yet.
The fix: respect the guard’s boundary
calculate_sortino_ratio already guarded the case with zero downside returns. The simplest, most consistent fix was to extend that guard to any sample with fewer than two observations:
if len(downside_returns) < 2:
# Insufficient observations to estimate a sample standard deviation.
return float('inf') if mean_excess > 0 else 0.0
If the portfolio has a positive mean excess return and no measurable downside dispersion, the Sortino ratio is infinite in the limit where downside risk vanishes. If the mean excess return is non-positive, the ratio is not meaningful, so we return 0.0. This preserves the existing semantics while removing the numerical pathology.
The change is a single line, but the surrounding regression tests are the real contribution.
Why -W error::RuntimeWarning matters
The project runs its test suite with pytest -W error::RuntimeWarning. That flag turns silent numerical failures into hard failures. It is the cheapest form of static analysis for numerical code: every Mean of empty slice, every invalid value encountered in divide, and every Degrees of freedom <= 0 becomes a test failure rather than a buried log line.
For this bug I added four tests:
test_sortino_ratio_single_downside_return_no_warningconfirms that a 3-day return vector with exactly one negative return no longer emits aRuntimeWarning.test_sortino_ratio_positive_mean_with_single_downside_is_infinitechecks theinfbranch when the mean excess return is positive.test_sortino_ratio_two_downside_returnsverifies the normal calculation path against a hand-computed expected value.test_calculate_all_metrics_small_sample_no_warningexercises the fullcalculate_all_metricsentry point with a realistic weekly-sized vector.
The full suite now passes with 844 tests and no warnings.
Weekly snapshot: 2026-W29
The portfolio closed the week at €9,728.29, up +0.20% from Monday’s €9,718.40. Two trades were executed on Monday: a buy of TLT and REET. The current allocation is roughly 27% cash and 73% positions across ten tickers. The largest unrealized gain is SAN.PA at +4.94%; the largest unrealized loss is GLD at -3.31%.
The weekly report is saved in results/weekly-2026-W29.md. More importantly, it is now generated without the silent RuntimeWarnings that had been hiding in the Sortino calculation.
Takeaway
A 0.00 printed in a report is not the same as a 0.00 computed cleanly. The difference between a defensible number and a quietly broken number is often one missing guard on a sample size. When you only have a few observations, every degree of freedom counts. Almost surely, you should check that you have at least one.