Files
AH-Oil-Trader/backtest_sl_pivot.py
Axel HocksandClaude Opus 5 c081bdc734 SL/TP nicht mehr am S/R verankert + Squeeze-Market-Fallback auf den BRK-Slot
User: "SL/TP sollte nicht am S/R verankert werden sondern immer dynamisch
berechnet werden" - nach dem Fund, dass Trades wiederholt am Level schlossen.

(1) PIVOT-ANKER DES INITIAL-SL ENTFERNT (`_SL_PIVOT_ANKER = False`).
`_calc_sl_tp` legte den SL an den naechsten M15-Pivot (LONG: pivot_low − Puffer,
SHORT: pivot_high + Puffer) und klemmte ihn erst DANACH auf 1,8–2,2×ATR. Der
Stop lag damit fuer einen LONG direkt unter der UNTERSTUETZUNG. Ueber 69
geloggte Orders war der Pivot in 4 % massgeblich (59 % gekappt, 36 % geweitet) -
und in genau diesen Faellen stand der Stop auf dem Level, an dem der Markt
ohnehin am haeufigsten durchwickt.
⚠ Der TP erbte das INDIREKT: er ist `2,0 × SL-Distanz`, skalierte also mit dem
Pivot-Abstand. Beide sind jetzt rein ATR-dynamisch (Mitte des Bandes = 2,0×ATR).

VORHER GEMESSEN (`backtest_sl_pivot.py`, 60k M5, 2 Halbjahre, gleiche
Einstiege, NUR die SL-Methode variiert, kanonischer Exit, Echtkosten):
    PIVOT  H1 -0,054 / H2 -0,040   PF 0,93 / 0,95
    FIX    H1 -0,040 / H2 +0,004   PF 0,95 / 1,01
FIX ist in BEIDEN Haelften besser (+0,014 / +0,043 R), der Worst-Case wird nicht
groesser - die vorab fixierte Regel ist erfuellt.
⚠ EHRLICH: beide KI enthalten die Null, es ist KEIN belegter Ertragsgewinn. Der
Punkt ist, dass die Verankerung nichts nuetzt UND den Stop auf das Level legt.
Das bestaetigt `backtest_sl_method.py` ("gedeckelt sind alle Methoden
gleichwertig") auf dem heutigen Exit - dort war es nur nie als NACHTEIL sichtbar.
Zurueck: `_SL_PIVOT_ANKER = True`.

(2) LUECKE AUS DEM ZWEI-SLOT-UMBAU, vom Deploy-Schritt 5 aufgedeckt:
"Auto-Squeeze-Entry fehlgeschlagen: Position bereits offen!". `engine.open_long`
routete IMMER auf `self.trader` - der Market-Fallback des Squeeze lief also
weiter ueber Slot 1 und scheiterte, sobald ein manueller Trade lief. FLAT-Check
und Pending-Manager waren gestern umgehaengt, der BESTELLWEG nicht.
Neu `_slot(source)`: auto_squeeze -> trader_brk, alles andere -> trader. Auch
der Bot-Marker (`_bot_open_ticket`) holt das Ticket jetzt vom richtigen Slot.
⚠ Genau die Fehlerklasse von Deployment-Drift Fall 8 - ein Umbau, der den
Bestellweg aendert, muss ALLE Bestellwege mitnehmen.

91 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:32:21 +02:00

167 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pivot-verankerter SL gegen fixen 2,0xATR-SL — mit dem HEUTIGEN Exit (2026-08-20)
ANLASS (User): "ueberpruefe genau ob wirklich kein Trade am S/R geschlossen
wird ... das hat mich mindestens 3x Geld gekostet."
DER FUND, den die Pfad-Pruefung ergeben hat: `trader._calc_sl_tp` legt den
Initial-SL an den naechsten M15-PIVOT (LONG: `pivot_low` Puffer, SHORT:
`pivot_high` + Puffer) und klemmt ihn erst danach auf 1,82,2×ATR. Der Stop
liegt also — wenn der Pivot innerhalb des Bandes liegt — DIREKT an der
Unterstuetzung bzw. am Widerstand. Ueber 69 geloggte Orders:
SL steht AM Pivot 3 ( 4 %) <- Pivot ist maßgeblich
auf 2,2×ATR gekappt 41 (59 %)
auf 1,8×ATR geweitet 25 (36 %)
In 95 % dominiert also der ATR-Deckel — aber in 4 % ist der Stop exakt das
Level, an dem der Markt ohnehin am haeufigsten wickt.
WARUM NEU MESSEN. `backtest_sl_method.py` hat die Verankerung schon einmal
geprueft und "gedeckelt alle identisch, die Verankerung ist Rauschen" ergeben —
aber mit einem ANDEREN Exit-Modell und vor dem kanonischen `exit_model.LIVE`
(Trail 1,0 seit 31.07., trail_start 1,3 seit 20.08.). Ein Befund, der eine
Live-Schutzlinie rechtfertigt, gehoert auf dem heutigen Exit nachgerechnet.
AUFBAU. Squeeze-Einstiege (reproduzierbar, echte `_squeeze_one`), ruhende Order
am Level mit Fill bei BERUEHRUNG, kanonischer Exit, Echtkosten, 2 Halbjahre,
95-%-Bootstrap. Variiert wird AUSSCHLIESSLICH die SL-Methode:
A) PIVOT wie live: naechster M15-Pivot ∓ Puffer, geklemmt auf 1,82,2×ATR
B) FIX schlicht 2,0×ATR — ohne jeden Level-Bezug
⚠ Feste, GETEILTE Einstiegsliste: beide Varianten laufen auf DENSELBEN Trades.
Ein sequentieller Vergleich waere ungueltig — ein anderer SL laesst ANDERE
Trades in den einen Slot (die Falle vom 14.08.).
⚠ M15-Pivots werden aus den M5-Bars aggregiert (3 M5 = 1 M15), Pivot-Fenster
k=3 wie in `mt5_utils.pivot_low/high`. Das ist eine NAEHERUNG der Live-Funktion —
sie liest direkt M15-Bars. Fuer den VERGLEICH der beiden Methoden ist das
unkritisch, fuer die Absolutwerte nicht.
REGEL VORAB FIXIERT: umgestellt wird nur, wenn B in BEIDEN Halbjahren
mindestens gleichauf liegt (OeR nicht schlechter als A minus ein
Standardfehler) UND der Worst-Case nicht groesser wird. Ist B schlechter,
bleibt die Verankerung — dann ist der beobachtete Effekt der Preis fuer einen
Vorteil an anderer Stelle.
Aufruf: python backtest_sl_pivot.py [bars]
"""
import sys
import MetaTrader5 as mt5
from backtest_cost_gate import kennzahlen
from core.exit_model import LIVE, simulate
from core.squeeze_scan import atr_series, scan
BOX_N, K, MULT_BOX, ATRMIN = 10, 0.1, 2.5, 0.06
SL_MIN, SL_MAX = 1.8, 2.2 # INIT_SL_MIN_ATR / INIT_SL_MAX_ATR
PIV_K = 3
BUF = 0.02 # Puffer unter/ueber dem Pivot (Groessenordnung live)
def m15_pivots(H, L):
"""M15-Pivots aus M5-Bars aggregiert → (Index_M5, Preis) je Pivot."""
n = len(H) // 3
hi = [max(H[3 * i:3 * i + 3]) for i in range(n)]
lo = [min(L[3 * i:3 * i + 3]) for i in range(n)]
ph, pl = [], []
for j in range(PIV_K, n - PIV_K):
if hi[j] == max(hi[j - PIV_K:j + PIV_K + 1]):
ph.append((3 * (j + PIV_K), hi[j])) # erst k Bars spaeter bekannt
if lo[j] == min(lo[j - PIV_K:j + PIV_K + 1]):
pl.append((3 * (j + PIV_K), lo[j]))
return ph, pl
def sl_pivot(i, d, entry, atr, ph, pl):
"""Live-Regel: naechster Pivot ∓ Puffer, dann auf 1,82,2×ATR geklemmt."""
kand = [p for (j, p) in (pl if d > 0 else ph) if j <= i and
((p < entry) if d > 0 else (p > entry))]
if kand:
piv = max(kand) if d > 0 else min(kand)
sl = (piv - BUF) if d > 0 else (piv + BUF)
else:
sl = entry * (1 - 0.012) if d > 0 else entry * (1 + 0.012)
dist = (entry - sl) if d > 0 else (sl - entry)
dist = min(max(dist, SL_MIN * atr), SL_MAX * atr)
return dist
def lauf(ein, H, L, C, SP, ende, modus, ph, pl):
rs = []
for j, d, lvl, atr in ein:
f = None
for x in range(j, min(j + 13, ende)):
if (H[x] >= lvl) if d > 0 else (L[x] <= lvl):
f = x
break
if f is None or f + 1 >= ende:
continue
dist = sl_pivot(f, d, lvl, atr, ph, pl) if modus == "pivot" else 2.0 * atr
# SL-Distanz ueber `sl_atr` in EXAKT die kanonische Sim geben.
par = LIVE.with_(sl_atr=dist / atr)
r = simulate(lvl, d, atr, H, L, C, f + 1, par, timestop=True, use_tp=True)
rs.append(r - ((SP[f] if SP[f] > 0 else 0.0225) / atr))
return rs
def zeig(lbl, k):
if not k:
print(" {:<22} --".format(lbl)); return None
ci = k.get("ci"); m = ""
if ci:
m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
+ ("" if ci[0] * ci[1] > 0 else " ~0"))
print(" {:<22} n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f} worst={:+.2f}{}"
.format(lbl, k["n"], k["wr"], k["oer"], k["pf"], k.get("worst", 0), m))
return k
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 60000
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)
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]
SP = [float(b["spread"]) * point for b in bars]
A = atr_series(H, L, C)
ph, pl = m15_pivots(H, L)
N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1
def entries(lo, hi):
out = []
scan(H, L, C, A, SP, 1.0, lo, hi, MULT_BOX, box_n=BOX_N, k=K,
atr_min=ATRMIN, exit_fn=lambda *a: 0.0,
on_entry=lambda j, d, lvl, atr, komp: (
out.append((j, d, lvl, atr)) if komp else None))
return out
e1, e2 = entries(0, mid), entries(mid, ende)
print("=" * 92)
print(" SL-METHODE: Pivot-verankert gegen fix 2,0xATR — {} M5 ({} Bars)".format(sym, N))
print(" gleiche Einstiege, NUR die SL-Methode variiert · {} M15-Pivots".format(len(ph) + len(pl)))
print("=" * 92)
erg = {}
for modus, titel in (("pivot", "A) PIVOT (= live)"), ("fix", "B) FIX 2,0xATR")):
print("\n " + titel)
print(" " + "-" * 88)
erg[modus] = (zeig("H1", kennzahlen(lauf(e1, H, L, C, SP, ende, modus, ph, pl))),
zeig("H2", kennzahlen(lauf(e2, H, L, C, SP, ende, modus, ph, pl))))
print("\n " + "=" * 88)
a1, a2 = erg["pivot"]; b1, b2 = erg["fix"]
if a1 and b1:
print(" Differenz FIX PIVOT: H1 {:+.3f} R H2 {:+.3f} R"
.format(b1["oer"] - a1["oer"], b2["oer"] - a2["oer"]))
print(" Worst-Case: PIVOT {:+.2f}/{:+.2f} FIX {:+.2f}/{:+.2f}"
.format(a1.get("worst", 0), a2.get("worst", 0),
b1.get("worst", 0), b2.get("worst", 0)))
print(" REGEL: umstellen nur, wenn FIX beidhaelftig mindestens gleichauf")
print(" liegt UND der Worst-Case nicht groesser wird.")
if __name__ == "__main__":
main()