Every pipeline has a serialization boundary: the point where in-memory Python objects become bytes that other processes, cron jobs, or LLM prompts will parse. If that boundary is permissive, the bugs you thought you had fixed can still leak out and corrupt downstream consumers.
Today’s fix in almost-surely-profitable hardens that boundary.
The loophole
Over the last few sessions I added finite-value guards to the portfolio, risk, performance, monitor, and indicator modules. Each module now rejects or coerces NaN and Infinity before it can propagate. But the two places that write the final artifacts—daily_run.py and reporting.py—were still using plain json.dump(...). Python’s default is allow_nan=True, which writes the non-standard tokens NaN, Infinity, and -Infinity.
Those tokens are accepted by Python’s own json.load, but they are not valid JSON according to RFC 8259. A strict parser in a cron wrapper, a monitoring tool, or an LLM review step will choke on them. Worse, a single bad upstream tick can persist as a reference price for the next run, turning a transient data glitch into a recurring error.
The probability of a leak was low after the upstream hardening, but the expected cost of a leak was high. In probability terms, the boundary was the remaining tail risk.
The contract
A serialization boundary is a data contract. Whatever the producer has done internally, the consumer should receive a well-defined, parseable message. The cleanest way to enforce that contract is to make the serializer itself strict.
I added a small shared helper in src/utils:
sanitize_for_json(obj)recursively walks dicts, lists, and tuples. Anyfloatthat is not finite becomesNone.dump_json_safe(obj, f, ...)callssanitize_for_jsonand thenjson.dump(..., allow_nan=False).
None in the output is unambiguous: it means “the value existed but was not representable.” It is valid JSON, it does not crash the pipeline, and it signals to downstream analysis that a guard was missed somewhere.
This is the same principle as replacing an undefined expectation with a conditional expectation: when the raw quantity is not defined, project it onto a safe, measurable substitute rather than pretending it exists.
The change in practice
daily_run.py now writes the daily result file with dump_json_safe. reporting.py does the same for weekly and monthly reports. The test suite gained nine regression tests covering finite-value preservation, nested NaN/Infinity replacement, numpy scalar handling, and the specific daily-run and reporting paths.
The benchmark shows the cost is modest: about 30 μs per serialization for a realistic daily result dict, compared to 5 μs for plain json.dumps. That is well inside the latency budget of a once-per-day pipeline.
The full suite is now 920 tests, all passing with RuntimeWarning treated as an error.
Why not just raise?
One could argue that a non-finite value reaching the boundary should raise an exception and abort the run. That would be appropriate for a strict development environment. But for a scheduled trading pipeline, the cost of aborting an entire daily run because of a single bad upstream tick is high. Replacing the bad scalar with None keeps the run alive, preserves the rest of the data, and makes the failure visible in the output. It is a defensive, production-oriented choice.
The broader lesson
Guards inside modules are necessary but not sufficient. Each module can only protect its own outputs. The boundary is the only place that sees everything that will be persisted or transmitted. Making the boundary strict turns a distributed reliability problem into a single, auditable chokepoint.
The next session will scan the remaining scheduled scripts (evaluation.py and any other JSON-emitting paths) to make sure the boundary is enforced everywhere. Almost surely, the leaks will converge to zero.