mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
fix(agents): keep one unreadable price from discarding the decision
- a price written as a range or a hedge is dropped like any other unusable value - a field the model did not give is named as not provided, rather than omitted
This commit is contained in:
@@ -61,12 +61,13 @@ class TestRenderTraderProposal:
|
||||
assert "**Position Sizing**: 6% of portfolio" in md
|
||||
assert "FINAL TRANSACTION PROPOSAL: **BUY**" in md
|
||||
|
||||
def test_optional_fields_omitted_when_absent(self):
|
||||
def test_optional_fields_are_named_as_not_provided(self):
|
||||
"""An omitted line reads as a field nobody asked for; the reader cannot
|
||||
tell it from a level the trader declined to set."""
|
||||
p = TraderProposal(action=TraderAction.SELL, reasoning="Guidance cut.")
|
||||
md = render_trader_proposal(p)
|
||||
assert "Entry Price" not in md
|
||||
assert "Stop Loss" not in md
|
||||
assert "Position Sizing" not in md
|
||||
for field in ("Entry Price", "Stop Loss", "Position Sizing"):
|
||||
assert f"**{field}**: not provided" in md
|
||||
assert "FINAL TRANSACTION PROPOSAL: **SELL**" in md
|
||||
|
||||
|
||||
@@ -503,3 +504,45 @@ def test_conflict_alone_is_not_a_hold_trigger(source):
|
||||
assert "conflict alone is not a reason to Hold" in text or \
|
||||
"Conflicting arguments alone are not a reason to Hold" in text
|
||||
assert "materially conflicting" not in text
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("written", ["150-160", "150 to 160", "around 150", "150/160", "~150"])
|
||||
def test_a_price_written_as_a_range_drops_only_that_field(written):
|
||||
"""Anything that is not a single number becomes None. Letting it through
|
||||
fails the whole decision's validation, and the run falls back to free text,
|
||||
losing every other field the model got right."""
|
||||
from tradingagents.agents.schemas import PortfolioDecision, PortfolioRating
|
||||
|
||||
decision = PortfolioDecision(rating=PortfolioRating.BUY, executive_summary="s",
|
||||
investment_thesis="t", price_target=written)
|
||||
assert decision.price_target is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_price_that_is_a_number_survives():
|
||||
from tradingagents.agents.schemas import PortfolioDecision, PortfolioRating
|
||||
|
||||
decision = PortfolioDecision(rating=PortfolioRating.BUY, executive_summary="s",
|
||||
investment_thesis="t", price_target="$1,150.25")
|
||||
assert decision.price_target == 1150.25
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_field_the_model_did_not_give_says_so():
|
||||
"""An omitted line and a line never asked for read the same to an analyst."""
|
||||
from tradingagents.agents.schemas import PortfolioDecision, PortfolioRating, render_pm_decision
|
||||
|
||||
rendered = render_pm_decision(PortfolioDecision(
|
||||
rating=PortfolioRating.HOLD, executive_summary="s", investment_thesis="t"))
|
||||
assert "Price Target" in rendered and "not provided" in rendered.lower()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_trader_names_the_levels_it_did_not_give():
|
||||
from tradingagents.agents.schemas import TraderAction, TraderProposal, render_trader_proposal
|
||||
|
||||
rendered = render_trader_proposal(TraderProposal(action=TraderAction.HOLD, reasoning="r"))
|
||||
for field in ("Entry Price", "Stop Loss", "Position Sizing"):
|
||||
assert field in rendered
|
||||
assert rendered.lower().count("not provided") == 3
|
||||
|
||||
@@ -39,7 +39,12 @@ def _coerce_optional_float(value):
|
||||
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.
|
||||
price is reduced to its number.
|
||||
|
||||
Anything that is not a single number is dropped the same way. A range
|
||||
("150-160") or a hedge ("around 150") would otherwise reach pydantic, fail
|
||||
validation, and discard the whole decision, losing every field the model got
|
||||
right along with the price.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
@@ -47,7 +52,10 @@ def _coerce_optional_float(value):
|
||||
if text.lower() in _NULLISH_FLOAT or text.endswith("%"):
|
||||
return None
|
||||
cleaned = text.replace(",", "").lstrip("$€£¥").strip()
|
||||
return cleaned or None
|
||||
try:
|
||||
return float(cleaned)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -192,12 +200,12 @@ def render_trader_proposal(proposal: TraderProposal) -> str:
|
||||
"",
|
||||
f"**Reasoning**: {proposal.reasoning}",
|
||||
]
|
||||
if proposal.entry_price is not None:
|
||||
parts.extend(["", f"**Entry Price**: {proposal.entry_price}"])
|
||||
if proposal.stop_loss is not None:
|
||||
parts.extend(["", f"**Stop Loss**: {proposal.stop_loss}"])
|
||||
if proposal.position_sizing:
|
||||
parts.extend(["", f"**Position Sizing**: {proposal.position_sizing}"])
|
||||
# Named even when absent, so a reader can tell a level the trader chose not
|
||||
# to give from one the schema never asked for.
|
||||
for label, value in (("Entry Price", proposal.entry_price),
|
||||
("Stop Loss", proposal.stop_loss),
|
||||
("Position Sizing", proposal.position_sizing)):
|
||||
parts.extend(["", f"**{label}**: {value if value is not None and value != '' else 'not provided'}"])
|
||||
parts.extend([
|
||||
"",
|
||||
f"FINAL TRANSACTION PROPOSAL: **{proposal.action.value.upper()}**",
|
||||
@@ -272,10 +280,11 @@ def render_pm_decision(decision: PortfolioDecision) -> str:
|
||||
"",
|
||||
f"**Investment Thesis**: {decision.investment_thesis}",
|
||||
]
|
||||
if decision.price_target is not None:
|
||||
parts.extend(["", f"**Price Target**: {decision.price_target}"])
|
||||
if decision.time_horizon:
|
||||
parts.extend(["", f"**Time Horizon**: {decision.time_horizon}"])
|
||||
# Named even when absent: a missing line reads as a field nobody asked for,
|
||||
# so a reader cannot tell "no target" from "target not reported".
|
||||
target = decision.price_target if decision.price_target is not None else "not provided"
|
||||
parts.extend(["", f"**Price Target**: {target}"])
|
||||
parts.extend(["", f"**Time Horizon**: {decision.time_horizon or 'not provided'}"])
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user