A prompt is only as useful as the data it contains. When a trading agent formats market state, portfolio totals, or risk metrics into an LLM prompt, every numeric field is a potential point of failure: a single NaN or inf can raise ValueError and abort the daily run, or leak a misleading nan% token into the model’s context.

Today’s patch hardens the LLM prompt builder in src/llm/trading_agent.py and the risk-metrics summary in src/risk/metrics.py against non-finite values.

The Failure Mode

Python’s f-string formatting is unforgiving with non-finite floats:

>>> f"{float('nan'):.2f}%"
ValueError: cannot convert float NaN to integer

Wait — actually, f"{float('nan'):.2f}" returns 'nan', not an exception. The real problem is subtler: nan propagates into percentages and comparisons. In the cooldown block, a non-finite trades_this_week could render as nan/nan and trigger a false “weekly cap reached” message because any comparison with NaN is false. The LLM then receives an ambiguous signal and may default to a defensive hold.

The Guard Pattern

I added a small _safe_format helper that:

  1. Rejects None, booleans, and non-numeric values.
  2. Checks finiteness with math.isfinite.
  3. Converts the value to float before formatting, so string numerals also work.
  4. Falls back to "n/a" when anything is off.

This helper was applied to every numeric f-string in build_prompt: asset indicators, portfolio totals, position fields, risk metrics, deflated-Sharpe metrics, and cooldown counters. The risk-summary formatter received the same treatment.

Why This Matters Mathematically

Non-finite values are not just formatting bugs — they are information. A NaN in volatility means the estimator failed; an inf in drawdown means a divide-by-zero somewhere upstream. Rendering these as "n/a" makes the failure explicit rather than letting a bogus number influence a stochastic decision process. The Markov property of good prompting: the current context should be well-defined.

Test and Benchmark Notes

The full suite passed with 1066 tests under -W error::RuntimeWarning. New regression tests cover non-finite asset fields, portfolio fields, position fields, cooldown status, and risk-summary fields.

A micro-benchmark shows the guarded prompt builder stays under 70 µs for normal inputs and under 110 µs when every guard is exercised. The absolute overhead is negligible compared to an LLM API call.

PR