last version
All checks were successful
MT5 Helm GitOps Deploy / build-and-deploy (push) Has been skipped
All checks were successful
MT5 Helm GitOps Deploy / build-and-deploy (push) Has been skipped
This commit is contained in:
parent
917b66ac0a
commit
d750da24e5
3 changed files with 53 additions and 6 deletions
|
|
@ -689,12 +689,37 @@ class LiveSignalTrader:
|
||||||
n_tranches = min(n_desired_tranches, max_tranches)
|
n_tranches = min(n_desired_tranches, max_tranches)
|
||||||
return total_lot, n_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):
|
def _resolve_missing_fields(self, signal):
|
||||||
"""Same default-fill logic as the backtester's
|
"""Same default-fill logic as the backtester's
|
||||||
_resolve_informal_signals, just resolved against a live price
|
_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:
|
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:
|
if price is None:
|
||||||
return False
|
return False
|
||||||
signal['entry'] = price
|
signal['entry'] = price
|
||||||
|
|
@ -703,6 +728,9 @@ class LiveSignalTrader:
|
||||||
if signal['sl'] is None:
|
if signal['sl'] is None:
|
||||||
dist = self.analyzer.default_sl_pips * pip
|
dist = self.analyzer.default_sl_pips * pip
|
||||||
signal['sl'] = signal['entry'] - dist if signal['action'] == 'BUY' else signal['entry'] + dist
|
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']:
|
if not signal['tp_levels']:
|
||||||
levels = [signal['entry'] + (d * pip if signal['action'] == 'BUY' else -d * pip)
|
levels = [signal['entry'] + (d * pip if signal['action'] == 'BUY' else -d * pip)
|
||||||
for d in self.analyzer.default_tp_ladder_pips]
|
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 '
|
'Default 90s: ch1-Goldex detailed signals typically arrive 30-60s after '
|
||||||
'the informal "buy/sell now" precursor (observed in diagnostics).')
|
'the informal "buy/sell now" precursor (observed in diagnostics).')
|
||||||
parser.add_argument('--min-lot', type=float, default=0.01,
|
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")
|
"tranches (or the signal skipped) if the risk-based lot can't meet this")
|
||||||
parser.add_argument('--channels', choices=['ch1', 'ch2', 'both'], default='both',
|
parser.add_argument('--channels', choices=['ch1', 'ch2', 'both'], default='both',
|
||||||
help="Which channel(s) to listen on. "
|
help="Which channel(s) to listen on. "
|
||||||
|
|
|
||||||
|
|
@ -495,7 +495,7 @@ class LiveBridge(MT5Bridge):
|
||||||
vol = grouped.count()
|
vol = grouped.count()
|
||||||
|
|
||||||
idx = ohlc_open.index.intersection(ohlc_close.index)
|
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:]
|
idx = idx[-bars:]
|
||||||
|
|
||||||
if len(idx) == 0:
|
if len(idx) == 0:
|
||||||
|
|
@ -974,7 +974,7 @@ async def mcp_tools():
|
||||||
{
|
{
|
||||||
"name": "get_orders",
|
"name": "get_orders",
|
||||||
"description": "Get pending orders",
|
"description": "Get pending orders",
|
||||||
"inputSchema": {"type": "object", "properties"{}},
|
"inputSchema": {"type": "object", "properties": {}},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
class TradeSignalAnalyzer:
|
||||||
"""
|
"""
|
||||||
Parses a Telegram signal export and runs an OBJECTIVE backtest against
|
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)
|
range_matches = re.findall(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)', entry_segment)
|
||||||
if range_matches:
|
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:
|
else:
|
||||||
single_matches = re.findall(r'\d+(?:\.\d+)?', entry_segment)
|
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 []
|
candidates = [(float(x), float(x), float(x)) for x in single_matches] if single_matches else []
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue