Files
AH-Oil-Trader/backtest_adverse_nosignal.py
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

138 lines
6.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.
#!/usr/bin/env python3
"""Früh-Ausstieg NUR bei Trades OHNE Signal-Deckung (2026-07-23, User-Idee nach dem
57,34-€-Trade: manueller LONG gegen WARTEN, kein Squeeze/Signal-Flip beteiligt).
Der pauschale Früh-Ausstieg (`_ADVERSE_EXIT_ATR`) ist AUS — er kappte gemessen auch
gute Trend-Trades bei normalen Rücksetzern. Hypothese: bei Trades OHNE Signal-Deckung
(Totband/WARTEN-Zone — die B0-Checkliste zeigt live 39% WR/166€ für diese Klasse)
könnte ein früher Adverse-Exit netto helfen, weil diese Population ohnehin schwächer
ist und weniger "gute" Trades träfe.
Zwei Populationen (EMA12/50, Totband wie live):
SIG = frischer EMA-Cross MIT Signal (|diff|≥Totband) — die normale, validierte Klasse.
NOSIG = Entry INNERHALB der Totband-Zone (WARTEN), Richtung = Vorzeichen des (kleinen)
EMA-Diffs — Proxy für einen manuellen Trade "in Richtung der Drift, ohne
bestätigtes Signal" (genau der Fall des 57-€-Trades).
Je Population: BASE-Exit (Live: SL 2,0×ATR+Trailing 1,5+BE 1,3) vs. BASE+Adverse-Exit
(schließt sofort, wenn der Kurs X×ATR gegen den Einstieg läuft, bevor SL/Trail greift).
2 Halbjahre, Echtkosten. Verdict: Adverse-Exit nur für NOSIG einbauen, wenn er dort in
BEIDEN Hälften verbessert — SIG bleibt unangetastet (bereits gemessen: schadet dort).
"""
import sys
import MetaTrader5 as mt5
from core.wave_rec import _EMA_FAST, _EMA_SLOW, _TREND_DEADBAND
_MAXH = 288; _ATRMIN = 0.06; _COOL = 12
def _ema(C, p):
k = 2.0 / (p + 1); e = C[0]; out = [e]
for x in C[1:]:
e += k * (x - e); out.append(e)
return out
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 sim(entry, d, atr, H, L, C, j0, adverse_x=None, trail=1.5, trail_on=0.3, be_on=1.3):
"""adverse_x: None=aus. Sonst schließt sofort, sobald Kurs adverse_x×ATR GEGEN den
Einstieg lief (VOR dem normalen SL/Trailing) — echte Intrabar-Prüfung je Bar."""
eff = entry - d*2.0*atr; hw = entry
end = min(j0+_MAXH, len(C)-1); exit_px = C[end]
for j in range(j0, end+1):
hj, lj = H[j], L[j]
if adverse_x:
adverse_px = entry - d*adverse_x*atr
if (lj <= adverse_px) if d > 0 else (hj >= adverse_px):
return (adverse_px-entry)*d/atr
if (lj <= eff) if d > 0 else (hj >= eff): exit_px = eff; break
hw = max(hw, hj) if d > 0 else min(hw, lj)
prof = (C[j]-entry)*d
if prof >= trail_on*atr:
cand = hw - d*trail*atr
if prof >= be_on*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
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:<20} —"
return (f" {lbl:<20} 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 run(H, L, C, A, SP, EF, ES, lo, hi, pop, adverse_x):
"""pop: 'sig' (frischer Cross, |diff|>=dead) oder 'nosig' (WARTEN-Zone, |diff|<dead,
Richtung=Vorzeichen des Diffs)."""
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
Rs = []; i = max(lo, _EMA_SLOW + 2)
state_prev = None # (sig|nosig, dir) des letzten Zustands, für "fresh"-Erkennung
while i < hi:
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
diff = EF[i] - ES[i]; dead = _TREND_DEADBAND * atr
if abs(diff) >= dead:
cur = ("sig", 1 if diff > 0 else -1)
elif diff != 0:
cur = ("nosig", 1 if diff > 0 else -1)
else:
cur = (None, 0)
fresh = cur != state_prev and cur[0] is not None
state_prev = cur
if not fresh or cur[0] != pop:
i += 1; continue
d = cur[1]
entry = C[i]
r, = [sim(entry, d, max(atr, _ATRMIN), H, L, C, i+1, adverse_x)]
Rs.append(r - cost(i, max(atr, _ATRMIN)))
i += _COOL
return Rs
def main():
n = 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)
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); EF = _ema(C, _EMA_FAST); ES = _ema(C, _EMA_SLOW)
N = len(C); mid = N//2
print("="*92)
print(f" Früh-Ausstieg NUR ohne Signal-Deckung — {sym} M5 ({N} Bars, 2 Halbjahre, Echtkosten)")
print(f" SIG = frischer EMA-Cross (Signal) · NOSIG = Totband/WARTEN-Zone (kein Signal)")
print("="*92)
for pop_lbl, pop in (("SIG (mit Signal)", "sig"), ("NOSIG (ohne Signal, WARTEN-Zone)", "nosig")):
print(f"\n{pop_lbl}:")
for lbl, lo, hi in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N-_MAXH-1)):
base = st(run(H, L, C, A, SP, EF, ES, lo, hi, pop, None))
print(f"\n {lbl}:")
print(line("BASE (kein Adverse)", base))
for x in (0.5, 0.75, 1.0, 1.5):
mod = st(run(H, L, C, A, SP, EF, ES, lo, hi, pop, x))
d_ = (mod["sum"] - base["sum"]) if (mod and base) else None
print(line(f"Adverse X={x}×ATR", mod)
+ (f" Δ={d_:+.0f}" if d_ is not None else ""))
print(f"\n Verdict: Adverse-Exit für NOSIG nur übernehmen, wenn EINE Schwelle in")
print(f" BEIDEN Hälften klar über BASE liegt. SIG bleibt so oder so unangetastet.")
if __name__ == "__main__":
main()