Commit ·
2ac24b1
1
Parent(s): 98060b8
Fallback: if Strategist output lacks a Current Price row, call get_price_change directly
Browse filesThe previous fallback only worked when the live price happened to
leak into the Chief Strategist's final answer. For NVDA the Strategist
emitted a long narrative without echoing the price through from the
Market Scanner, so the fallback returned None and the user saw 'Failed
to parse'. This version calls the get_price_change tool directly as a
backstop whenever the transcript scan fails — yfinance is on the same
machine so there's no real cost.
- crew/crew.py +65 -26
crew/crew.py
CHANGED
|
@@ -219,45 +219,84 @@ class FinAgentCrew:
|
|
| 219 |
) -> Optional[TradingSignal]:
|
| 220 |
"""Build a TradingSignal directly from tool outputs when LLM parsing fails.
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
From those we derive:
|
| 229 |
* **entry** = live price
|
| 230 |
* **action** = BUY if today's change is ≥ +1 %, SELL if ≤ −1 %,
|
| 231 |
otherwise HOLD
|
| 232 |
-
* **stop / target** = ± 3 % / ± 5 % of entry
|
| 233 |
-
sanity fix)
|
| 234 |
* **confidence** = 50 baseline, + up to 25 scaled by |% change|
|
| 235 |
* **reasoning** = preserve the LLM's narrative (first ~800 chars)
|
| 236 |
|
| 237 |
-
Returns ``None`` only if no live price
|
| 238 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
price_match = re.search(
|
| 240 |
r"Current Price:\s*\$\s*([\d,]+\.?\d*)", raw_output
|
| 241 |
)
|
| 242 |
-
if
|
| 243 |
-
return None
|
| 244 |
-
|
| 245 |
-
try:
|
| 246 |
-
entry = float(price_match.group(1).replace(",", ""))
|
| 247 |
-
except ValueError:
|
| 248 |
-
return None
|
| 249 |
-
|
| 250 |
-
# Percent-change row, e.g. "Change: +$5.90 (+2.05%)"
|
| 251 |
-
pct_match = re.search(
|
| 252 |
-
r"Change:[^()]*\(([+-]?)([\d.]+)%\)", raw_output
|
| 253 |
-
)
|
| 254 |
-
pct_change = 0.0
|
| 255 |
-
if pct_match:
|
| 256 |
-
sign = -1.0 if pct_match.group(1) == "-" else 1.0
|
| 257 |
try:
|
| 258 |
-
|
| 259 |
except ValueError:
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
# Choose action from today's trend.
|
| 263 |
if pct_change >= 1.0:
|
|
|
|
| 219 |
) -> Optional[TradingSignal]:
|
| 220 |
"""Build a TradingSignal directly from tool outputs when LLM parsing fails.
|
| 221 |
|
| 222 |
+
First we try to scan ``raw_output`` for the canonical
|
| 223 |
+
``Current Price: $X.XX`` row emitted by ``tools.market_scanner``
|
| 224 |
+
— that row leaks into the transcript when the agents call
|
| 225 |
+
``get_price_change``. If the Strategist's final answer doesn't
|
| 226 |
+
include it (it often doesn't — the Strategist is the last agent
|
| 227 |
+
and only sees the prior agents' *summaries*), we call
|
| 228 |
+
``get_price_change`` directly as a backstop so we still have a
|
| 229 |
+
live price to ground the card on.
|
| 230 |
+
|
| 231 |
+
From the live price we derive:
|
| 232 |
|
|
|
|
| 233 |
* **entry** = live price
|
| 234 |
* **action** = BUY if today's change is ≥ +1 %, SELL if ≤ −1 %,
|
| 235 |
otherwise HOLD
|
| 236 |
+
* **stop / target** = ± 3 % / ± 5 % of entry
|
|
|
|
| 237 |
* **confidence** = 50 baseline, + up to 25 scaled by |% change|
|
| 238 |
* **reasoning** = preserve the LLM's narrative (first ~800 chars)
|
| 239 |
|
| 240 |
+
Returns ``None`` only if no live price can be retrieved at all.
|
| 241 |
"""
|
| 242 |
+
entry: Optional[float] = None
|
| 243 |
+
pct_change = 0.0
|
| 244 |
+
|
| 245 |
+
# Try the transcript first (cheap — no extra network call).
|
| 246 |
price_match = re.search(
|
| 247 |
r"Current Price:\s*\$\s*([\d,]+\.?\d*)", raw_output
|
| 248 |
)
|
| 249 |
+
if price_match:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
try:
|
| 251 |
+
entry = float(price_match.group(1).replace(",", ""))
|
| 252 |
except ValueError:
|
| 253 |
+
entry = None
|
| 254 |
+
|
| 255 |
+
pct_match = re.search(
|
| 256 |
+
r"Change:[^()]*\(([+-]?)([\d.]+)%\)", raw_output
|
| 257 |
+
)
|
| 258 |
+
if pct_match:
|
| 259 |
+
sign = -1.0 if pct_match.group(1) == "-" else 1.0
|
| 260 |
+
try:
|
| 261 |
+
pct_change = sign * float(pct_match.group(2))
|
| 262 |
+
except ValueError:
|
| 263 |
+
pct_change = 0.0
|
| 264 |
+
|
| 265 |
+
# Backstop: call get_price_change directly if the Strategist's
|
| 266 |
+
# response did not carry the price row through from upstream.
|
| 267 |
+
if entry is None:
|
| 268 |
+
try:
|
| 269 |
+
market_tools = self._tools.get("market_scanner", [])
|
| 270 |
+
price_tool = next(
|
| 271 |
+
(t for t in market_tools
|
| 272 |
+
if getattr(t, "name", "") == "Get Price Change"),
|
| 273 |
+
None,
|
| 274 |
+
)
|
| 275 |
+
if price_tool is None:
|
| 276 |
+
return None
|
| 277 |
+
# crewai wraps tools; the underlying function is .func.
|
| 278 |
+
price_fn = getattr(price_tool, "func", price_tool)
|
| 279 |
+
result = price_fn(ticker)
|
| 280 |
+
p = re.search(
|
| 281 |
+
r"Current Price:\s*\$\s*([\d,]+\.?\d*)", str(result)
|
| 282 |
+
)
|
| 283 |
+
if not p:
|
| 284 |
+
return None
|
| 285 |
+
entry = float(p.group(1).replace(",", ""))
|
| 286 |
+
pct = re.search(
|
| 287 |
+
r"Change:[^()]*\(([+-]?)([\d.]+)%\)", str(result)
|
| 288 |
+
)
|
| 289 |
+
if pct:
|
| 290 |
+
sign = -1.0 if pct.group(1) == "-" else 1.0
|
| 291 |
+
try:
|
| 292 |
+
pct_change = sign * float(pct.group(2))
|
| 293 |
+
except ValueError:
|
| 294 |
+
pct_change = 0.0
|
| 295 |
+
except Exception:
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
if entry is None or entry <= 0:
|
| 299 |
+
return None
|
| 300 |
|
| 301 |
# Choose action from today's trend.
|
| 302 |
if pct_change >= 1.0:
|