From d750da24e52d5fbb445af8c8035589acc5335ddc Mon Sep 17 00:00:00 2001 From: bast Date: Fri, 6 Nov 2026 00:59:10 +0100 Subject: [PATCH] last version --- bots/live_signal_trader.py | 34 +++++++++++++++++++++++++++++++--- bots/mcp_server.py | 4 ++-- bots/traderanalyser.py | 21 ++++++++++++++++++++- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/bots/live_signal_trader.py b/bots/live_signal_trader.py index 3d912ec1..6745d30c 100644 --- a/bots/live_signal_trader.py +++ b/bots/live_signal_trader.py @@ -689,12 +689,37 @@ class LiveSignalTrader: n_tranches = min(n_desired_tranches, max_tranches) return total_lot, n_tranches + # Maximale Abweichung eines geparsten Einstiegs vom Live-Kurs, bevor er als + # Parse-Fehler gilt (Gold bewegt sich intraday selten >2 %). + MAX_ENTRY_DEVIATION = 0.02 + def _resolve_missing_fields(self, signal): """Same default-fill logic as the backtester's _resolve_informal_signals, just resolved against a live price - instead of historical bars.""" + instead of historical bars. + + Zusätzlich: ein geparster Einstieg, der weit v Live-Kurs entfernt + ist (z.B. 2077.75 statt 4127 durch "4126.5-29"), wird verworfen und + durch den Live-Kurs ersetzt. SL/TP, die dann auf der falschen Seite + oder unplausibel weit weg liegen, werden neu aus dem Default-Plan + gebildet bzw. das Signal wird übersprungen.""" + live = self._get_live_price(signal['pair'], signal['action']) + if signal['entry'] is not None and live: + dev = abs(signal['entry'] - live) / live + if dev > self.MAX_ENTRY_DEVIATION: + logger.warning(f"[{self.channel_name}] Entry {signal['entry']} weicht {dev:.0%} vom " + f"Live-Kurs {live} ab – Parse-Fehler, verwende Live-Kurs") + signal['entry'] = live + signal['tp_levels'] = [t for t in signal['tp_levels'] + if abs(t - live) / live <= self.MAX_ENTRY_DEVIATION] + if signal['entry'] is not None and signal['sl'] is not None: + e, sl = signal['entry'], signal['sl'] + wrong_side = (signal['action'] == 'BUY' and sl >= e) or (signal['action'] == 'SELL' and sl <= e) + if wrong_side or abs(sl - e) / e > self.MAX_ENTRY_DEVIATION: + logger.warning(f"[{self.channel_name}] SL {sl} unplausibel zu Entry {e} – Signal übersprungen") + return False if signal['entry'] is None: - price = self._get_live_price(signal['pair'], signal['action']) + price = live or self._get_live_price(signal['pair'], signal['action']) if price is None: return False signal['entry'] = price @@ -703,6 +728,9 @@ class LiveSignalTrader: if signal['sl'] is None: dist = self.analyzer.default_sl_pips * pip signal['sl'] = signal['entry'] - dist if signal['action'] == 'BUY' else signal['entry'] + dist + signal['tp_levels'] = [t for t in signal['tp_levels'] + if (signal['action'] == 'BUY' and t > signal['entry']) + or (signal['action'] == 'SELL' and t < signal['entry'])] if not signal['tp_levels']: levels = [signal['entry'] + (d * pip if signal['action'] == 'BUY' else -d * pip) for d in self.analyzer.default_tp_ladder_pips] @@ -970,7 +998,7 @@ def main(): 'Default 90s: ch1-Goldex detailed signals typically arrive 30-60s after ' 'the informal "buy/sell now" precursor (observed in diagnostics).') parser.add_argument('--min-lot', type=float, default=0.01, - help="Broker's minimum tradable lot - the TP ladder is consolidated to fewer " + help="Broker's minimum tradable lot - the TP ladder is consolidated to few " "tranches (or the signal skipped) if the risk-based lot can't meet this") parser.add_argument('--channels', choices=['ch1', 'ch2', 'both'], default='both', help="Which channel(s) to listen on. " diff --git a/bots/mcp_server.py b/bots/mcp_server.py index 79b5e25d..a79b9156 100644 --- a/bots/mcp_server.py +++ b/bots/mcp_server.py @@ -495,7 +495,7 @@ class LiveBridge(MT5Bridge): vol = grouped.count() idx = ohlc_open.index.intersection(ohlc_close.index) - idx = idx.intersection(ohlhigh.index).intersection(ohlc_low.index) + idx = idx.intersection(ohlc_high.index).intersection(ohlc_low.index) idx = idx[-bars:] if len(idx) == 0: @@ -974,7 +974,7 @@ async def mcp_tools(): { "name": "get_orders", "description": "Get pending orders", - "inputSchema": {"type": "object", "properties"{}}, + "inputSchema": {"type": "object", "properties": {}}, }, ] } diff --git a/bots/traderanalyser.py b/bots/traderanalyser.py index 3fed4f90..3536487d 100644 --- a/bots/traderanalyser.py +++ b/bots/traderanalyser.py @@ -140,6 +140,22 @@ CANCEL_RE = re.compile( ) + +def _expand_short_range_end(start, end_str): + """Signal-Kurzschreibweise auflösen: "4126.5-29" meint 4126.5-4129, + "4198-02" meint 4198-4202. Ist das Ende schon vollständig (gleiche + Größenordnung wie der Start), wird es unverändert zurückgegeben.""" + end = float(end_str) + if start <= 0 or end >= start / 10: + return end + digits = len(end_str.split('.')[0]) + base = 10 ** digits + full = (int(start) // base) * base + end + if full < start - base / 2: # Überlauf, z.B. 4198-02 -> 4202 + full += base + return full + + class TradeSignalAnalyzer: """ Parses a Telegram signal export and runs an OBJECTIVE backtest against @@ -309,7 +325,10 @@ class TradeSignalAnalyzer: range_matches = re.findall(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)', entry_segment) if range_matches: - candidates = [((float(a) + float(b)) / 2, float(a), float(b)) for a, b in range_matches] + candidates = [] + for a, b in range_matches: + lo, hi = float(a), _expand_short_range_end(float(a), b) + candidates.append(((lo + hi) / 2, lo, hi)) else: single_matches = re.findall(r'\d+(?:\.\d+)?', entry_segment) candidates = [(float(x), float(x), float(x)) for x in single_matches] if single_matches else []