User: "hast du Optimierungsvorschlaege um den Tagestrend mehr Gewicht zu
verleihen?" Vier konkrete Vorschlaege, alle VORHER gemessen statt gebaut.
backtest_tagestrend.py (neu): 60k M5 = 244 Handelstage, voller Live-Gate-Stack
(echtes `_build` + Breakout k=0,3 + `_room_gate`), kanonischer Exit,
Echtkosten, 2 Halbjahre, 95-%-Bootstrap, Schwellen 0,15 / 0,30 / 0,60 %.
Tagestrend = Kurs gegen die TAGESEROEFFNUNG (Berlin, DST-korrekt) - das, was
ein Mensch sieht, und es braucht keinen 16.000-Bar-Vorlauf.
H1 OeR/SR H2 OeR/SR n
BASIS -0,062 / -34,9 -0,107 / -88,9 563/831
A VETO -0,058 / -31,5 -0,114 / -89,5 543/785
B NUR-MIT -0,010 / -4,8 -0,126 / -86,1 483/683
C SPAET (3,5->5,0) -0,031 / -20,3 -0,089 / -84,8 656/953
D PULLBACK -0,008 / -1,6 -0,012 / -3,5 196/291
A und B: in H2 an ALLEN DREI Schwellen schlechter als BASIS; die von ihnen
entfernte Gruppe kippt zwischen den Haelften (+0,032 bzw. +0,208 in H2) - sie
werfen also teils die BESSEREN Trades weg. Vierte Messung dieser Klasse nach
Hoeher-TF-Winkel, H1-Staerke und Konfluenz-Strafe.
C sieht beidhaelftig besser aus und ist trotzdem kein Beleg: die zusaetzlich
genommenen Trades sind H1 -0,134 / H2 +0,060, also gegenlaeufig. Aufloesung in
der Mechanik: die Sim hat EINEN Slot, ein Extra-Trade verschiebt die GANZE
Folge. C ist keine Obermenge von BASIS, sondern eine andere Trade-Folge - der
scheinbare Gewinn ist ein Selektionseffekt (die Falle vom 14.08.).
D hat als EINZIGE die richtige Signatur: beidhaelftig besser an allen drei
Schwellen, und die ENTFERNTE Gruppe ist an allen SECHS Stellen negativ
(-0,068/-0,060/-0,063 und -0,072/-0,080/-0,053). Trotzdem kein Bau: jedes
Intervall enthaelt die Null, und der SR-Sprung kommt fast vollstaendig daher,
dass D nur noch 35 % der Trades nimmt - Handelsvermeidung, nicht Edge (Muster
aus backtest_metalabel.py).
INHALTLICHE ANTWORT im Vergleich C gegen D: wenn der Tagestrend etwas
beitraegt, dann NICHT als Erlaubnis fuer den spaeten Einstieg, sondern als
Kontext fuer den RUECKSETZER. Deckt sich mit dem einzigen belegten
Einstiegs-Effekt (Pullback-Bonus +15) und mit dem invertierten Winkel-Befund
vom 04.08.
Ehrlichster Satz zum Lauf: die BASIS-Population ist mit -0,062/-0,107 selbst
negativ. Kein Umgang mit dem Tagestrend macht daraus einen Edge.
29. verworfener Signal-Eingriff.
METHODEN-BEFUND ueber den Lauf hinaus: das Werkzeug fuer den gepaarten
Vergleich war hier das FALSCHE - und es hat es selbst gezeigt (Delta +0,000,
KI von exakt 0,000 bis 0,000 in ALLEN acht Zellen). Diese Varianten aendern
nicht den EXIT, sondern WELCHE Einstiege genommen werden; auf gemeinsamen
Faellen sind die R-Werte per Konstruktion identisch.
Regel: gepaart fuer EXIT-Varianten, Auswertung der entfernten bzw.
hinzugefuegten Gruppe fuer AUSWAHL-Varianten. Ein Intervall von exakt
[0,000 .. 0,000] ist kein Ergebnis, sondern die Fehlermeldung eines falsch
gewaehlten Tests.
Nachtrag: die Nachricht des ersten Commit-Versuchs wurde von der
dokumentierten Backtick-Falle beschaedigt (Kommando-Substitution in der
Bash-Zeile). Diese Fassung kommt ueber eine Datei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
284 lines
12 KiB
Python
284 lines
12 KiB
Python
"""Bringt es etwas, dem TAGESTREND mehr Gewicht zu geben? (2026-08-20)
|
|
|
|
ANLASS (User): "hast du Optimierungsvorschlaege, um den Tagestrend mehr Gewicht
|
|
zu verleihen?" - nach dem Befund, dass die Empfehlung heute um 09:13 LONG sagte
|
|
und seit 12:58 wegen Ueberdehnung schweigt, waehrend der Tag +2,89 % lief.
|
|
|
|
⚠ HOHE BEWEISLAST. Signal-Eingriffe sind in diesem Projekt 28-mal gescheitert,
|
|
darunter dreimal genau diese Klasse (Hoeher-TF-Winkel-Filter, H1-Staerke-Filter,
|
|
Konfluenz-Strafe - alle gemessen SCHLECHTER). Deshalb wird hier NICHTS gebaut,
|
|
sondern zuerst gemessen.
|
|
|
|
VIER VARIANTEN, alle auf DERSELBEN Population (voller Live-Gate-Stack: echtes
|
|
`_build` + Breakout-Bestaetigung k=0,3 + `_room_gate`, Reversal aus):
|
|
|
|
BASIS wie live
|
|
A VETO Signal GEGEN den Tagestrend verwerfen - die einzige Form, die
|
|
im Projekt je robust war (Ausrichtungs-Split: gegen das Signal
|
|
-6,60 EUR/Lot, beidhaelftig)
|
|
B NUR-MIT nur Signale MIT dem Tagestrend (strenger als A: auch neutrale
|
|
Tage fallen weg)
|
|
C SPAET `_STRETCH_MAX` 3,5 -> 5,0, solange der Tagestrend in
|
|
Signalrichtung zeigt. Das ist EXAKT die heutige Lage.
|
|
D PULLBACK nur MIT dem Tagestrend UND nur bei kleiner Ueberdehnung
|
|
(|stretch| <= 1,5) - nicht spaet kaufen, sondern den
|
|
Ruecksetzer nehmen. Nutzt den im Projekt BELEGTEN
|
|
Pullback-Effekt ("tiefer Pullback" +15).
|
|
|
|
TAGESTREND = Kurs gegen die TAGESEROEFFNUNG (Berlin-Kalendertag, DST-korrekt).
|
|
Bewusst so und nicht als D1-EMA: das ist, was ein Mensch sieht, wenn er sagt
|
|
"der Tagestrend ist aufwaerts" - und es braucht keinen 16.000-Bar-Vorlauf.
|
|
Schwelle 0,30 % vom Kurs; 0,15 / 0,60 % werden als Nachbarn mitgeprueft.
|
|
⚠ KAUSAL: die Eroeffnung des LAUFENDEN Tages ist jederzeit bekannt, es fliesst
|
|
nichts aus der Zukunft ein.
|
|
|
|
REGEL VORAB FIXIERT: eine Variante wird nur dann zum Bauen empfohlen, wenn sie
|
|
(1) in BEIDEN Halbjahren einen hoeheren OeR liefert als BASIS,
|
|
(2) ihr GEPAARTES 95-%-Intervall (gemeinsame Einstiege - die Falle vom 14.08.)
|
|
die Null ausschliesst, und
|
|
(3) die Nachbar-Schwellen nicht kippen.
|
|
|
|
Aufruf: python backtest_tagestrend.py [bars]
|
|
"""
|
|
import datetime as dt
|
|
import sys
|
|
from bisect import bisect_left, bisect_right
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import MetaTrader5 as mt5
|
|
|
|
import core.wave_rec as WR
|
|
from backtest_breakout_gated import (_ATRMIN, _ATR_FLOOR_PB, _COOL, _PIV_K,
|
|
_PIV_LOOK, _TIMEOUT_BARS, _TU,
|
|
_atr_series, _htf_sign)
|
|
from backtest_cost_gate import kennzahlen
|
|
from core.analysis import calc_trend_angle
|
|
from core.exit_model import LIVE, simulate
|
|
from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW,
|
|
_ema_series)
|
|
|
|
BERLIN = ZoneInfo("Europe/Berlin")
|
|
BROKER = dt.timezone(dt.timedelta(hours=3))
|
|
K_BREAKOUT = 0.3
|
|
PULLBACK_MAX = 1.5
|
|
STRETCH_LOCKER = 5.0
|
|
MODI = (("basis", "BASIS (wie live)"),
|
|
("veto", "A) VETO gegen Tagestrend"),
|
|
("nurmit", "B) NUR MIT Tagestrend"),
|
|
("spaet", "C) Spaet-Einstieg bis 5,0"),
|
|
("pullback", "D) Pullback im Tagestrend"))
|
|
|
|
|
|
def _tagesoeffnung(T, C):
|
|
"""Index -> Eroeffnungskurs SEINES Berliner Kalendertags (kausal).
|
|
|
|
⚠ MT5 liefert in `time` die Broker-WALLCLOCK, keinen echten UTC-Epoch -
|
|
genau der Fehler, der `analyze_tageszeit.py` am 05.08. um 3 h verschoben hat.
|
|
"""
|
|
out = [None] * len(C)
|
|
tag = op = None
|
|
for i, ts in enumerate(T):
|
|
d = dt.datetime.fromtimestamp(ts, dt.timezone.utc) \
|
|
.replace(tzinfo=BROKER).astimezone(BERLIN).date()
|
|
if d != tag:
|
|
tag, op = d, C[i]
|
|
out[i] = op
|
|
return out
|
|
|
|
|
|
def _tagestrend(C, OP, i, schwelle):
|
|
if not OP[i]:
|
|
return 0
|
|
rel = (C[i] - OP[i]) / OP[i]
|
|
return 0 if abs(rel) < schwelle else (1 if rel > 0 else -1)
|
|
|
|
|
|
def run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, OP,
|
|
lo, hi, modus, schwelle):
|
|
Rs, keys = [], []
|
|
pend = None
|
|
i = max(lo, _PIV_LOOK + _EMA_SLOW + 5)
|
|
while i < hi:
|
|
atr = A[i]
|
|
if not atr or atr < _ATRMIN:
|
|
i += 1
|
|
continue
|
|
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
|
|
a1 = bisect_left(PH, i - _PIV_LOOK)
|
|
b1 = bisect_right(PH, i - _PIV_K)
|
|
a2 = bisect_left(PL, i - _PIV_LOOK)
|
|
b2 = bisect_right(PL, i - _PIV_K)
|
|
w._pb_levels = {"ph": [C[j] for j in PH[a1:b1]],
|
|
"pl": [C[j] for j in PL[a2:b2]]}
|
|
|
|
td = _tagestrend(C, OP, i, schwelle)
|
|
WR._STRETCH_MAX = 3.5
|
|
if modus == "spaet" and td:
|
|
stretch = (C[i] - ES[i]) / atr
|
|
if (td > 0 and stretch > 0) or (td < 0 and stretch < 0):
|
|
WR._STRETCH_MAX = STRETCH_LOCKER
|
|
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
|
|
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
|
|
WR._STRETCH_MAX = 3.5
|
|
sig = rec.get("signal")
|
|
|
|
if K_BREAKOUT > 0 and sig in ("LONG", "SHORT"):
|
|
d = 1 if sig == "LONG" else -1
|
|
if pend is None or pend["dir"] != d:
|
|
pend = {"dir": d, "level": C[i] + d * K_BREAKOUT * atr,
|
|
"invalid": C[i] - d * K_BREAKOUT * atr,
|
|
"confirmed": False, "bar": i}
|
|
if not pend["confirmed"]:
|
|
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
|
|
gegen = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
|
|
if broke:
|
|
pend["confirmed"] = True
|
|
else:
|
|
if gegen or (i - pend["bar"]) > _TIMEOUT_BARS:
|
|
pend = {"dir": d, "level": C[i] + d * K_BREAKOUT * atr,
|
|
"invalid": C[i] - d * K_BREAKOUT * atr,
|
|
"confirmed": False, "bar": i}
|
|
i += 1
|
|
continue
|
|
elif sig == "WARTEN":
|
|
pend = None
|
|
if sig not in ("LONG", "SHORT"):
|
|
i += 1
|
|
continue
|
|
rec = w._room_gate(rec, C[i])
|
|
if rec.get("signal") not in ("LONG", "SHORT"):
|
|
i += 1
|
|
continue
|
|
|
|
d = 1 if rec["signal"] == "LONG" else -1
|
|
if modus == "veto" and td and td != d:
|
|
i += 1
|
|
pend = None
|
|
continue
|
|
if modus in ("nurmit", "pullback") and td != d:
|
|
i += 1
|
|
pend = None
|
|
continue
|
|
if modus == "pullback" and abs((C[i] - ES[i]) / atr) > PULLBACK_MAX:
|
|
i += 1
|
|
pend = None
|
|
continue
|
|
|
|
a = max(atr, _ATRMIN)
|
|
r, xj = simulate(C[i], d, a, H, L, C, i + 1, LIVE,
|
|
timestop=True, use_tp=True, ret_bar=True)
|
|
Rs.append(r - ((SP[i] if SP[i] > 0 else 0.0225) / a))
|
|
keys.append(i)
|
|
pend = None
|
|
i = xj + _COOL
|
|
return Rs, keys
|
|
|
|
|
|
def zeig(lbl, k):
|
|
if not k:
|
|
print(" {:<28} --".format(lbl))
|
|
return
|
|
ci = k.get("ci")
|
|
m = ""
|
|
if ci:
|
|
m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
|
|
+ ("" if ci[0] * ci[1] > 0 else " ~0"))
|
|
print(" {:<28} n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}"
|
|
.format(lbl, k["n"], k["wr"], k["oer"], k["pf"], m))
|
|
|
|
|
|
def main():
|
|
n = int(sys.argv[1]) if len(sys.argv) > 1 else 60000
|
|
schwellen = [0.0030, 0.0015, 0.0060]
|
|
mt5.initialize()
|
|
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
|
|
if mt5.symbol_info(c)), None)
|
|
b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
|
point = mt5.symbol_info(sym).point
|
|
mt5.shutdown()
|
|
T = [int(x["time"]) for x in b]
|
|
H = [float(x["high"]) for x in b]
|
|
L = [float(x["low"]) for x in b]
|
|
C = [float(x["close"]) for x in b]
|
|
SP = [float(x["spread"]) * point for x in b]
|
|
A = _atr_series(H, L, C)
|
|
EF = _ema_series(C, _EMA_FAST)
|
|
ES = _ema_series(C, _EMA_SLOW)
|
|
OP = _tagesoeffnung(T, C)
|
|
AD = [calc_trend_angle(C[max(0, i - _ANGLE_LR):i + 1]) if i >= _ANGLE_LR else 90.0
|
|
for i in range(len(C))]
|
|
HTF = [_htf_sign(C, i, 6, A[i]) for i in range(len(C))]
|
|
H1 = [_htf_sign(C, i, 12, A[i]) for i in range(len(C))]
|
|
PH = [j for j in range(_PIV_K, len(C) - _PIV_K)
|
|
if C[j] == max(C[j - _PIV_K:j + _PIV_K + 1])]
|
|
PL = [j for j in range(_PIV_K, len(C) - _PIV_K)
|
|
if C[j] == min(C[j - _PIV_K:j + _PIV_K + 1])]
|
|
|
|
w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
|
|
w.set_reversal_enabled(False)
|
|
w.set_entry_room(0.6)
|
|
w.set_breakout_k(0.0)
|
|
|
|
N = len(C)
|
|
mid = N // 2
|
|
ende = N - LIVE.max_hold - 1
|
|
tage = len({dt.datetime.fromtimestamp(t, dt.timezone.utc)
|
|
.replace(tzinfo=BROKER).astimezone(BERLIN).date() for t in T})
|
|
print("=" * 104)
|
|
print(" TAGESTREND MEHR GEWICHT? - {} · {} M5-Bars · {} Handelstage"
|
|
.format(sym, N, tage))
|
|
print(" voller Live-Gate-Stack (echtes _build · Breakout k=0,3 · _room_gate)"
|
|
" · kanonischer Exit · Echtkosten")
|
|
print("=" * 104)
|
|
|
|
for schwelle in schwellen:
|
|
titel = "Tagestrend-Schwelle {:.2f} %".format(schwelle * 100)
|
|
if schwelle != schwellen[0]:
|
|
titel += " (Nachbar-Probe)"
|
|
print("\n " + titel)
|
|
print(" " + "-" * 100)
|
|
erg = {}
|
|
for modus, lbl in MODI:
|
|
r1, k1 = run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, OP,
|
|
0, mid, modus, schwelle)
|
|
r2, k2 = run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, OP,
|
|
mid, ende, modus, schwelle)
|
|
erg[modus] = (dict(zip(k1, r1)), dict(zip(k2, r2)))
|
|
zeig("H1 " + lbl, kennzahlen(r1))
|
|
zeig("H2 " + lbl, kennzahlen(r2))
|
|
print()
|
|
# ⚠⚠ KEIN gepaarter Test hier - er waere DEGENERIERT. Die Varianten
|
|
# aendern nicht den EXIT, sondern WELCHE Einstiege genommen werden;
|
|
# auf den gemeinsamen Faellen sind die R-Werte per Konstruktion
|
|
# IDENTISCH (erster Lauf: Delta +0,000, KI [0,000 .. 0,000] in allen
|
|
# acht Zellen). `paarweise` ist das Werkzeug fuer Exit-Varianten
|
|
# (Falle vom 14.08.), nicht fuer Auswahl-Varianten.
|
|
# Entscheidend ist stattdessen, WAS der Eingriff wegnimmt bzw.
|
|
# hinzufuegt - Muster wie in `backtest_htf_vola.py`.
|
|
print(" WAS DER EINGRIFF WEGNIMMT / HINZUFUEGT (die eigentliche Frage):")
|
|
b1, b2 = erg["basis"]
|
|
for modus, lbl in MODI[1:]:
|
|
v1, v2 = erg[modus]
|
|
for hl, bb, vv in (("H1", b1, v1), ("H2", b2, v2)):
|
|
weg = [bb[k] for k in bb if k not in vv]
|
|
neu_ = [vv[k] for k in vv if k not in bb]
|
|
gruppe, txt = (neu_, "hinzugefuegt") if len(neu_) > len(weg) else (weg, "entfernt")
|
|
kk = kennzahlen(gruppe)
|
|
if not kk:
|
|
print(" {:<28} {} {} n={} - zu wenig"
|
|
.format(lbl, hl, txt, len(gruppe)))
|
|
continue
|
|
ci = kk.get("ci"); m = ""
|
|
if ci:
|
|
m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
|
|
+ ("" if ci[0] * ci[1] > 0 else " ~0"))
|
|
print(" {:<28} {} {:<12} n={:>4} WR={:>3.0f}% OeR={:+.3f}{}"
|
|
.format(lbl, hl, txt, kk["n"], kk["wr"], kk["oer"], m))
|
|
print("\n " + "=" * 100)
|
|
print(" REGEL: bauen nur, wenn beidhaelftig besser UND das gepaarte")
|
|
print(" Intervall die Null ausschliesst UND die Nachbarn nicht kippen.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|