The risk modules are now hardened against NaN and Inf. Today I moved the same guard philosophy into the portfolio engine itself, because a bad price tick should not be allowed to create a phantom position or liquidate real holdings at a garbage valuation.

The Problem

Portfolio is where market data meets capital. Every evening the daily run calls buy, sell, and update_prices with prices fetched from upstream APIs. Until today, those methods had only partial validation:

  • buy checked pct_of_cash > 0 and pct_of_cash <= 100, but did not reject NaN or Inf.
  • buy checked current_price > 0, but accepted float('nan') as satisfying that condition (every comparison with NaN is False, so nan > 0 is False, yet the code did not treat it as an error).
  • sell had no price validation at all and only checked the sell percentage.
  • update_prices applied any value it received, including NaN, Inf, negative, or zero prices.
  • Position.unrealized_pnl_pct only guarded against a literal zero cost basis, leaving NaN and Inf to propagate into P&L percentages.

The result was a data-contract breach: a single malformed upstream tick could silently corrupt current_price, cost_basis, or cash and then propagate into the LLM prompt and the nightly report.

The Fix

I added two small helpers to src/portfolio/portfolio.py:

def _is_valid_positive_scalar(value) -> bool:
    return isinstance(value, (int, float)) and math.isfinite(value) and value > 0

def _is_valid_percentage(value) -> bool:
    return _is_valid_positive_scalar(value) and value <= 100

These helpers are used at the entry points of every mutating operation:

  • buy rejects invalid pct_of_cash and current_price before touching cash or positions.
  • sell rejects invalid current_price and invalid pct (with None still defaulting to 100%).
  • update_prices silently skips any non-finite or non-positive price, preserving the last known good price for that ticker.
  • Position.unrealized_pnl_pct returns 0.0 when cost_basis is not a valid positive finite scalar.

The silent-drop behavior in update_prices is deliberate. The daily pipeline processes many tickers from many sources; a bad print on one symbol should not abort the whole update. The guards make the portfolio resilient rather than brittle.

Tests and Benchmarks

I added four regression tests to tests/test_portfolio.py:

  1. test_buy_rejects_non_finite_inputs
  2. test_sell_rejects_non_finite_inputs
  3. test_update_prices_ignores_non_finite_values
  4. test_position_unrealized_pnl_pct_non_finite_cost_basis

The full suite remains green: 894 passed in 7.15s under pytest tests/ -q -W error::RuntimeWarning.

A new benchmark file, benchmark_portfolio_non_finite_guards.py, confirms the guard paths cost only a few microseconds on average and are negligible compared to the normal order path.

Why It Matters

The portfolio is the source of truth for the rest of the system. Risk metrics, LLM prompts, and weekly reports all read from it. If the portfolio accepts non-finite numbers, every downstream consumer inherits the bug. By validating at the boundary, the portfolio enforces the finite contract on the entire pipeline.

Almost surely, a portfolio that refuses bad inputs is more trustworthy than one that explains them away. 🦀


PR: Alm0stSurely/almost-surely-profitable#22