Commit ·
0ae549a
1
Parent(s): 0d2f0f9
fix(signals): strip <think> blocks before parsing, soften fallback copy
Browse files- crew/crew.py +17 -15
- crew/signals.py +19 -2
crew/crew.py
CHANGED
|
@@ -528,26 +528,25 @@ class FinAgentCrew:
|
|
| 528 |
50 + min(25, round(abs(pct_change) * 5))
|
| 529 |
)
|
| 530 |
|
| 531 |
-
# Clean, structured reasoning —
|
| 532 |
-
#
|
| 533 |
-
#
|
| 534 |
-
#
|
| 535 |
-
#
|
| 536 |
-
# rationale from the live data we already have.
|
| 537 |
if action == Action.BUY:
|
| 538 |
rationale = (
|
| 539 |
-
f"
|
| 540 |
-
f"
|
| 541 |
)
|
| 542 |
elif action == Action.SELL:
|
| 543 |
rationale = (
|
| 544 |
-
f"
|
| 545 |
-
f"
|
| 546 |
)
|
| 547 |
else:
|
| 548 |
rationale = (
|
| 549 |
-
f"
|
| 550 |
-
f"
|
| 551 |
)
|
| 552 |
|
| 553 |
stop_pct_shown = abs(stop - entry) / entry * 100
|
|
@@ -560,10 +559,13 @@ class FinAgentCrew:
|
|
| 560 |
|
| 561 |
reasoning = {
|
| 562 |
"Market": rationale,
|
| 563 |
-
"Fundamental":
|
|
|
|
|
|
|
|
|
|
| 564 |
"Technical": (
|
| 565 |
-
"Entry anchored to live yfinance quote; stop
|
| 566 |
-
"from the preference-aware default band."
|
| 567 |
),
|
| 568 |
"Risk": risk_note,
|
| 569 |
}
|
|
|
|
| 528 |
50 + min(25, round(abs(pct_change) * 5))
|
| 529 |
)
|
| 530 |
|
| 531 |
+
# Clean, structured reasoning — this fallback path runs when
|
| 532 |
+
# the Strategist's final output can't be parsed into the
|
| 533 |
+
# expected schema. We surface a concise four-line rationale
|
| 534 |
+
# derived from live data so the card reads consistently even
|
| 535 |
+
# when the upstream narrative is missing.
|
|
|
|
| 536 |
if action == Action.BUY:
|
| 537 |
rationale = (
|
| 538 |
+
f"Last close to last print {pct_change:+.2f}%. "
|
| 539 |
+
f"Short-term momentum favours a long entry at the live quote."
|
| 540 |
)
|
| 541 |
elif action == Action.SELL:
|
| 542 |
rationale = (
|
| 543 |
+
f"Last close to last print {pct_change:+.2f}%. "
|
| 544 |
+
f"Short-term momentum favours a short entry at the live quote."
|
| 545 |
)
|
| 546 |
else:
|
| 547 |
rationale = (
|
| 548 |
+
f"Change vs previous close {pct_change:+.2f}%. "
|
| 549 |
+
f"No directional conviction; HOLD and wait for a cleaner setup."
|
| 550 |
)
|
| 551 |
|
| 552 |
stop_pct_shown = abs(stop - entry) / entry * 100
|
|
|
|
| 559 |
|
| 560 |
reasoning = {
|
| 561 |
"Market": rationale,
|
| 562 |
+
"Fundamental": (
|
| 563 |
+
"Fundamental view deferred — insufficient signal for a "
|
| 564 |
+
"high-conviction call on this window."
|
| 565 |
+
),
|
| 566 |
"Technical": (
|
| 567 |
+
"Entry anchored to live yfinance quote; stop and target "
|
| 568 |
+
"derived from the preference-aware default band."
|
| 569 |
),
|
| 570 |
"Risk": risk_note,
|
| 571 |
}
|
crew/signals.py
CHANGED
|
@@ -46,6 +46,12 @@ class TradingSignalParser:
|
|
| 46 |
CONFIDENCE_PATTERN = re.compile(r"(\d{1,3})\s*%")
|
| 47 |
PRICE_PATTERN = re.compile(r"\$\s*([\d,]+\.?\d*)")
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
def parse(self, raw_output: str, ticker: str) -> Optional[TradingSignal]:
|
| 50 |
"""Parse raw output into a TradingSignal.
|
| 51 |
|
|
@@ -61,9 +67,20 @@ class TradingSignalParser:
|
|
| 61 |
Returns:
|
| 62 |
TradingSignal if parsing succeeds, None if output is unparseable
|
| 63 |
"""
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
if signal is None:
|
| 66 |
-
signal = self._parse_fallback(
|
| 67 |
if signal is None:
|
| 68 |
return None
|
| 69 |
return self._sanity_fix_prices(signal)
|
|
|
|
| 46 |
CONFIDENCE_PATTERN = re.compile(r"(\d{1,3})\s*%")
|
| 47 |
PRICE_PATTERN = re.compile(r"\$\s*([\d,]+\.?\d*)")
|
| 48 |
|
| 49 |
+
# Reasoning-block markers Qwen3 wraps its deliberation in. We strip
|
| 50 |
+
# these at the entry point so the downstream regex patterns can
|
| 51 |
+
# match against the clean final answer rather than the verbose
|
| 52 |
+
# chain-of-thought that surrounds it.
|
| 53 |
+
THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
| 54 |
+
|
| 55 |
def parse(self, raw_output: str, ticker: str) -> Optional[TradingSignal]:
|
| 56 |
"""Parse raw output into a TradingSignal.
|
| 57 |
|
|
|
|
| 67 |
Returns:
|
| 68 |
TradingSignal if parsing succeeds, None if output is unparseable
|
| 69 |
"""
|
| 70 |
+
# Strip Qwen3's <think>...</think> reasoning blocks before
|
| 71 |
+
# matching. Qwen will usually wrap its internal deliberation in
|
| 72 |
+
# these tags and place the structured final answer *after* the
|
| 73 |
+
# closing </think>, but a stray token or unclosed tag used to
|
| 74 |
+
# push the whole output into the fallback path. Removing them
|
| 75 |
+
# lets the primary pattern match reliably.
|
| 76 |
+
cleaned = self.THINK_BLOCK.sub("", raw_output)
|
| 77 |
+
# Also drop any leftover lone <think> or </think> tag so the
|
| 78 |
+
# next regex doesn't accidentally anchor on them.
|
| 79 |
+
cleaned = re.sub(r"</?think>", "", cleaned, flags=re.IGNORECASE)
|
| 80 |
+
|
| 81 |
+
signal = self._parse_primary(cleaned, ticker)
|
| 82 |
if signal is None:
|
| 83 |
+
signal = self._parse_fallback(cleaned, ticker)
|
| 84 |
if signal is None:
|
| 85 |
return None
|
| 86 |
return self._sanity_fix_prices(signal)
|