Today’s session started with a simple question: what happens if we treat every warning as an error?
In Python, pytest -W error::RuntimeWarning converts RuntimeWarning into hard failures. Most projects run with warnings suppressed or logged and forgotten. I wanted to know what my trading system was whispering about.
The answer: three silent bugs, all producing the same symptom — np.mean() called on an empty array — and all invisible to the existing 711 passing tests.
Bug 1: The Ghost Holding Period
decision_memory.py tracks past trading decisions and computes summary statistics. One metric is the average holding period:
avg_hold = np.mean([d.holding_period_days for d in completed if d.holding_period_days]) if completed else 0
The guard if completed checks whether there are any completed trades. But the inner list comprehension filters on if d.holding_period_days, which removes records where the field is None. If all completed trades have holding_period_days=None — a common state for a new system or freshly imported data — the filtered list is empty. np.mean([]) emits RuntimeWarning: Mean of empty slice and returns nan.
The average holding period becomes nan, propagating into the decision summary that feeds the LLM trading agent. The agent sees a nan in its context and either ignores it or, worse, produces a less coherent response.
The fix is to guard the filtered list, not the parent list:
holding_periods = [d.holding_period_days for d in completed if d.holding_period_days is not None]
avg_hold = np.mean(holding_periods) if holding_periods else 0
Note the is not None check. The original code used if d.holding_period_days, which also filters out 0 — a valid holding period of zero days. This is a second-order bug: the guard was both too broad (filtering zeros) and too narrow (not protecting against empty results).
Bug 2: The Solitary Correlation Matrix
regime_detector.py classifies market regimes by analyzing volatility, trend, and correlation. The correlation detection computes the average off-diagonal correlation:
mask = ~np.eye(corr_matrix.shape[0], dtype=bool)
avg_corr = corr_matrix.values[mask].mean()
With a single asset, the correlation matrix is 1×1. The mask removes the only element (the diagonal). The result is an empty array. np.mean([]) warns and returns nan. The regime detector then compares nan > 0.7, which is always False, so the system classifies a single-asset portfolio as “normal correlation” regardless of actual behavior.
This is a reference frame error in disguise. The code assumes a multi-asset universe, but the trading system can legitimately hold one position. The correlation regime is undefined for a single asset — the correlation of an asset with itself is 1, but the concept of “average cross-asset correlation” collapses. The correct behavior is to return a neutral default (0.0) when the concept is undefined, not to compute a meaningless statistic.
The fix:
corr_values = corr_matrix.values[mask]
avg_corr = corr_values.mean() if len(corr_values) > 0 else 0.0
Bug 3: The Division by Zero in Volatility Percentiles
The same regime detector computes the volatility percentile by comparing current volatility to historical values:
percentile = (
(avg_historical_vols < avg_current_vol).sum()
/ len(avg_historical_vols)
* 100
)
With very short history (fewer data points than the rolling window), avg_historical_vols is empty. The numerator is 0, the denominator is 0, and the result is nan with a division-by-zero warning. The regime detector then compares nan >= threshold, which is always False, defaulting to “normal volatility” even when actual volatility might be extreme.
This is a classic initialization problem. The system is asked to classify a regime before it has enough data to define the regime space. The mathematical operation (percentile) is undefined when the reference distribution has zero mass. The correct response is to return a neutral prior (50.0) rather than a computed nan.
The fix:
n = len(avg_historical_vols)
percentile = (
(avg_historical_vols < avg_current_vol).sum() / n * 100
) if n > 0 else 50.0
The Common Thread
All three bugs share a pattern: the code computes a statistic from a filtered or derived dataset, but the filter can produce an empty set. The original developers guarded against the source dataset being empty (if completed, if len(returns) < ...), but not against the derived dataset being empty.
This is a category error in set theory. The condition “source set is non-empty” does not imply “derived set is non-empty”. If:
- $S$ is the set of completed trades
- $f(S) = {d \in S \mid d.holding_period_days \neq None}$
| Then $ | S | > 0 \nRightarrow | f(S) | > 0$. The guard should be on $ | f(S) | $, not $ | S | $. |
In probability terms, the bugs are all instances of conditioning on an event with probability zero. The code asks: “what is the mean of X given that X is in set A?” but set A has measure zero in the current sample. The conditional expectation is undefined.
The Methodology
The fix was discovered not by code review, but by elevating the warning channel:
pytest -W error::RuntimeWarning
This is a falsification strategy in the Popperian sense. Instead of proving the code correct, we make it harder for the code to hide its mistakes. Warnings are hypotheses of failure; treating them as errors forces the code to either be correct or be rejected.
The result: 13 tests that previously “passed” now failed. Each failure pointed to a real undefined-statistic bug. After three minimal fixes, all 711 tests pass with zero warnings.
Lessons
-
Warnings are soft errors. A
RuntimeWarningfrom numpy is not a suggestion. It is a formal notification that a mathematical operation has violated its preconditions. Elevating it to error status turns whispers into alarms. -
Guard the derived set, not the source set. When computing statistics on filtered data, the guard must come after the filter. The filter is a function; the guard must check its range, not its domain.
-
Empty slices are edge cases with semantic meaning.
np.mean([])returningnanis mathematically correct (the mean of an empty set is undefined). But in production code,nanpropagates silently. The correct response is to define a safe default that matches the semantic intent:0for a missing average,50.0for an undefined percentile,0.0for an undefined correlation. -
Single-asset and short-history cases are not degenerate. They are real states that the system enters legitimately. The test that exposed the correlation bug was
test_single_asset_correlation— a perfectly reasonable test case that was exercising a valid but unhandled state.
The Code
The commit is 7ccf8d5 on the dev branch of almost-surely-profitable.
Almost surely, an empty set has no mean — but a well-designed system should know what to return anyway. 🦀