From 3225a3c054c652f83b1bd18e439c47f06ce4cf6e Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Tue, 27 Oct 2026 00:47:54 +0000 Subject: [PATCH] fix(portfolio): refuse a portfolio with a number that is not finite (#1509) - cash, quantities and average prices reject NaN and infinity when the file loads --- tests/test_portfolio_context.py | 14 ++++++++++++++ tradingagents/portfolio.py | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_portfolio_context.py b/tests/test_portfolio_context.py index 32e0ede6d..feef30b62 100644 --- a/tests/test_portfolio_context.py +++ b/tests/test_portfolio_context.py @@ -96,6 +96,20 @@ def test_a_holding_keeps_the_instrument_it_was_listed_as(): assert "Other positions: WTI 100" in text and "CL=F" not in text +@pytest.mark.unit +@pytest.mark.parametrize("book", [ + '{"cash": NaN}', + '{"positions": [{"ticker": "AAPL", "quantity": Infinity}]}', + '{"positions": [{"ticker": "AAPL", "quantity": 1, "average_price": "nan"}]}', +]) +def test_load_rejects_a_number_that_is_not_finite(tmp_path, book): + # Python's json writes NaN and Infinity, and they would reach the agents as "nan units". + bad = tmp_path / "p.json" + bad.write_text(book) + with pytest.raises(ValueError, match="portfolio"): + load_portfolio(bad) + + @pytest.mark.unit def test_load_reads_a_valid_file(tmp_path): good = tmp_path / "p.json" diff --git a/tradingagents/portfolio.py b/tradingagents/portfolio.py index 1040e8fb1..abbc93bca 100644 --- a/tradingagents/portfolio.py +++ b/tradingagents/portfolio.py @@ -17,18 +17,22 @@ import hashlib import json from pathlib import Path -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel, ConfigDict, Field, ValidationError from tradingagents.dataflows.symbols import normalize_symbol class Position(BaseModel): + model_config = ConfigDict(allow_inf_nan=False) + ticker: str = Field(description="Instrument symbol, e.g. AAPL") quantity: float = Field(description="Signed units held; negative is short") average_price: float | None = Field(default=None, description="Average entry price per unit") class PortfolioContext(BaseModel): + model_config = ConfigDict(allow_inf_nan=False) + cash: float | None = Field(default=None, description="Free cash available") currency: str | None = Field(default=None, description="Currency label for cash and prices") positions: list[Position] = Field(default_factory=list)