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>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user