fix(agents): require absolute price levels from the Trader

- asking the Trader for concrete entry/stop levels invited a percentage
  (stop_loss: '15%'), which is not a price and failed the whole structured
  parse, dropping the run to a free-text retry
- state the requirement in the prompt and in both field descriptions
- a percentage now nulls that field instead of failing the proposal; it is
  never salvaged, since 15% must not become a 15 stop. Human-formatted
  prices with a currency symbol or thousands separator parse #1288
This commit is contained in:
Yijia-Xiao
2026-09-07 20:54:19 +00:00
parent 96111aa368
commit 1c44dd1ffc
2 changed files with 33 additions and 4 deletions

View File

@@ -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,

View File

@@ -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()
),