The zero is the bug too: four silent zeros in one codebase
The biggest number in a result set is the one you check first. The zeros are not, and in one research codebase four of them were wrong in four different ways — a counter that could not distinguish 'declined to trade' from 'crashed', a missing volume field that deleted the highest-paying venue from two strategies, a getattr default that reported the benchmark as uncapped while it was capped at every tier, and a flag that latched before the position filled. Every one of them read as a legitimate result.
Why zeros are harder than outliers
An implausibly large number announces itself. It breaks a chart axis, it fails a sanity check, somebody asks about it. A zero does not. A zero looks like an answer, and it is frequently the correct answer, so it earns a kind of trust that a big number never gets.
Over one research codebase — a funding-carry study across 88 venue-token pairs with a sealed out-of-sample split — I found four zeros that were not answers. They failed in four distinct ways, and every one of them survived review because zero is the least suspicious value a number can take.
Zero #1: a counter that could not tell “declined” from “crashed”
Two strategies returned exactly 0.00% out of sample. Zero trades, zero cost, zero fills. The rejection counters on the result object told me why they didn’t trade:
n_rejected_capacity = 0
n_rejected_no_predicted_rate = 0
n_rotations = 0
pct_periods_deployed = 0.0Nothing was rejected. So nothing was ever offered. But both counters only increment for a candidate that has already become a target — the scoring loop that decides whether a candidate is worth proposing sits upstream, and it increments nothing at all. A strategy that scored a hundred thousand candidates and liked none of them produces a result byte-identical to one that crashed before the loop ran.
I spent real time treating this as a bug. It was not. Replaying the scoring over the real decision calendar:
candidate-periods scored 104,910
candidates in universe 30
cleared the entry bar 0
best score anywhere 1.31 bps
fitted entry bar 2.00 bpsThe strategy worked perfectly and declined to trade for 146 days, because the threshold fitted in-sample was never reachable out of sample. That is a finding — arguably the most interesting one in the study — and the instrumentation rendered it indistinguishable from a failure.
The fix was not a bug fix. It was making a legitimate result able to say so about itself: a score ledger that records candidates offered, scored, and clearing the bar, and a report line that reads DECLINED to trade (nothing cleared the bar) versus EMPTY UNIVERSE — investigate, this is not a result.
Zero #2: a missing field that deleted a venue
This one was a real bug, and expensive. Position sizing is capped by what the book can absorb:
if not np.isfinite(depth50) or not np.isfinite(adv):
return 0.0The reasoning is sound — unknown liquidity is not infinite liquidity, so refuse to size. But adv (average daily volume) came from a ticker stream, and one venue in the study publishes no ticker channel at all. Every one of its 10,008 context rows carried adv_usd = NaN.
So every candidate on that venue was sized to zero and dropped. Silently. Its order books were present and perfectly good; only the volume was missing, and the guard conflated “we have no book” with “we have no volume”.
The cost: that venue paid the highest funding in the entire universe. Its top candidate ranked +10.13%/yr in-sample — the single best candidate in its structure — and could never be sized. Two of four strategies had been running against a universe with their best instrument quietly removed, and no artefact anywhere said so.
The fix was to recover volume from a different source and validate it rather than assume it. Volume per sampled hour × 24, calibrated against the 30 pairs that have both sources — median ratio 1.339, 90% within 2× either way — and checked against the one pair that happens to hold a full 24 hours of a single day, where it matched the ticker to 0.3%. Each row now carries an adv_source field so a derived number can never be mistaken for a measured one.
Sanity after the fix was the reassuring part: the repaired venue’s HYPE capacity came out at $343,520 against a competing venue’s $355,070 for the same token, and its BTC at $1.65M against Binance’s $11.07M — about 15%, which is what you would expect.
Zero #3: a getattr default that lied about the benchmark
The benchmark — buy BTC spot, short the perpetual, never rotate — returned +4.22%/yr at $100k and +0.17%/yr at $100M. Something was obviously capping it. The report said otherwise:
"n_size_capped_by_execution": getattr(strat, "n_size_capped", 0)The benchmark strategy class did not define n_size_capped. So the default fired, every tier reported 0, and the single most important fact about the benchmark was published as its opposite. It was capped at every tier above $10M — requesting $99.0M of notional and receiving $12.3M, with 87% of capital never deployed. That one number explains the entire decay from 4.22% to 0.17%, and it was reported as “not capped”.
getattr(obj, name, default) is the hazard. It converts “this object does not implement that concept” into a plausible measurement, with no error and no warning, at exactly the moment you most want to know the difference.
Zero #4: a flag that latched before the fill
The same benchmark returned exactly $0 at leverage 1.0 and 1.5, while working normally at 2.0 and above. Lower leverage means a smaller position, so it should be the easiest case to fill.
Two bugs, stacked:
It never checked its gross-leverage budget. Both legs of a delta-neutral position count toward gross notional, so the rotating strategies capped size at headroom / 2. The benchmark did not. At $10M and 1.0× it proposed $9.9M per leg — $19.8M gross against a $10M ceiling — which the portfolio had no choice but to refuse.
And the “already opened” flag was set at proposal time, not at fill time. So one refusal disabled the benchmark permanently. The comment said the flag existed to prevent re-entry after a forced close, which is a genuinely different condition — the code conflated proposed with held.
After the fix that cell went from $0 and zero rotations to $45,638 and one rotation, deployed at $5.0M per leg — exactly 1.0× gross on $10M of equity across two legs. Correct, and checkable by hand.
The pattern
Four zeros, four mechanisms, one shape. In every case a value that meant “unknown”, “not applicable” or “never evaluated” was rendered as the number 0, which the consuming code read as a measurement.
NaN volume -> 0 capacity -> venue deleted
missing attribute -> 0 cap events -> benchmark reported uncapped
nothing proposed -> 0 rejections -> declined reads as crashed
refused once -> 0 return forever -> two cells silently deadThe defences are unglamorous and they work. Keep “we did not measure this” distinguishable from “we measured this and it is zero” all the way to the report, rather than collapsing them at the first convenient boundary. Count what you considered, not only what you rejected — a pipeline that records rejections but not offers cannot explain an empty result. Treat getattr defaults and or 0 as data loss. And put the provenance of every derived number next to the number.
The biggest number in a result set is always worth a second look. So is the zero — and nobody ever gives it one.