emmanuelakbi commited on
Commit
eb4ef03
·
1 Parent(s): 1d4dd5c

Add deterministic sanity-fix for Strategist's runaway prices

Browse files

The 14B Strategist occasionally emits wild numbers at synthesis time
(e.g. a $50 000 target on a $290 stock, or stop on the wrong side of
entry). Rather than surface those to the user, the parser now runs
_sanity_fix_prices after extracting the fields: if a stop/target is
more than 20 percent away from the entry or sits on the wrong side of
entry for the BUY/SELL direction, replace it with a conservative
default derived arithmetically from the entry (±3 percent for stop,
±5 percent for target). Valid prices pass through untouched.

Files changed (1) hide show
  1. crew/signals.py +82 -2
crew/signals.py CHANGED
@@ -50,6 +50,9 @@ class TradingSignalParser:
50
  """Parse raw output into a TradingSignal.
51
 
52
  Attempts primary pattern first, falls back to heuristic extraction.
 
 
 
53
 
54
  Args:
55
  raw_output: Raw text from Chief Strategist agent
@@ -59,9 +62,86 @@ class TradingSignalParser:
59
  TradingSignal if parsing succeeds, None if output is unparseable
60
  """
61
  signal = self._parse_primary(raw_output, ticker)
62
- if signal is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  return signal
64
- return self._parse_fallback(raw_output, ticker)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  def _parse_primary(self, raw_output: str, ticker: str) -> Optional[TradingSignal]:
67
  """Attempt to parse using the primary structured format."""
 
50
  """Parse raw output into a TradingSignal.
51
 
52
  Attempts primary pattern first, falls back to heuristic extraction.
53
+ After parsing, prices are sanity-checked so the Strategist can't
54
+ ship obviously-wrong numbers (e.g. $50 000 target on a $290 stock,
55
+ or stop-loss on the wrong side of entry for the given action).
56
 
57
  Args:
58
  raw_output: Raw text from Chief Strategist agent
 
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)
70
+
71
+ @staticmethod
72
+ def _sanity_fix_prices(signal: TradingSignal) -> TradingSignal:
73
+ """Clamp runaway prices and enforce BUY/SELL inequality semantics.
74
+
75
+ Small LLMs occasionally emit obvious absurdities at synthesis time
76
+ (e.g. target that is 100× entry, or stop_loss on the wrong side
77
+ of entry for a BUY). Rather than surface nonsense to the user,
78
+ detect these and replace with conservative defaults derived from
79
+ the entry price:
80
+
81
+ * For BUY: stop = entry × 0.97, target = entry × 1.05
82
+ * For SELL: stop = entry × 1.03, target = entry × 0.95
83
+ * For HOLD: defaults left as-is.
84
+
85
+ We only override a field when it fails a plausibility test
86
+ (>20 % away from entry, or on the wrong side of entry for the
87
+ current action). Valid prices from the model are preserved.
88
+ """
89
+ entry = signal.entry_price
90
+ # No entry price → nothing to clamp against.
91
+ if entry is None or entry <= 0:
92
  return signal
93
+
94
+ stop = signal.stop_loss
95
+ target = signal.target_price
96
+ action = signal.action
97
+
98
+ def _out_of_band(value: Optional[float]) -> bool:
99
+ """True if ``value`` is missing, non-positive, or >20 % from entry."""
100
+ if value is None or value <= 0:
101
+ return True
102
+ return abs(value - entry) / entry > 0.20
103
+
104
+ def _wrong_side_stop(value: Optional[float]) -> bool:
105
+ """Stop on the wrong side of entry for the current action."""
106
+ if value is None:
107
+ return False
108
+ if action == Action.BUY and value >= entry:
109
+ return True
110
+ if action == Action.SELL and value <= entry:
111
+ return True
112
+ return False
113
+
114
+ def _wrong_side_target(value: Optional[float]) -> bool:
115
+ """Target on the wrong side of entry for the current action."""
116
+ if value is None:
117
+ return False
118
+ if action == Action.BUY and value <= entry:
119
+ return True
120
+ if action == Action.SELL and value >= entry:
121
+ return True
122
+ return False
123
+
124
+ if action == Action.BUY:
125
+ if _out_of_band(stop) or _wrong_side_stop(stop):
126
+ stop = round(entry * 0.97, 2)
127
+ if _out_of_band(target) or _wrong_side_target(target):
128
+ target = round(entry * 1.05, 2)
129
+ elif action == Action.SELL:
130
+ if _out_of_band(stop) or _wrong_side_stop(stop):
131
+ stop = round(entry * 1.03, 2)
132
+ if _out_of_band(target) or _wrong_side_target(target):
133
+ target = round(entry * 0.95, 2)
134
+ # HOLD: leave as-is; the Strategist can describe a range.
135
+
136
+ return TradingSignal(
137
+ ticker=signal.ticker,
138
+ action=signal.action,
139
+ confidence=signal.confidence,
140
+ entry_price=entry,
141
+ stop_loss=stop,
142
+ target_price=target,
143
+ reasoning=signal.reasoning,
144
+ )
145
 
146
  def _parse_primary(self, raw_output: str, ticker: str) -> Optional[TradingSignal]:
147
  """Attempt to parse using the primary structured format."""