Files
AH-Oil-Trader/backtest_rsi_v2.py
Axel HocksandClaude Opus 5 d7dc997eee RSI zum vierten Mal gemessen - faellt in allen 24 Zellen durch
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 <noreply@anthropic.com>
2026-08-19 20:52:37 +02:00

194 lines
7.4 KiB
Python

"""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()