emmanuelakbi commited on
Commit
0ae549a
·
1 Parent(s): 0d2f0f9

fix(signals): strip <think> blocks before parsing, soften fallback copy

Browse files
Files changed (2) hide show
  1. crew/crew.py +17 -15
  2. 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 — the fallback path runs precisely
532
- # when the LLM's final output is unparseable (often because Qwen
533
- # dumped raw <think>…</think> reasoning instead of the required
534
- # structured format). Dropping that noise onto the UI card would
535
- # look unprofessional, so we synthesise a concise four-line
536
- # rationale from the live data we already have.
537
  if action == Action.BUY:
538
  rationale = (
539
- f"Price up {pct_change:+.2f}% vs previous close "
540
- f"— short-term momentum favours a long entry."
541
  )
542
  elif action == Action.SELL:
543
  rationale = (
544
- f"Price down {pct_change:+.2f}% vs previous close "
545
- f"— short-term momentum favours a short entry."
546
  )
547
  else:
548
  rationale = (
549
- f"Price flat ({pct_change:+.2f}% vs previous close) "
550
- f" no directional conviction; HOLD and wait for a cleaner setup."
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": "Deterministic fallback — LLM output was unparseable.",
 
 
 
564
  "Technical": (
565
- "Entry anchored to live yfinance quote; stop/target derived "
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
- signal = self._parse_primary(raw_output, ticker)
 
 
 
 
 
 
 
 
 
 
 
65
  if signal is None:
66
- signal = self._parse_fallback(raw_output, ticker)
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)