Files
AH-Oil-Trader/backtest_doubletop.py
T
Axel HocksandClaude Opus 4.8 75d28827e8 Initial commit: Oil Trading Bot (MT5, WTI)
Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:29:23 +02:00

141 lines
5.9 KiB
Python
Raw 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.
#!/usr/bin/env python3
"""Doppeltop / Doppelboden als SIGNAL — Backtest (2026-07-20, User-Idee „Muster-
erkennung"). Das am saubersten mechanisch definierbare Umkehrmuster; nutzt die
bestehende Pivot-Erkennung aus `structure.py` (kausal, Bar für Bar → Signal == was
die Anzeige sehen würde).
Definition:
Doppeltop (→ SHORT): die letzten zwei Swing-HOCHS ~gleich hoch (|h1h2| ≤ tol×ATR),
dazwischen ein Tal (Nackenlinie) mind. `depth`×ATR tiefer; Einstieg wenn der Kurs
FRISCH unter die Nackenlinie bricht (C[i] < neck ≤ C[i1]).
Doppelboden (→ LONG): symmetrisch (zwei ~gleiche Tiefs, Bruch über die Nackenlinie).
Muster verfällt, wenn der Bruch nicht binnen `stale` Bars nach dem 2. Swing kommt.
Sequentielle 1-Positions-Sim, Live-Exit (SL 2,0×ATR + Trailing 1,5 + BE 1,3),
Echtkosten = Bar-Spread/ATR. M30, 2 Halbjahre. Maßstab: Squeeze ØR +0,14…+0,23 &
PF>1 in BEIDEN Hälften. Verdict = beidhälftig robust über die Toleranz-Varianten.
"""
import sys
import MetaTrader5 as mt5
from core.structure import _pivots
_MAXH = 96; _ATRMIN = 0.06; _COOL = 3; _LOOK = 300; _STALE = 40; _DEPTH = 0.5
def _atr_series(H, L, C, p=14):
t = [0.0]
for i in range(1, len(C)):
t.append(max(H[i]-L[i], abs(H[i]-C[i-1]), abs(L[i]-C[i-1])))
return [(sum(t[max(1, i-p+1):i+1])/max(1, len(t[max(1, i-p+1):i+1]))) if i else None
for i in range(len(C))]
def st(Rs):
if not Rs: return None
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
return dict(n=n, wr=100*w/n, oR=s/n, pf=(up/dn if dn > 0 else 9.99), sum=s)
def line(lbl, s):
if not s: return f" {lbl:<26} — (keine Trades)"
return (f" {lbl:<26} n={s['n']:>4} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+6.0f}")
def sim(entry, d, atr, H, L, C, j0):
eff = entry - d*2.0*atr; hw = entry
end = min(j0+_MAXH, len(C)-1); exit_px = C[end]; exit_j = end
for j in range(j0, end+1):
hj, lj = H[j], L[j]
if (lj <= eff) if d > 0 else (hj >= eff):
return (eff-entry)*d/atr, j
hw = max(hw, hj) if d > 0 else min(hw, lj)
prof = (C[j]-entry)*d
if prof >= 0.3*atr:
cand = hw - d*1.5*atr
if prof >= 1.3*atr: cand = max(cand, entry) if d > 0 else min(cand, entry)
eff = max(eff, cand) if d > 0 else min(eff, cand)
return (exit_px-entry)*d/atr, exit_j
def _neckline(pivots, i, atr, tol, want_top):
"""Prüft, ob am Bar i ein Doppeltop (want_top) bzw. -boden fertig ist.
Gibt (True, neckline) zurück, wenn die letzten 2 gleichseitigen Pivots das
Muster bilden. KEIN Bruch-Check hier — nur die Formation."""
kind = "H" if want_top else "L"
same = [p for p in pivots if p[2] == kind]
opp = [p for p in pivots if p[2] != kind]
if len(same) < 2:
return False, None
p1, p2 = same[-2], same[-1]
mids = [q for q in opp if p1[0] < q[0] < p2[0]]
if not mids:
return False, None
# Nackenlinie = Extrem zwischen den beiden gleichseitigen Pivots
neck = (min(mids, key=lambda q: q[1]) if want_top else max(mids, key=lambda q: q[1]))
if abs(p1[1] - p2[1]) > tol * atr: # Schultern ~gleich hoch?
return False, None
depth = (min(p1[1], p2[1]) - neck[1]) if want_top else (neck[1] - max(p1[1], p2[1]))
if depth < _DEPTH * atr: # echtes Tal/Berg dazwischen?
return False, None
if i - p2[0] > _STALE: # zu alt → verfallen
return False, None
return True, neck[1]
def run(H, L, C, A, SP, lo, hi, tol):
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
Rs = []; i = max(lo, _LOOK)
while i < hi:
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
piv = _pivots(H[max(0, i-_LOOK):i+1], L[max(0, i-_LOOK):i+1], 3)
# Index-Offset korrigieren (piv-Indizes sind fensterrelativ)
off = max(0, i-_LOOK)
piv = [(idx+off, pr, k) for (idx, pr, k) in piv]
d = 0; neck = None
ok_t, neck_t = _neckline(piv, i, atr, tol, True)
if ok_t and C[i] < neck_t <= C[i-1]: # frischer Bruch UNTER Nackenlinie
d = -1
else:
ok_b, neck_b = _neckline(piv, i, atr, tol, False)
if ok_b and C[i] > neck_b >= C[i-1]: # frischer Bruch ÜBER Nackenlinie
d = 1
if d == 0:
i += 1; continue
r, xj = sim(C[i], d, max(atr, _ATRMIN), H, L, C, i+1)
Rs.append(r - cost(i, max(atr, _ATRMIN)))
i = xj + _COOL
return Rs
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_M30, 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); N = len(C); mid = N//2
print("="*90)
print(f" Doppeltop/-boden als SIGNAL — {sym} M30 ({N} Bars, seq. Sim, Live-Exit, Echtkosten)")
print(f" Einstieg = Nackenlinien-Bruch. Maßstab: Squeeze ØR +0,14…+0,23 & PF>1 BEIDE Hälften.")
print("="*90)
for tol in (0.3, 0.6, 1.0):
s1 = st(run(H, L, C, A, SP, 0, mid, tol))
s2 = st(run(H, L, C, A, SP, mid, N-_MAXH-1, tol))
print(f"\n Schulter-Toleranz {tol}×ATR:")
print(line("H1 (alt)", s1))
print(line("H2 (neu)", s2))
ok = (s1 and s2 and s1['oR'] > 0 and s2['oR'] > 0 and s1['pf'] > 1 and s2['pf'] > 1)
print(f" → {'ROBUST (beide Hälften positiv)' if ok else 'fällt durch'}")
print()
if __name__ == "__main__":
main()