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>
141 lines
5.9 KiB
Python
141 lines
5.9 KiB
Python
#!/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 (|h1−h2| ≤ tol×ATR),
|
||
dazwischen ein Tal (Nackenlinie) mind. `depth`×ATR tiefer; Einstieg wenn der Kurs
|
||
FRISCH unter die Nackenlinie bricht (C[i] < neck ≤ C[i−1]).
|
||
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()
|