Squeeze-Einstieg ueber ruhende Stop-Orders + Meta-Labeling gemessen (verworfen)
1) STOP-ORDER STATT MARKET (gebaut, live). engine._manage_squeeze_pending haelt beide Box-Grenzen mit BUY_STOP/SELL_STOP bestueckt, solange der Squeeze armed ist; trader.place_stop/cancel_pending/pending_orders neu. Groesse und SL kommen aus denselben Funktionen wie die Market-Order, nur bezogen auf den Trigger. OCO-Ersatz: faellt eine Seite, wird die andere im selben Tick storniert. Der kritische Punkt bei einer ruhenden Order sind die Gates - sie waren bisher Laufzeit-Checks im Market-Pfad. Neu: engine._squeeze_guard() als EINE Quelle fuer beide Wege, jeden Tick neu bewertet; schlaegt ein Gate zu, werden die Orders storniert. Market bleibt Fallback mit Nachjagd-Bremse squeeze_max_chase_atr=0.20 (gemessen der noch positive Bereich). Behandelt: Broker-Mindestabstand, falsche Marktseite, Toleranz 0,02 xATR gegen Order-Churn, Log nur bei Fehleraenderung, Startup-Schonfrist. 10 Szenarien getestet, live am Snapshot verifiziert. 2) META-LABELING (backtest_metalabel.py) - VERWORFEN. Zweitmodell auf 12 kausalen Merkmalen, Fit/Test in BEIDEN Richtungen: AUC 0,508 und 0,479 = Muenzwurf, Kalibrierung im Top-Bucket 74 % vorhergesagt gegen 39 % real, und 8 von 12 Gewichten kippen das Vorzeichen. Die scheinbar besseren Schwellen sind reine Handelsvermeidung und reproduzieren gegenlaeufig nicht. 23. verworfener Signal-Eingriff - deckt sich mit dem Caveat der Quelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b00820c15
commit
5e0e8b00ca
@@ -1236,6 +1236,32 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
**21. verworfener Signal-Eingriff.** ⚠ Grenzen: eine Schwung-Definition (M15-Pivots
|
||||
k=3, ≥2×ATR); die Pivot-Kontrolle ist dünner (n=173/224). Der Vergleich ist aber
|
||||
design-intern, die relative Aussage damit robuster als die Absolutwerte.
|
||||
- **Meta-Labeling (López de Prado) auf dem Wellensignal = VERWORFEN (gemessen
|
||||
`backtest_metalabel.py`, 2026-08-05, aus der Web-Recherche „Best Practices"):**
|
||||
Kein 23. Filter, sondern ein **Zweitmodell**, das für ein bereits erzeugtes Signal
|
||||
P(Gewinn) schätzt — dieselbe Machinerie wie das erfolgreiche P(break)-Modell
|
||||
(12 kausale Merkmale: Konfidenz · Überdehnung · HTF/H1-Übereinstimmung · Kosten ·
|
||||
Vola-Regime · mom3/mom6 · Efficiency Ratio · Nacht · Entry-Raum · Winkel;
|
||||
logistische Regression, Fit auf einer Hälfte, Test auf der anderen, **in BEIDEN
|
||||
Richtungen**). Population = der volle Live-Gate-Stack (929 bzw. 1316 Signale).
|
||||
**Fällt klar durch:**
|
||||
| | Fit H1 → Test H2 | Fit H2 → Test H1 |
|
||||
|---|---|---|
|
||||
| **AUC oos** | **0,508** | **0,479** |
|
||||
| Kalibrierung | 49,3 % vorhergesagt vs. **38,6 %** real | 37,2 vs. 38,0 % |
|
||||
| Top-Bucket (p≥0,55) | **74,1 % vorhergesagt, 39,1 % real** | (existiert nicht) |
|
||||
Münzwurf in beiden Richtungen (P(break) erreicht 0,65), und **8 von 12
|
||||
Merkmalsgewichten kippen das Vorzeichen** zwischen den Fits — das Modell lernt
|
||||
nichts Stabiles. Die im H1→H2-Lauf scheinbar „besseren" Schwellen (p≥0,45…0,55)
|
||||
sind reine **Handelsvermeidung** (ΣR steigt nur, weil n fällt; ØR bleibt negativ)
|
||||
und reproduzieren in der Gegenrichtung nicht. **23. verworfener Signal-Eingriff.**
|
||||
⚠ Deckt sich exakt mit dem Caveat der Quelle (Hudson & Thames): *„if the algorithm
|
||||
is bad then meta-labeling would likely only reduce the downside."* Das Wellensignal
|
||||
ist in H1 gemessen negativ — ein Zweitmodell rettet ein schwaches Primärsignal nicht.
|
||||
✅ **Was das positiv zeigt:** die Trennung „am Signal ist nichts zu holen, der Hebel
|
||||
liegt in **Ausführung** und Verhalten" hält auch gegen die aktuelle Lehrmeinung.
|
||||
Am selben Tag hat der Ausführungs-Hebel (Stop-Order statt Market) **+0,61 ØR**
|
||||
gebracht — mehr als alle 23 Signal-Ideen zusammen.
|
||||
- **„Erst messen, dann umbauen":** Strategie-Änderungen per Backtest belegen, nicht
|
||||
raten. Gegen-Trend verliert; nicht bei WARTEN handeln. **Stichproben ~28 Tage** —
|
||||
ein Trend-Regime; Befunde später auf mehr/andere History gegenprüfen.
|
||||
@@ -1500,14 +1526,43 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
**blockiert-dann-freigegebenen** Ausbrüchen: feuert der Squeeze, während eine
|
||||
Position offen ist (oder Nacht-/News-Fenster), wird KEIN Dedup-Marker gesetzt — der
|
||||
Entry kommt später zum dann gültigen Marktpreis, u. U. weit hinter dem Level.
|
||||
⚠ **Noch NICHT gebaut** — Umsetzung hat echte Fallstricke: die Guards (Nacht,
|
||||
News-Blackout, Cooldowns, flat-Prüfung) wirken heute als Laufzeit-Checks; mit einer
|
||||
ruhenden Order müssten sie über **Platzieren/Stornieren** durchgesetzt werden, sonst
|
||||
feuert eine vergessene Pending-Order in einem gesperrten Fenster. Dazu: MT5-Mindest-
|
||||
abstand (`SYMBOL_TRADE_STOPS_LEVEL`) kann die Platzierung ablehnen, wenn der Kurs
|
||||
schon zu nah am Level steht, und die Order muss storniert werden, sobald der Squeeze
|
||||
sich klärt. **Das ist der stärkste offene Hebel im Projekt** — größer als jeder der
|
||||
22 verworfenen Signal-Eingriffe.
|
||||
✅✅ **GEBAUT & LIVE (2026-08-05, User-Vorgabe „setze alle Vorschläge um"),
|
||||
`[trading] squeeze_pending_entry=true`.** `engine._manage_squeeze_pending` (im
|
||||
`_pos_loop`, VOR dem Market-Pfad) hält **beide Box-Grenzen** mit ruhenden
|
||||
`BUY_STOP`/`SELL_STOP` bestückt, solange der Squeeze `armed` ist:
|
||||
`trader.place_stop/cancel_pending/pending_orders` (Größe und SL aus **denselben**
|
||||
Funktionen wie die Market-Order, nur bezogen auf den Trigger-Preis — sonst wäre der
|
||||
SL-Abstand ein anderer als validiert). **OCO-Ersatz:** füllt eine Seite, existiert
|
||||
eine Position → die Bedingung „soll liegen" wird falsch und die Gegenseite wird im
|
||||
selben Tick storniert (MT5 kennt kein natives OCO).
|
||||
⚠ **Der kritische Punkt bei einer RUHENDEN Order: die Gates.** Nacht-Fenster,
|
||||
News-Blackout, Circuit-Breaker und der An/Aus-Schalter waren bisher Laufzeit-Checks
|
||||
im Market-Pfad; eine liegende Order würde sie schlicht ignorieren. Deshalb neu
|
||||
**`engine._squeeze_guard()` als EINE Quelle für beide Wege** — jeden Tick neu
|
||||
bewertet, und sobald ein Gate zuschlägt, werden die Orders **storniert**. Zwei
|
||||
Kopien derselben Gates wären genau die Divergenz, die hier siebenmal als
|
||||
Deployment-Drift dokumentiert ist.
|
||||
⚠ **Market bleibt nur noch FALLBACK — mit Nachjagd-Bremse** (`squeeze_max_chase_atr`
|
||||
= **0,20**): greift keine Pending (Broker-Mindestabstand, Squeeze sprang direkt auf
|
||||
`active`, Order-Fehler), wird nur noch eingestiegen, solange der Kurs **höchstens
|
||||
0,20×ATR** über dem Level steht — genau der Bereich, der gemessen in beiden Hälften
|
||||
positiv ist (+0,244/+0,426). Darüber: Ausbruch auslassen, Log „⏭ Auto-Squeeze
|
||||
übersprungen". `_squeeze_last_sig` wird dabei NICHT gesetzt → läuft der Kurs zum
|
||||
Level zurück, darf der Entry noch kommen.
|
||||
⚠ Weitere Fallstricke, alle behandelt: **Broker-Mindestabstand**
|
||||
(`trade_stops_level`) wird VOR dem Senden geprüft und der Fehler nur bei ÄNDERUNG
|
||||
geloggt (sonst 1 Zeile/Sekunde, wenn der Kurs an der Box-Grenze klebt) · Order auf
|
||||
der falschen Marktseite wird abgelehnt · **Toleranz 0,02×ATR**, damit nicht jede
|
||||
ATR-Zuckung die Orders löscht und neu setzt · Startup-Schonfrist gilt auch hier.
|
||||
**Mit 10 Szenarien getestet** (setzen · Idempotenz beim zweiten Tick · Fill →
|
||||
Gegenseite weg · Squeeze klärt sich · Gate schlägt zu · Schonfrist · Feature aus ·
|
||||
Box verschiebt sich · Broker lehnt ab · Nachjagd-Bremse in beide Richtungen).
|
||||
Live verifiziert am Snapshot: `squeeze_pending=true`, `squeeze_max_chase_atr=0.2`,
|
||||
`squeeze_pending_levels` (leer, weil eine Position offen war = korrektes Verhalten).
|
||||
**Zurück zum alten Verhalten: `squeeze_pending_entry=false`.**
|
||||
⚠ **B4-Pflicht:** der Live-Abstand Einstieg↔Level muss jetzt gegen ~0 gehen
|
||||
(vorher Median +0,306×ATR). Das ist die Erfolgskontrolle — nachrechnen wie in
|
||||
diesem Abschnitt (Log-Zeile „Ausbruch <level>" × `trades.entry_price`).
|
||||
- **Auto-Squeeze-Entry = LIVE AUTONOM (User-Vorgabe 2026-07-16, `[trading]
|
||||
auto_squeeze=true`, Default FALSE):** der Bot eröffnet **selbständig eine echte
|
||||
Order**, sobald der **gemessen-validierte** Squeeze-Ausbruch feuert (`wave.squeeze.
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""META-LABELING auf dem Wellensignal (López de Prado) — 2026-08-05.
|
||||
|
||||
IDEE (aus der Web-Recherche): NICHT ein 23. Signal-Filter, sondern ein
|
||||
**Zweitmodell**, das für ein bereits erzeugtes Signal die Wahrscheinlichkeit
|
||||
schätzt, dass es GEWINNT — und nur die guten durchlässt. Das Primärmodell
|
||||
(Richtung) bleibt unangetastet.
|
||||
|
||||
WARUM DAS HIER ETWAS ANDERES IST ALS DIE 22 VERWORFENEN EINGRIFFE:
|
||||
Jene waren einzelne, handgewählte Bedingungen, je einzeln getestet („shorte nicht
|
||||
gegen den Winkel", „meide Chop", …). Meta-Labeling lernt eine **kalibrierte
|
||||
Wahrscheinlichkeit aus vielen Merkmalen gleichzeitig** und wird wie das
|
||||
P(break)-Modell out-of-sample auf AUC UND Kalibrierung geprüft. Genau dieses
|
||||
Vorgehen hat im Projekt schon einmal funktioniert (P(break), AUC 0,65 oos).
|
||||
|
||||
⚠ ERWARTUNGSMANAGEMENT (Hudson & Thames, wörtlich): „if the algorithm is bad then
|
||||
meta-labeling would likely only reduce the downside." Das Wellensignal ist in H1
|
||||
gemessen negativ. Realistisch macht ein Zweitmodell daraus einen kleineren
|
||||
Verlierer oder Breakeven — keinen Gewinner. Genau das wird hier geprüft, nicht
|
||||
behauptet.
|
||||
|
||||
AUFBAU
|
||||
* Primärsignal = der volle LIVE-Gate-Stack über die ECHTEN Methoden (`_build`,
|
||||
`_confirm_breakout`-Mechanik, `_room_gate`) — identisch zu
|
||||
`backtest_breakout_gated.py`, damit die Population die live gehandelte ist.
|
||||
* Label = hat der Trade mit dem kanonischen Exit + Echtkosten GEWONNEN (R > 0)?
|
||||
* Merkmale: nur zum Signalzeitpunkt bekannte Größen (kausal).
|
||||
* Fit auf einer Hälfte → gemessen auf der ANDEREN. **Beide Richtungen**
|
||||
(H1→H2 und H2→H1), sonst ist ein Einweg-Zufall nicht auszuschließen.
|
||||
|
||||
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
|
||||
Übernehmen nur, wenn eine Schwelle in **BEIDEN** Fit-Richtungen ΣR **und** ØR
|
||||
gegenüber „alle Signale nehmen" verbessert, UND die Nachbar-Schwellen mithalten.
|
||||
|
||||
Aufruf: python backtest_metalabel.py [bars]
|
||||
"""
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import numpy as np
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
from core.analysis import calc_trend_angle
|
||||
from core.exit_model import LIVE, simulate
|
||||
from core.wave_rec import (WaveRecommender, _EMA_FAST, _EMA_SLOW, _N_BARS,
|
||||
_ANGLE_LR, _ANGLE_DEAD, _HTF_DEADBAND)
|
||||
|
||||
_BERLIN = ZoneInfo("Europe/Berlin")
|
||||
_BROKER = timezone(timedelta(hours=3))
|
||||
_UTC = timezone.utc
|
||||
_ATRMIN = 0.06
|
||||
_COOL = 3
|
||||
_TIMEOUT_BARS = 12
|
||||
_PIV_K = 3
|
||||
_PIV_LOOK = 300
|
||||
_ATR_FLOOR_PB = 0.12
|
||||
_K = 0.3 # breakout_k wie live
|
||||
_SCHWELLEN = (0.30, 0.35, 0.40, 0.45, 0.50, 0.55)
|
||||
|
||||
_MERKMALE = ["conf", "stretch", "htf", "h1", "kosten", "vola",
|
||||
"mom3", "mom6", "er", "nacht", "raum", "winkel"]
|
||||
|
||||
|
||||
def _berlin(ts):
|
||||
return (datetime.fromtimestamp(int(ts), _UTC)
|
||||
.replace(tzinfo=_BROKER).astimezone(_BERLIN))
|
||||
|
||||
|
||||
def _ema_series(v, p):
|
||||
k = 2 / (p + 1); out = [v[0]]
|
||||
for x in v[1:]:
|
||||
out.append(out[-1] + k * (x - out[-1]))
|
||||
return out
|
||||
|
||||
|
||||
def _atr_series(H, L, C, p=14):
|
||||
tr = [0.0]
|
||||
for i in range(1, len(C)):
|
||||
tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
|
||||
out = [None]
|
||||
for i in range(1, len(C)):
|
||||
seg = tr[max(1, i - p + 1):i + 1]
|
||||
out.append(sum(seg) / len(seg))
|
||||
return out
|
||||
|
||||
|
||||
def _htf_sign(C, i, n, atr):
|
||||
"""M30/H1-Trend aus M5 aggregiert (wie in `backtest_breakout_gated.py`)."""
|
||||
if i < n * _EMA_SLOW:
|
||||
return 0
|
||||
seg = C[i - n * _EMA_SLOW:i + 1:n]
|
||||
if len(seg) < _EMA_SLOW + 2:
|
||||
return 0
|
||||
ef = _ema_series(seg, _EMA_FAST)[-1]; es = _ema_series(seg, _EMA_SLOW)[-1]
|
||||
dead = _HTF_DEADBAND * atr * (n ** 0.5)
|
||||
return 1 if ef - es > dead else (-1 if es - ef > dead else 0)
|
||||
|
||||
|
||||
def sammle(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi, vol_med):
|
||||
"""Alle live-gegateten Signale mit Merkmalen UND Ergebnis."""
|
||||
def kosten(i, atr):
|
||||
return (SP[i] if SP[i] > 0 else 0.0225) / atr
|
||||
|
||||
out = []
|
||||
pend = None
|
||||
i = max(lo, _PIV_LOOK + _EMA_SLOW + 5)
|
||||
while i < hi:
|
||||
atr = A[i]
|
||||
if not atr or atr < _ATRMIN:
|
||||
i += 1; continue
|
||||
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
|
||||
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K],
|
||||
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]}
|
||||
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
|
||||
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
|
||||
sig = rec.get("signal")
|
||||
if _K > 0 and sig in ("LONG", "SHORT"):
|
||||
d = 1 if sig == "LONG" else -1
|
||||
if pend is None or pend["dir"] != d:
|
||||
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
||||
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
|
||||
if not pend["confirmed"]:
|
||||
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
|
||||
against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
|
||||
if broke:
|
||||
pend["confirmed"] = True
|
||||
else:
|
||||
if against or (i - pend["bar"]) > _TIMEOUT_BARS:
|
||||
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
||||
"invalid": C[i] - d * _K * atr,
|
||||
"confirmed": False, "bar": i}
|
||||
i += 1; continue
|
||||
elif sig == "WARTEN":
|
||||
pend = None
|
||||
if sig not in ("LONG", "SHORT"):
|
||||
i += 1; continue
|
||||
rec = w._room_gate(rec, C[i])
|
||||
if rec.get("signal") not in ("LONG", "SHORT"):
|
||||
i += 1; continue
|
||||
|
||||
d = 1 if rec["signal"] == "LONG" else -1
|
||||
a = max(atr, _ATRMIN)
|
||||
# ── Merkmale, ALLE zum Signalzeitpunkt bekannt ──────────────────────
|
||||
seg = [abs(C[j] - C[j - 1]) for j in range(max(1, i - 11), i + 1)]
|
||||
er = (abs(C[i] - C[max(0, i - 12)]) / sum(seg)) if sum(seg) > 0 else 0.0
|
||||
# Abstand zum nächsten GEGENlevel (dieselbe Quelle wie das Raum-Gate)
|
||||
gg = [p for p in (w._pb_levels["ph"] if d > 0 else w._pb_levels["pl"])
|
||||
if ((p > C[i]) if d > 0 else (p < C[i]))]
|
||||
raum = (min(abs(p - C[i]) for p in gg) / a) if gg else 5.0
|
||||
f = [rec.get("conf_pct", 0) / 100.0,
|
||||
abs(C[i] - ES[i]) / a,
|
||||
float(HTF[i] * d),
|
||||
float(H1[i] * d),
|
||||
kosten(i, a),
|
||||
a / vol_med,
|
||||
(C[i] - C[max(0, i - 3)]) * d / a,
|
||||
(C[i] - C[max(0, i - 6)]) * d / a,
|
||||
er,
|
||||
1.0 if _berlin(T[i]).hour in (0, 1, 2, 3, 4, 5, 6, 7) else 0.0,
|
||||
min(raum, 5.0),
|
||||
AD[i] * d]
|
||||
r, xj = simulate(C[i], d, a, H, L, C, i + 1, LIVE,
|
||||
timestop=True, use_tp=True, ret_bar=True)
|
||||
rn = r - kosten(i, a)
|
||||
out.append({"f": f, "R": rn, "win": 1.0 if rn > 0 else 0.0})
|
||||
pend = None
|
||||
i = xj + _COOL
|
||||
return out
|
||||
|
||||
|
||||
def fit_logreg(X, y, iters=4000, lr=0.3):
|
||||
"""Dieselbe schlichte Gradientenmethode wie beim P(break)-Modell —
|
||||
bewusst kein neues Verfahren, damit die Ergebnisse vergleichbar bleiben."""
|
||||
n, m = X.shape
|
||||
Xb = np.hstack([np.ones((n, 1)), X]); w = np.zeros(m + 1)
|
||||
for _ in range(iters):
|
||||
p = 1 / (1 + np.exp(-(Xb @ w)))
|
||||
w -= lr * (Xb.T @ (p - y)) / n
|
||||
return w
|
||||
|
||||
|
||||
def auc(y, p):
|
||||
y = np.asarray(y); p = np.asarray(p)
|
||||
pos, neg = p[y == 1], p[y == 0]
|
||||
if len(pos) == 0 or len(neg) == 0:
|
||||
return float("nan")
|
||||
r = np.argsort(np.argsort(np.concatenate([pos, neg]))) + 1
|
||||
return (r[:len(pos)].sum() - len(pos) * (len(pos) + 1) / 2) / (len(pos) * len(neg))
|
||||
|
||||
|
||||
def kenn(rs):
|
||||
if not rs:
|
||||
return None
|
||||
n = len(rs); s = sum(rs)
|
||||
g = sum(x for x in rs if x > 0); v = -sum(x for x in rs if x < 0)
|
||||
return {"n": n, "wr": 100 * sum(1 for x in rs if x > 0) / n,
|
||||
"oer": s / n, "sr": s, "pf": (g / v) if v > 0 else float("inf")}
|
||||
|
||||
|
||||
def bewerte(train, test, lbl):
|
||||
Xt = np.array([e["f"] for e in train]); yt = np.array([e["win"] for e in train])
|
||||
mu = Xt.mean(0); sd = Xt.std(0) + 1e-9
|
||||
w = fit_logreg((Xt - mu) / sd, yt)
|
||||
Xs = (np.array([e["f"] for e in test]) - mu) / sd
|
||||
p = 1 / (1 + np.exp(-(np.hstack([np.ones((len(Xs), 1)), Xs]) @ w)))
|
||||
ys = np.array([e["win"] for e in test]); R = [e["R"] for e in test]
|
||||
print(f"\n {lbl} (Fit n={len(train)} · Test n={len(test)})")
|
||||
print(f" AUC out-of-sample: {auc(ys, p):.3f} "
|
||||
f"(0,5 = Muenzwurf; P(break) erreicht 0,65)")
|
||||
print(f" Kalibrierung: Ø-Vorhersage {p.mean()*100:.1f} % vs. echte "
|
||||
f"Gewinnrate {ys.mean()*100:.1f} %")
|
||||
print(f" {'Bucket':<12}{'n':>6}{'vorhergesagt':>14}{'real':>8}")
|
||||
for a_, b_ in ((0, .35), (.35, .45), (.45, .55), (.55, 1.01)):
|
||||
m = (p >= a_) & (p < b_)
|
||||
if m.sum() >= 20:
|
||||
print(f" {f'{a_:.2f}-{b_:.2f}':<12}{int(m.sum()):>6}"
|
||||
f"{p[m].mean()*100:>13.1f} %{ys[m].mean()*100:>7.1f} %")
|
||||
basis = kenn(R)
|
||||
print(f" {'Schwelle':<12}{'n':>6}{'WR':>7}{'OeR':>9}{'PF':>7}{'SigmaR':>9}")
|
||||
print(f" {'alle nehmen':<12}{basis['n']:>6}{basis['wr']:>6.1f}%"
|
||||
f"{basis['oer']:>+9.3f}{basis['pf']:>7.2f}{basis['sr']:>+9.1f}")
|
||||
erg = {}
|
||||
for t in _SCHWELLEN:
|
||||
rs = [R[i] for i in range(len(R)) if p[i] >= t]
|
||||
k = kenn(rs)
|
||||
erg[t] = k
|
||||
if k:
|
||||
print(f" {f'p >= {t:.2f}':<12}{k['n']:>6}{k['wr']:>6.1f}%"
|
||||
f"{k['oer']:>+9.3f}{k['pf']:>7.2f}{k['sr']:>+9.1f}"
|
||||
+ (" besser" if (k['sr'] > basis['sr'] and k['oer'] > basis['oer'])
|
||||
else ""))
|
||||
return basis, erg, dict(zip(_MERKMALE, w[1:]))
|
||||
|
||||
|
||||
def main():
|
||||
n_bars = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||
mt5.initialize()
|
||||
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
|
||||
if mt5.symbol_info(c)), None)
|
||||
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n_bars)
|
||||
point = mt5.symbol_info(sym).point
|
||||
mt5.shutdown()
|
||||
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
|
||||
C = [float(b["close"]) for b in bars]; T = [int(b["time"]) for b in bars]
|
||||
SP = [float(b["spread"]) * point for b in bars]
|
||||
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
|
||||
A = _atr_series(H, L, C)
|
||||
AD = [0.0] * len(C)
|
||||
for i in range(_N_BARS, len(C)):
|
||||
AD[i] = calc_trend_angle(C[i - _N_BARS:i + 1], _ANGLE_LR) - 90.0
|
||||
if abs(AD[i]) < _ANGLE_DEAD:
|
||||
AD[i] = 0.0
|
||||
HTF = [_htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
|
||||
H1S = [_htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
|
||||
PH = [j for j in range(_PIV_K, len(C) - _PIV_K)
|
||||
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
|
||||
PL = [j for j in range(_PIV_K, len(C) - _PIV_K)
|
||||
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
|
||||
vol_med = sorted(x for x in A if x)[len(A) // 2]
|
||||
|
||||
w = WaveRecommender(lambda: None)
|
||||
w.set_reversal_enabled(False) # live-Stand seit 2026-08-04
|
||||
w.set_dead_hours("")
|
||||
mid = len(C) // 2
|
||||
|
||||
print("=" * 92)
|
||||
print(f" META-LABELING auf dem Wellensignal — {sym} M5, {len(C)} Bars")
|
||||
print(f" {_berlin(T[0]):%d.%m.%Y} bis {_berlin(T[-1]):%d.%m.%Y} · voller Live-Gate-Stack"
|
||||
f" · kanonischer Exit · Echtkosten")
|
||||
print("=" * 92)
|
||||
e1 = sammle(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1S, PH, PL,
|
||||
0, mid, vol_med)
|
||||
e2 = sammle(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1S, PH, PL,
|
||||
mid, len(C), vol_med)
|
||||
print(f" Signale: H1 {len(e1)} · H2 {len(e2)}")
|
||||
if len(e1) < 100 or len(e2) < 100:
|
||||
print(" Zu wenige Signale fuer eine Aussage."); return
|
||||
|
||||
b1, g1, w1 = bewerte(e1, e2, "FIT auf H1 -> TEST auf H2")
|
||||
b2, g2, w2 = bewerte(e2, e1, "FIT auf H2 -> TEST auf H1")
|
||||
|
||||
print("\n" + "=" * 92)
|
||||
print(" URTEIL (Regel VOR dem Lauf fixiert: in BEIDEN Fit-Richtungen SigmaR UND")
|
||||
print(" OeR besser als „alle nehmen\", plus Nachbar-Schwellen)")
|
||||
print("=" * 92)
|
||||
|
||||
def gut(t, g, b):
|
||||
k = g.get(t)
|
||||
return bool(k) and k["sr"] > b["sr"] and k["oer"] > b["oer"]
|
||||
|
||||
treffer = []
|
||||
for idx, t in enumerate(_SCHWELLEN):
|
||||
selbst = gut(t, g1, b1) and gut(t, g2, b2)
|
||||
nb = [_SCHWELLEN[j] for j in (idx - 1, idx + 1) if 0 <= j < len(_SCHWELLEN)]
|
||||
nb_ok = all(gut(m, g1, b1) and gut(m, g2, b2) for m in nb)
|
||||
if selbst and nb_ok:
|
||||
treffer.append(t)
|
||||
print(f" p >= {t:.2f} " + ("BESTEHT" if (selbst and nb_ok) else
|
||||
"beidseitig besser, Nachbarn NICHT" if selbst
|
||||
else "faellt durch"))
|
||||
print()
|
||||
if treffer:
|
||||
print(f" => Kandidat(en): {', '.join(f'{t:.2f}' for t in treffer)}")
|
||||
print(" Naechster Schritt waere ein Live-Einbau als ANZEIGE (nicht als Gate),")
|
||||
print(" analog zum P(break)-Modell — erst kalibriert beobachten, dann schalten.")
|
||||
else:
|
||||
print(" => KEINE Schwelle besteht. Meta-Labeling traegt auf dieser Population")
|
||||
print(" nicht — passt zu Hudson & Thames: ein schwaches Primaersignal laesst")
|
||||
print(" sich auch mit einem Zweitmodell nicht retten.")
|
||||
print("\n Merkmalsgewichte (standardisiert, Fit H1 | Fit H2):")
|
||||
for m in _MERKMALE:
|
||||
print(f" {m:<10}{w1[m]:>+8.3f} | {w2[m]:>+8.3f}"
|
||||
+ (" (Vorzeichen kippt)" if w1[m] * w2[m] < 0 else ""))
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+157
-28
@@ -50,7 +50,12 @@ from core.candles import CandleAnatomy
|
||||
from core.cone import build as _cone_build
|
||||
from core.candle_logger import CandleLogger
|
||||
from core.wave_rec import (_atr as _wr_atr, _ema_last,
|
||||
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX)
|
||||
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX,
|
||||
# Ausbruchs-Aufschlag der Box — DIESELBE Konstante, die
|
||||
# `_squeeze_one` nutzt. Nachbauen wäre die Sorte Kopie,
|
||||
# die im Projekt schon 4 divergierende Exit-Modelle
|
||||
# erzeugt hat.
|
||||
_SQ_K as _SQ_K_ENG)
|
||||
|
||||
log = get_logger("engine")
|
||||
|
||||
@@ -331,6 +336,21 @@ class TradingEngine:
|
||||
# der flat-Entry bleibt davon unberührt (der validierte Teil).
|
||||
self._squeeze_reverse = self.cfg["trading"].get("auto_squeeze_reverse", "false").lower() == "true"
|
||||
self._squeeze_last_sig = None # Dedup Entry (flat)
|
||||
# ── Einstieg über RUHENDE Stop-Orders statt Market (2026-08-05, gemessen
|
||||
# `backtest_squeeze_entry.py`: der Einstiegspreis IST der Edge). Die Orders
|
||||
# liegen, solange der Squeeze `armed` ist, an beiden Box-Grenzen.
|
||||
self._squeeze_pending = self.cfg["trading"].get(
|
||||
"squeeze_pending_entry", "true").lower() == "true"
|
||||
# Market bleibt der Fallback — aber nur, solange der Kurs noch im gemessen
|
||||
# positiven Bereich über dem Level steht (0,20×ATR → ØR +0,244/+0,426).
|
||||
try:
|
||||
self._squeeze_max_chase = float(self.cfg["trading"].get(
|
||||
"squeeze_max_chase_atr", "0.20"))
|
||||
except (TypeError, ValueError):
|
||||
self._squeeze_max_chase = 0.20
|
||||
self._squeeze_pending_levels = None
|
||||
self._squeeze_pending_err = None
|
||||
self._squeeze_chase_logged = None
|
||||
self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade)
|
||||
self._squeeze_entry_count = 0
|
||||
# ── Autonomer Entry auf die EMPFEHLUNG (User-Wunsch 2026-07-30) ───────────
|
||||
@@ -677,6 +697,10 @@ class TradingEngine:
|
||||
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
||||
self._check_flip_close() # Empfehlung dreht → schließen
|
||||
self._check_sr_close()
|
||||
# ZUERST die ruhenden Stop-Orders pflegen (setzen/stornieren),
|
||||
# DANN der Market-Fallback: so kann eine bereits liegende Order
|
||||
# den Ausbruch am Level abfangen, bevor nachgejagt würde.
|
||||
self._manage_squeeze_pending()
|
||||
self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch
|
||||
self._check_auto_signal() # autonomer Entry auf die Empfehlung
|
||||
except Exception as e:
|
||||
@@ -1049,16 +1073,112 @@ class TradingEngine:
|
||||
tg["bot_token"], tg["chat_id"])
|
||||
except Exception as e: log.warning(f"S/R-Close-Telegram: {e}")
|
||||
|
||||
def _squeeze_guard(self) -> str | None:
|
||||
"""Die NICHT richtungsabhängigen Gates für autonome Squeeze-Entries.
|
||||
→ Grundtext, wenn geblockt, sonst None.
|
||||
|
||||
⚠ Bewusst EINE Funktion für beide Einstiegswege (ruhende Stop-Order UND
|
||||
Market-Fallback). Zwei Kopien derselben Gates wären genau die Divergenz, die
|
||||
dieses Projekt siebenmal als Deployment-Drift dokumentiert hat — und bei einer
|
||||
RUHENDEN Order wiegt sie schwerer: eine vergessene Pending-Order feuert im
|
||||
gesperrten Fenster, ohne dass jemand hinschaut."""
|
||||
if not self._auto_squeeze:
|
||||
return "Auto-Squeeze aus"
|
||||
if self._circuit_halted():
|
||||
return "Circuit-Breaker"
|
||||
if (self._squeeze_skip_night
|
||||
and datetime.now(_BERLIN).hour in self._squeeze_night_hours):
|
||||
return "Nacht-Fenster"
|
||||
_bl = self.calendar.blackout()
|
||||
if _bl:
|
||||
if self._news_block_logged != _bl["title"]:
|
||||
self._news_block_logged = _bl["title"]
|
||||
log.info(f"📅 News-Blackout — kein Auto-Entry: {_bl['title']} "
|
||||
f"({_bl['impact']}, in {_bl['minutes']:.0f} min)")
|
||||
return "News-Blackout"
|
||||
self._news_block_logged = None
|
||||
return None
|
||||
|
||||
def _manage_squeeze_pending(self):
|
||||
"""RUHENDE Stop-Orders an beiden Box-Grenzen, solange der Squeeze `armed` ist.
|
||||
|
||||
WARUM (gemessen `backtest_squeeze_entry.py`, 2026-08-05): der Squeeze hat
|
||||
seinen gesamten Edge im EINSTIEGSPREIS. Die Market-Order auf dem 5-s-Poll
|
||||
füllt live im Median 0,306×ATR hinter dem Level (Mittel 0,741, Max 4,3) →
|
||||
ØR kippt von +0,456 auf −0,158. Eine ruhende Stop-Order deckelt den Preis
|
||||
konstruktiv: selbst mit 0,20×ATR Fill-Slippage bleibt ØR +0,244/+0,426.
|
||||
|
||||
Beide Seiten werden bestückt (die Box kann nach oben ODER unten brechen);
|
||||
füllt eine, existiert eine Position → `soll` wird False und die Gegenseite
|
||||
wird im selben Tick storniert. Das ersetzt ein echtes OCO, das MT5 nicht hat.
|
||||
"""
|
||||
if not self._squeeze_pending:
|
||||
return
|
||||
sym = self.data.symbol or ""
|
||||
if not sym:
|
||||
return
|
||||
try:
|
||||
ps = self.trader.snapshot()
|
||||
sq = (self.wave.snapshot() or {}).get("squeeze") or {}
|
||||
atr = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0
|
||||
hi, lo = sq.get("box_hi"), sq.get("box_lo")
|
||||
grund = self._squeeze_guard()
|
||||
# Orders sollen NUR liegen, wenn: flat · kein Gate · Kompression aktiv ·
|
||||
# Box bekannt · Schonfrist vorbei. Jede dieser Bedingungen kann sich
|
||||
# jederzeit ändern — deshalb wird das JEDEN Tick neu bewertet und nicht
|
||||
# beim Platzieren einmal geprüft.
|
||||
soll = (not ps.get("ticket") and grund is None
|
||||
and sq.get("state") == "armed"
|
||||
and hi and lo and atr > 0
|
||||
and not self._in_startup_grace())
|
||||
offen = self.trader.pending_orders(sym)
|
||||
if not soll:
|
||||
for o in offen:
|
||||
err = self.trader.cancel_pending(o.ticket)
|
||||
if not err:
|
||||
log.info(f"⏸ Squeeze-Pending storniert (T={o.ticket}) — "
|
||||
f"{grund or ('Position offen' if ps.get('ticket') else 'Squeeze geklärt')}")
|
||||
if offen:
|
||||
self._squeeze_pending_levels = None
|
||||
return
|
||||
ziel = {mt5.ORDER_TYPE_BUY_STOP: round(float(hi) + _SQ_K_ENG * atr, 3),
|
||||
mt5.ORDER_TYPE_SELL_STOP: round(float(lo) - _SQ_K_ENG * atr, 3)}
|
||||
# Bestehende Orders behalten, wenn ihr Trigger noch passt (Toleranz
|
||||
# 0,02×ATR) — sonst löschen und neu setzen. Ohne die Toleranz würde
|
||||
# jede ATR-Zuckung die Orders löschen/neu setzen (Broker-Last, Log-Flut).
|
||||
tol = max(0.02 * atr, 0.001)
|
||||
vorhanden = {}
|
||||
for o in offen:
|
||||
z = ziel.get(o.type)
|
||||
if z is not None and abs(o.price_open - z) <= tol and o.type not in vorhanden:
|
||||
vorhanden[o.type] = o.ticket
|
||||
else:
|
||||
self.trader.cancel_pending(o.ticket)
|
||||
for otype, preis in ziel.items():
|
||||
if otype in vorhanden:
|
||||
continue
|
||||
t, err = self.trader.place_stop(sym, otype, preis, comment="SQZ-STOP")
|
||||
if t:
|
||||
log.info(f"🎯 Squeeze-Pending gesetzt: "
|
||||
f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}"
|
||||
f" @ {preis:.3f} (T={t})")
|
||||
elif err and self._squeeze_pending_err != err:
|
||||
# ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits passiert"
|
||||
# tritt im 1-s-Takt auf, sobald der Kurs an einer Box-Grenze klebt.
|
||||
self._squeeze_pending_err = err
|
||||
log.info(f"Squeeze-Pending nicht platzierbar ({preis:.3f}): {err}")
|
||||
self._squeeze_pending_levels = ziel
|
||||
except Exception as e:
|
||||
log.error(f"_manage_squeeze_pending: {e}", exc_info=True)
|
||||
|
||||
def _check_auto_squeeze(self):
|
||||
"""Autonomer Entry auf den gemessen-validierten Volatilitäts-Squeeze-Ausbruch
|
||||
(`wave.squeeze.state == 'active'`), NUR wenn flat. Feuert **1× je Ausbruch**
|
||||
(Dedup über dir+level; re-armt, sobald der Squeeze klärt). Emergency-Stop
|
||||
armiert beim Öffnen automatisch (2 % Konto, via `_check_auto_close`). Fail-safe."""
|
||||
if not self._auto_squeeze:
|
||||
return
|
||||
if self._circuit_halted(): # Tagesverlust-Stopp aktiv → heute keine Auto-Trades
|
||||
return
|
||||
try:
|
||||
if self._squeeze_guard() is not None: # aus · Circuit · Nacht · News
|
||||
return
|
||||
sq = (self.wave.snapshot() or {}).get("squeeze") or {}
|
||||
if sq.get("state") != "active":
|
||||
self._squeeze_last_sig = None # Ausbruch geklärt → re-armen
|
||||
@@ -1067,29 +1187,9 @@ class TradingEngine:
|
||||
if d not in ("LONG", "SHORT") or lvl is None:
|
||||
return
|
||||
sig = (d, round(float(lvl), 2))
|
||||
# ── Nacht-Guard (Fenster aus `auto_squeeze_night_hours`, Default 3–6 Uhr
|
||||
# Berlin; war bis 2026-08-05 fest 0–7): kein Entry UND kein Reverse. Setzt
|
||||
# keine Dedup-Marker → feuert nach dem Fenster, falls noch aktiv.
|
||||
# ⚠ Der Guard ist NICHT ertrags-begründet: gemessen ist keine Nachtstunde in
|
||||
# beiden Halbjahren negativ, jede geblockte Stunde KOSTET (`backtest_cost_
|
||||
# gate.py`). Er kauft Beaufsichtigungs-Ruhe — 3–6 ist die günstigste Variante
|
||||
# davon. `auto_squeeze_skip_night=false` (oder leere Liste) schaltet ihn aus.
|
||||
if (self._squeeze_skip_night
|
||||
and datetime.now(_BERLIN).hour in self._squeeze_night_hours):
|
||||
return
|
||||
# ── News-Blackout (Handbuch-Regel, NICHT gemessen): 15 min um ein
|
||||
# High-Impact-Event (FOMC/NFP/CPI/EIA/OPEC) kein autonomer Entry. Setzt
|
||||
# KEINEN Dedup-Marker → nach dem Fenster feuert der Ausbruch, falls noch
|
||||
# aktiv. Gilt nur für den Bot; manuelle Orders bleiben frei.
|
||||
_bl = self.calendar.blackout()
|
||||
if _bl:
|
||||
if self._news_block_logged != _bl["title"]:
|
||||
self._news_block_logged = _bl["title"]
|
||||
log.info(f"📅 News-Blackout — kein Auto-Entry: {_bl['title']} "
|
||||
f"({_bl['impact']}, in {_bl['minutes']:.0f} min)")
|
||||
return
|
||||
self._news_block_logged = None
|
||||
|
||||
# Nacht-Fenster, News-Blackout, Circuit-Breaker und der An/Aus-Schalter
|
||||
# sitzen jetzt in `_squeeze_guard()` (oben geprüft) — EINE Quelle für
|
||||
# Pending- und Market-Weg, s. dort.
|
||||
ps = self.trader.snapshot()
|
||||
ticket = ps.get("ticket")
|
||||
if ticket:
|
||||
@@ -1148,6 +1248,28 @@ class TradingEngine:
|
||||
if (d == self._pos_close_dir
|
||||
and time.time() - self._pos_close_ts < _SQUEEZE_REENTRY_COOLDOWN_S):
|
||||
return
|
||||
# ── NACHJAGD-BREMSE (2026-08-05, gemessen `backtest_squeeze_entry.py`).
|
||||
# Der Market-Weg ist nur noch der FALLBACK, wenn keine Pending-Order
|
||||
# gegriffen hat (Broker-Mindestabstand, Squeeze sprang direkt auf
|
||||
# `active`, Order-Fehler). Gemessen ist ein Einstieg bis rund
|
||||
# **0,20×ATR** über dem Level in BEIDEN Hälften positiv (ØR +0,244/
|
||||
# +0,426); darüber kippt es Richtung −0,158/−0,006. Also: nur bis dahin
|
||||
# nachkaufen, sonst diesen Ausbruch auslassen.
|
||||
# ⚠ `_squeeze_last_sig` wird dabei NICHT gesetzt — läuft der Kurs zum
|
||||
# Level zurück (Ausbruch bleibt `active`), darf der Entry noch kommen.
|
||||
atr_m5 = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0
|
||||
tick = self.data.snapshot() or {}
|
||||
px = tick.get("ask") if d == "LONG" else tick.get("bid")
|
||||
if atr_m5 > 0 and px:
|
||||
weg = (float(px) - float(lvl)) * (1 if d == "LONG" else -1) / atr_m5
|
||||
if weg > self._squeeze_max_chase:
|
||||
if self._squeeze_chase_logged != sig:
|
||||
self._squeeze_chase_logged = sig
|
||||
log.info(f"⏭ Auto-Squeeze übersprungen: Kurs steht schon "
|
||||
f"{weg:.2f}×ATR über dem Level {lvl} "
|
||||
f"(Grenze {self._squeeze_max_chase:.2f}) — "
|
||||
f"gemessen ab hier kein Edge mehr")
|
||||
return
|
||||
self._squeeze_last_sig = sig
|
||||
if isinstance(self._last_rec, dict):
|
||||
self._last_rec["setup"] = f"SQUEEZE_{d}"
|
||||
@@ -3391,6 +3513,13 @@ class TradingEngine:
|
||||
# Python-Code läuft (der Batch hat schon 2× still nicht neu gestartet).
|
||||
"squeeze_night_hours": list(self._squeeze_night_hours)
|
||||
if self._squeeze_skip_night else [],
|
||||
# Einstieg über ruhende Stop-Orders (2026-08-05) + die aktuell gesetzten
|
||||
# Trigger — macht im Snapshot prüfbar, ob wirklich Orders liegen.
|
||||
"squeeze_pending": self._squeeze_pending,
|
||||
"squeeze_pending_levels": (
|
||||
sorted(round(v, 3) for v in self._squeeze_pending_levels.values())
|
||||
if self._squeeze_pending_levels else []),
|
||||
"squeeze_max_chase_atr": self._squeeze_max_chase,
|
||||
"squeeze_entry_count": self._squeeze_entry_count,
|
||||
"auto_signal": self._auto_signal,
|
||||
"auto_signal_min_conf": self._auto_signal_min_conf,
|
||||
|
||||
@@ -194,6 +194,94 @@ class TradeManager:
|
||||
self.last_error = _retcode_msg(res)
|
||||
return None, 0.0
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# PENDING STOP-ORDERS (Squeeze-Einstieg am Level, 2026-08-05)
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# WARUM: `backtest_squeeze_entry.py` hat gemessen, dass der Squeeze seinen
|
||||
# gesamten Edge im EINSTIEGSPREIS hat. Die Market-Order auf dem 5-s-Poll füllt
|
||||
# live im Median 0,306×ATR hinter dem Level (Mittel 0,741, Max 4,3) — damit
|
||||
# dreht ØR von +0,456 auf −0,158. Eine ruhende Stop-Order deckelt den Preis
|
||||
# konstruktiv: selbst mit 0,20×ATR unterstellter Fill-Slippage bleibt ØR
|
||||
# +0,244/+0,426 in beiden Halbjahren.
|
||||
|
||||
def pending_orders(self, sym: str) -> list:
|
||||
"""Eigene (magic-gleiche) Pending-Orders des Symbols."""
|
||||
with mt5_lock(timeout=5) as got:
|
||||
if not got:
|
||||
return []
|
||||
try:
|
||||
orders = mt5.orders_get(symbol=sym) or ()
|
||||
except Exception:
|
||||
return []
|
||||
return [o for o in orders if getattr(o, "magic", 0) == MAGIC]
|
||||
|
||||
def place_stop(self, sym: str, otype: int, trigger: float,
|
||||
comment: str = "Widget-SQZ") -> tuple[int | None, str]:
|
||||
"""Pending BUY_STOP/SELL_STOP am Ausbruchs-Level. → (ticket, Fehlertext).
|
||||
|
||||
Größe und SL werden mit DERSELBEN Logik bestimmt wie bei der Market-Order
|
||||
(`calc_lots` / `_calc_sl_tp`), nur bezogen auf den Trigger-Preis statt auf
|
||||
den aktuellen Tick — sonst wäre der SL-Abstand ein anderer als validiert.
|
||||
⚠ Broker-Mindestabstand (`trade_stops_level`) wird geprüft: liegt der Kurs
|
||||
schon zu nah am Trigger, lehnt MT5 die Order mit einem kryptischen retcode
|
||||
ab. Dann meldet die Funktion das sauber, und der Aufrufer entscheidet."""
|
||||
with mt5_lock(timeout=15) as got:
|
||||
if not got:
|
||||
return None, "MT5 belegt"
|
||||
si = mt5.symbol_info(sym)
|
||||
tick = get_tick(sym)
|
||||
if not si or not tick:
|
||||
return None, "Kein Symbol/Tick"
|
||||
ist_long = otype == mt5.ORDER_TYPE_BUY_STOP
|
||||
trigger = round(float(trigger), si.digits)
|
||||
# Stop-Order muss auf der richtigen Seite des Marktes liegen …
|
||||
bezug = tick.ask if ist_long else tick.bid
|
||||
if (trigger <= bezug) if ist_long else (trigger >= bezug):
|
||||
return None, f"Kurs hat das Level bereits passiert ({bezug:.3f})"
|
||||
# … und den Broker-Mindestabstand einhalten
|
||||
mind = (getattr(si, "trade_stops_level", 0) or 0) * (si.point or 0.001)
|
||||
if mind and abs(trigger - bezug) < mind:
|
||||
return None, (f"unter Broker-Mindestabstand "
|
||||
f"({abs(trigger-bezug):.3f} < {mind:.3f})")
|
||||
sl, _tp, sl_src = self._calc_sl_tp(sym, mt5.ORDER_TYPE_BUY if ist_long
|
||||
else mt5.ORDER_TYPE_SELL, trigger)
|
||||
lots = calc_lots(sym, trigger,
|
||||
mt5.ORDER_TYPE_BUY if ist_long else mt5.ORDER_TYPE_SELL)
|
||||
if lots <= 0:
|
||||
return None, "Lot-Fehler"
|
||||
req = {"action": mt5.TRADE_ACTION_PENDING, "symbol": sym,
|
||||
"volume": float(lots), "type": otype, "price": float(trigger),
|
||||
"magic": MAGIC, "comment": comment,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": get_filling(sym)}
|
||||
if sl:
|
||||
req["sl"] = float(sl)
|
||||
res = mt5.order_send(req)
|
||||
for mode in (mt5.ORDER_FILLING_RETURN, mt5.ORDER_FILLING_IOC,
|
||||
mt5.ORDER_FILLING_FOK):
|
||||
if res and res.retcode != 10030:
|
||||
break
|
||||
req["type_filling"] = mode
|
||||
res = mt5.order_send(req)
|
||||
if res and res.retcode == mt5.TRADE_RETCODE_DONE:
|
||||
log_trade.info(f"PENDING {'BUY_STOP' if ist_long else 'SELL_STOP'} "
|
||||
f"{lots:.2f}L @ {trigger:.3f} T={res.order} "
|
||||
f"SL={sl:.3f} ({sl_src})")
|
||||
return res.order, ""
|
||||
return None, _retcode_msg(res)
|
||||
|
||||
def cancel_pending(self, ticket: int) -> str:
|
||||
"""Eine Pending-Order löschen. → "" bei Erfolg, sonst Fehlertext."""
|
||||
with mt5_lock(timeout=15) as got:
|
||||
if not got:
|
||||
return "MT5 belegt"
|
||||
res = mt5.order_send({"action": mt5.TRADE_ACTION_REMOVE,
|
||||
"order": int(ticket)})
|
||||
if res and res.retcode == mt5.TRADE_RETCODE_DONE:
|
||||
log_trade.info(f"PENDING storniert T={ticket}")
|
||||
return ""
|
||||
return _retcode_msg(res)
|
||||
|
||||
def open_long(self, sym):
|
||||
with self._lock:
|
||||
if self.ticket:
|
||||
|
||||
Reference in New Issue
Block a user