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 "**Position Sizing**: 6% of portfolio" in md
|
||||||
assert "FINAL TRANSACTION PROPOSAL: **BUY**" 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.")
|
p = TraderProposal(action=TraderAction.SELL, reasoning="Guidance cut.")
|
||||||
md = render_trader_proposal(p)
|
md = render_trader_proposal(p)
|
||||||
assert "Entry Price" not in md
|
for field in ("Entry Price", "Stop Loss", "Position Sizing"):
|
||||||
assert "Stop Loss" not in md
|
assert f"**{field}**: not provided" in md
|
||||||
assert "Position Sizing" not in md
|
|
||||||
assert "FINAL TRANSACTION PROPOSAL: **SELL**" 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 \
|
assert "conflict alone is not a reason to Hold" in text or \
|
||||||
"Conflicting arguments alone are not a reason to Hold" in text
|
"Conflicting arguments alone are not a reason to Hold" in text
|
||||||
assert "materially conflicting" not 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
|
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
|
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
|
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):
|
if not isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
@@ -47,7 +52,10 @@ def _coerce_optional_float(value):
|
|||||||
if text.lower() in _NULLISH_FLOAT or text.endswith("%"):
|
if text.lower() in _NULLISH_FLOAT or text.endswith("%"):
|
||||||
return None
|
return None
|
||||||
cleaned = text.replace(",", "").lstrip("$€£¥").strip()
|
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}",
|
f"**Reasoning**: {proposal.reasoning}",
|
||||||
]
|
]
|
||||||
if proposal.entry_price is not None:
|
# Named even when absent, so a reader can tell a level the trader chose not
|
||||||
parts.extend(["", f"**Entry Price**: {proposal.entry_price}"])
|
# to give from one the schema never asked for.
|
||||||
if proposal.stop_loss is not None:
|
for label, value in (("Entry Price", proposal.entry_price),
|
||||||
parts.extend(["", f"**Stop Loss**: {proposal.stop_loss}"])
|
("Stop Loss", proposal.stop_loss),
|
||||||
if proposal.position_sizing:
|
("Position Sizing", proposal.position_sizing)):
|
||||||
parts.extend(["", f"**Position Sizing**: {proposal.position_sizing}"])
|
parts.extend(["", f"**{label}**: {value if value is not None and value != '' else 'not provided'}"])
|
||||||
parts.extend([
|
parts.extend([
|
||||||
"",
|
"",
|
||||||
f"FINAL TRANSACTION PROPOSAL: **{proposal.action.value.upper()}**",
|
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}",
|
f"**Investment Thesis**: {decision.investment_thesis}",
|
||||||
]
|
]
|
||||||
if decision.price_target is not None:
|
# Named even when absent: a missing line reads as a field nobody asked for,
|
||||||
parts.extend(["", f"**Price Target**: {decision.price_target}"])
|
# so a reader cannot tell "no target" from "target not reported".
|
||||||
if decision.time_horizon:
|
target = decision.price_target if decision.price_target is not None else "not provided"
|
||||||
parts.extend(["", f"**Time Horizon**: {decision.time_horizon}"])
|
parts.extend(["", f"**Price Target**: {target}"])
|
||||||
|
parts.extend(["", f"**Time Horizon**: {decision.time_horizon or 'not provided'}"])
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user