From d7dc997eee3d03a69df9e6614341c8a50f023d70 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Wed, 19 Aug 2026 20:52:37 +0200 Subject: [PATCH] RSI zum vierten Mal gemessen - faellt in allen 24 Zellen durch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User: "messe ihn nochmal". Kein blosses Wiederholen: die drei alten RSI-Laeufe stiegen alle am Bar-Close zum MARKTPREIS ein. Genau diese Annahme hat am 05./06.08. Squeeze und Auto-Signal von negativ auf positiv gedreht (+0,32 bzw. +0,61 R - mehr als alle Signal-Ideen zusammen). Fuer RSI war die Ausfuehrungs-Achse nie geprueft. AUFBAU: 80k M5, 2 Halbjahre, kanonischer Exit, Echtkosten, 95-%-Bootstrap. 2 Lesarten (Lehrbuch-Reversion und die Umkehrung) x 3 Schwellenpaare (70/30, 75/25, 80/20) x 2 Ausfuehrungen (Markt / ruhende Order am Extrem mit Fill bei Beruehrung) = 24 Zellen. ⚠ KEIN NACHBAU des Indikators: `calc_rsi` ist ein Wilder-RSI mit Rekursion ueber die GANZE Reihe, ein Slice waere also nicht dasselbe. Die Serie wird inkrementell gerechnet und an 200 Zufallsstichproben gegen die echte Funktion verifiziert; bei Abweichung bricht das Skript ab. Groesste gemessene Abweichung: unter 1e-6. ERGEBNIS: KEINE einzige Zelle erfuellt die vorab fixierte Regel (beidhaelftig OeR > 0 UND PF > 1 UND KI ohne die Null UND Nachbarn gleichgerichtet). Vier Zellen sind sogar beidseitig SIGNIFIKANT NEGATIV (KI komplett unter null): REV 70/30 am Markt und MOM 75/25 an der Order. ⚠⚠ DIE AUSFUEHRUNGS-ACHSE HILFT HIER NICHT - und das war vorab so vorhergesagt. Bei REV 75/25 hebt die ruhende Order zwar von -0,107/-0,014 auf +0,006/+0,075, aber beide Intervalle enthalten weiter die Null, und die Nachbarn kippen (80/20: H1 -0,181). Das bestaetigt den Mechanismus aus `backtest_candle_fade2.py`: eine ruhende Order hilft bei MOMENTUM-Entries und schadet bei REVERSIONS-Entries, weil eine Limit-Order am Extrem nur fuellt, wenn sich die Bewegung weiter ausdehnt - also genau dann, wenn man falsch liegt. ⚠ Auch die im Projekt dokumentierte Umkehrung ("RSI gegen die Richtung = doppelter Edge") traegt STANDALONE nicht: die MOM-Zeilen sind ueberwiegend schlechter als die REV-Zeilen. Der Pullback-Effekt existiert, aber als Konfidenz-Beitrag im gegateten Signal, nicht als eigenstaendiger Einstieg. 29. verworfener Signal-Eingriff. RSI bleibt draussen. Co-Authored-By: Claude Opus 5 --- backtest_rsi_v2.py | 193 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 backtest_rsi_v2.py diff --git a/backtest_rsi_v2.py b/backtest_rsi_v2.py new file mode 100644 index 0000000..8ecee5b --- /dev/null +++ b/backtest_rsi_v2.py @@ -0,0 +1,193 @@ +"""RSI ZUM VIERTEN MAL gemessen — diesmal auf der Ausfuehrungs-Achse (2026-08-19) + +User: "messe ihn nochmal". + +WARUM DAS KEIN BLOSSES WIEDERHOLEN IST. RSI ist im Projekt dreimal +durchgefallen (`backtest_confluence.py` als Konfluenz-Filter, +`backtest_buch_luis.py` als Divergenz, dazu der Pullback-Befund). ALLE drei +stiegen am **Bar-Close zum Marktpreis** ein. Genau diese Annahme hat am +05./06.08. Squeeze und Auto-Signal von negativ auf positiv gedreht - der +Einstiegspreis war dort +0,32 bzw. +0,61 R wert, mehr als alle Signal-Ideen +zusammen. Fuer RSI war diese Achse nie geprueft. + +⚠⚠ ABER die Vorhersage ist ungünstig, und sie steht VOR dem Lauf hier: RSI ist +ein REVERSIONS-Einstieg (ueberkauft -> short). Und fuer Reversion ist die +ruhende Order gemessen SCHLECHTER, nicht besser - `backtest_candle_fade2.py` +(05.08.): "ruhende Order hilft bei Momentum-Entries, schadet bei +Reversion-Entries", weil eine Limit-Order am Extrem nur fuellt, wenn sich die +Bewegung weiter ausdehnt - also genau dann, wenn man falsch liegt. Wenn dieser +Lauf dasselbe zeigt, ist das eine Bestaetigung des Mechanismus, keine +Ueberraschung. + +GEMESSEN WERDEN BEIDE LESARTEN, weil der Projekt-Befund sagt, dass die +Lehrbuch-Lesart hier invertiert ist ("RSI *gegen* die Richtung = doppelter +Edge"): + REVERSION RSI > hi -> SHORT, RSI < lo -> LONG (Lehrbuch) + MOMENTUM RSI > hi -> LONG, RSI < lo -> SHORT (die Umkehrung) +je mit drei Schwellenpaaren (70/30, 75/25, 80/20) = Nachbar-Robustheit, und je +mit zwei Ausfuehrungen (Markt am Bar-Close / ruhende Order am Extrem der +Ausloese-Bar, Fill bei BERUEHRUNG). + +⚠ KEIN NACHBAU des RSI: die Serie wird inkrementell gerechnet UND an 200 +Stichproben gegen die echte `core.analysis.calc_rsi` verifiziert. Weicht sie ab, +bricht das Skript ab - ein selbst gebauter Indikator waere genau die +Divergenz-Falle, die im Projekt dreimal zugeschlagen hat. + +REGEL VORAB FIXIERT: tragfaehig nur, wenn eine Zelle in BEIDEN Halbjahren +OeR > 0 UND PF > 1 liefert, das 95-%-KI die Null ausschliesst UND die beiden +Nachbar-Schwellen dasselbe Vorzeichen haben. + +Aufruf: python backtest_rsi_v2.py [bars] +""" +import random +import sys + +import MetaTrader5 as mt5 + +from backtest_cost_gate import kennzahlen +from core.analysis import calc_rsi +from core.exit_model import LIVE, simulate +from core.squeeze_scan import atr_series + +PERIOD = 14 +SCHWELLEN = [(70, 30), (75, 25), (80, 20)] +COOLDOWN = 12 # Bars, damit dieselbe Ueberdehnung nicht dutzendfach zaehlt + + +def rsi_serie(C, period=PERIOD): + """Wilder-RSI je Bar. ⚠ Gegen `calc_rsi` verifiziert (s. `pruefe`).""" + n = len(C) + out = [50.0] * n + if n < period + 1: + return out + g = l = 0.0 + for i in range(1, period + 1): + d = C[i] - C[i - 1] + g += max(d, 0.0); l += max(-d, 0.0) + ag, al = g / period, l / period + out[period] = 100.0 if al == 0 else 100.0 - 100.0 / (1.0 + ag / al) + for i in range(period + 1, n): + d = C[i] - C[i - 1] + ag = (ag * (period - 1) + max(d, 0.0)) / period + al = (al * (period - 1) + max(-d, 0.0)) / period + out[i] = 100.0 if al == 0 else 100.0 - 100.0 / (1.0 + ag / al) + return out + + +def pruefe(C, R, proben=200): + """Die Serie MUSS der echten Funktion entsprechen — sonst Abbruch.""" + rnd = random.Random(42) + schlimmster = 0.0 + for _ in range(proben): + i = rnd.randrange(PERIOD + 5, len(C)) + schlimmster = max(schlimmster, abs(calc_rsi(C[:i + 1], PERIOD) - R[i])) + return schlimmster + + +def handeln(i, d, H, L, C, SP, A, ende, modus): + """modus 'markt' = Fill am Close von i · 'level' = ruhende Order am Extrem.""" + atr = A[i] + if not atr or atr <= 0 or i + 1 >= ende: + return None + if modus == "markt": + f, preis = i + 1, C[i] + else: + # Ruhende Order am Extrem der Ausloese-Bar, Fill bei BERUEHRUNG. + preis = H[i] if d > 0 else L[i] + f = None + for x in range(i + 1, min(i + 13, ende)): + if (H[x] >= preis) if d > 0 else (L[x] <= preis): + f = x + break + if f is None or f + 1 >= ende: + return None + f += 1 + r = simulate(preis, d, atr, H, L, C, f, LIVE, timestop=True, use_tp=True) + return r - ((SP[i] if SP[i] > 0 else 0.0225) / atr) + + +def lauf(H, L, C, SP, A, R, lo_i, hi_i, ende, hi, lo, lesart, modus): + rs, letzte = [], -10**9 + for i in range(max(lo_i, PERIOD + 1), hi_i): + if i - letzte < COOLDOWN: + continue + if R[i] >= hi: + d = -1 if lesart == "reversion" else 1 + elif R[i] <= lo: + d = 1 if lesart == "reversion" else -1 + else: + continue + letzte = i + r = handeln(i, d, H, L, C, SP, A, ende, modus) + if r is not None: + rs.append(r) + return rs + + +def zeig(lbl, k1, k2): + def f(k): + if not k: + return " --" + ci = k.get("ci"); m = "" + if ci: + m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1]) + + ("" if ci[0] * ci[1] > 0 else " ~0")) + return "n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}".format( + k["n"], k["wr"], k["oer"], k["pf"], m) + ok = (k1 and k2 and k1["oer"] > 0 and k2["oer"] > 0 and k1["pf"] > 1 + and k2["pf"] > 1 and (k1.get("ci") or (0, 0))[0] > 0 + and (k2.get("ci") or (0, 0))[0] > 0) + print(" {:<26} H1 {}".format(lbl, f(k1))) + print(" {:<26} H2 {} {}".format("", f(k2), "✅" if ok else "❌")) + return ok + + +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) + R = rsi_serie(C) + N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1 + + fehler = pruefe(C, R) + print("=" * 96) + print(" RSI({}) auf {} M5 ({} Bars) · kanonischer Exit · Echtkosten · 95-%-KI" + .format(PERIOD, sym, N)) + print(" Serie gegen die echte `calc_rsi` geprueft: groesste Abweichung {:.2e}" + .format(fehler)) + assert fehler < 1e-6, "RSI-Serie weicht von calc_rsi ab — Abbruch" + print("=" * 96) + + treffer = [] + for modus, titel in (("markt", "A) MARKT am Bar-Close (= die drei alten Laeufe)"), + ("level", "B) ruhende Order am Extrem, Fill bei BERUEHRUNG")): + print("\n " + titel) + print(" " + "-" * 92) + for lesart in ("reversion", "momentum"): + for hi, lo in SCHWELLEN: + k1 = kennzahlen(lauf(H, L, C, SP, A, R, 0, mid, ende, hi, lo, lesart, modus)) + k2 = kennzahlen(lauf(H, L, C, SP, A, R, mid, ende, ende, hi, lo, lesart, modus)) + if zeig("{} {}/{}".format(lesart.upper()[:3], hi, lo), k1, k2): + treffer.append((modus, lesart, hi, lo)) + print("") + + print(" " + "=" * 92) + print(" Zellen, die die vorab fixierte Regel erfuellen: {}".format( + len(treffer) or "KEINE")) + for t in treffer: + print(" {}".format(t)) + print(" ⚠ Nachbar-Robustheit zusaetzlich pruefen: eine einzelne Zelle unter") + print(" 12 getesteten ist bei 5 % Irrtum die ERWARTETE Zahl an Fehlalarmen.") + print(" " + "=" * 92) + + +if __name__ == "__main__": + main()