diff --git a/tradingagents/agents/schemas.py b/tradingagents/agents/schemas.py index 961cde79a..e933017d7 100644 --- a/tradingagents/agents/schemas.py +++ b/tradingagents/agents/schemas.py @@ -31,9 +31,23 @@ _NULLISH_FLOAT = {"", "none", "n/a", "na", "null", "nil", "-", "tbd", "unknown"} def _coerce_optional_float(value): - if isinstance(value, str) and value.strip().lower() in _NULLISH_FLOAT: + """Normalise an LLM-written optional numeric field before validation. + + Three shapes show up in practice: a placeholder string ("None", "N/A") in + place of an omitted value (#1058); a percentage where a price was asked for + ("15%", #1288); and a human-formatted price ("$1,234.50"). A percentage + cannot be salvaged into an absolute level -- reading "15%" as 15 would put a + stop at $15 on a $600 stock -- so it is dropped like a placeholder, leaving + one bad field to null out instead of failing the whole proposal. A formatted + price is reduced to its number. Anything else passes through to pydantic. + """ + if not isinstance(value, str): + return value + text = value.strip() + if text.lower() in _NULLISH_FLOAT or text.endswith("%"): return None - return value + cleaned = text.replace(",", "").lstrip("$€£¥").strip() + return cleaned or None # --------------------------------------------------------------------------- @@ -140,11 +154,19 @@ class TraderProposal(BaseModel): ) entry_price: float | None = Field( default=None, - description="Optional entry price target in the instrument's quote currency.", + description=( + "Optional entry price target as an absolute number in the instrument's " + "quote currency (e.g. 189.5), never a percentage or a range. Omit it " + "if you cannot state a specific level." + ), ) stop_loss: float | None = Field( default=None, - description="Optional stop-loss price in the instrument's quote currency.", + description=( + "Optional stop-loss as an absolute price in the instrument's quote " + "currency (e.g. 172.0), never a percentage. Convert a percentage " + "distance to the price level it implies, or omit it." + ), ) position_sizing: str | None = Field( default=None, diff --git a/tradingagents/agents/trader/trader.py b/tradingagents/agents/trader/trader.py index 402023bf1..dbf1f942e 100644 --- a/tradingagents/agents/trader/trader.py +++ b/tradingagents/agents/trader/trader.py @@ -50,6 +50,13 @@ def create_trader(llm): "You are a trading agent analyzing market data to make investment decisions. " "Based on your analysis, provide a specific recommendation to buy, sell, or hold. " + grounding + # Entry/stop are numeric price fields. Asking for concrete + # levels invites a percentage ("15%"), which is not a price + # and fails the structured parse (#1288). + + "State entry price and stop-loss as absolute price levels in the " + "instrument's quote currency (for example 189.5), never a percentage " + "or a range; convert a percentage distance to the price level it " + "implies, or omit the field if you cannot state a number. " + NO_EXTERNAL_TOOLS + get_language_instruction() ),