Files
AH-Oil-Trader/backtest_rsi_filter.py
Axel HocksandClaude Opus 5 e3e8a6b9fd RSI als Filter auf dem GEGATETEN Signal - die neue Achse traegt auch nicht
User: "neue achse" -> "messe". Das war die einzige nie gepruefte RSI-Population:
alle bisherigen Laeufe massen RSI als EIGENSTAENDIGEN Einstieg
(backtest_rsi_v2.py heute) oder als Konfluenz-Beitrag auf dem UNGEGATETEN
_build-Signal mit altem Exit (backtest_confluence.py). Hier laeuft er auf den
Signalen, die den VOLLEN Live-Gate-Stack passieren - 1344/2038 statt ~1700/2500.

AUFBAU: echte Gate-Pipeline aus backtest_entryroom_level.sammle (kein Nachbau,
inkl. set_entry_room - die Lehre vom 05.08.), RSI-Serie gegen calc_rsi
verifiziert, kanonischer Exit, Echtkosten, 2 Halbjahre, 95-%-KI, beide
Ausfuehrungen, 3 Schwellen als Nachbar-Robustheit.

ERGEBNIS: KEINE der 12 Zellen erfuellt die vorab fixierte Regel. Die Effekte
sind winzig - alle Deltas gegen die Basis liegen bei ±0,06 R, ausser in den
Extrem-Baendern, und die haben n=10 bis 50 (Rauschen).

⚠ Die Projekt-Hypothese "RSI GEGEN die Richtung = doppelter Edge" reproduziert
NICHT: am Markt +0,040 in H2 aber -0,021 in H1, an der Order genau umgekehrt
(+0,010 / -0,041). Vorzeichen kippt in beiden Ausfuehrungen. Der Pullback-Effekt
existiert, aber der RSI ist nicht das Werkzeug, ihn zu greifen - im Bot laeuft er
ueber die EMA-Distanz.

 EIN robuster Befund bleibt, und er ist die Bestaetigung einer vorhandenen
Regel: MIT RSI>70 an der ruhenden Order ist in H2 OeR -0,683 mit KI
[-1,041 .. -0,310], also signifikant NEGATIV. In eine Richtung einzusteigen,
waehrend der RSI dort schon extrem steht, ist messbar schaedlich - genau das,
was die Anti-Ueberdehnung (_STRETCH_MAX 3,5) bereits abfaengt. Kein neuer Filter
noetig, die Wirkung ist schon da.

30. verworfener Signal-Eingriff. RSI bleibt reine Anzeige.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:05:11 +02:00

169 lines
6.8 KiB
Python

"""RSI als FILTER auf dem gegateten Wellensignal (2026-08-19) — die neue Achse.
User: "neue achse" → "messe".
WAS DARAN NEU IST. Alle bisherigen RSI-Laeufe des Projekts massen RSI als
EIGENSTAENDIGEN Einstieg (`backtest_rsi_v2.py` heute: 24 Zellen, keine besteht)
oder als Konfluenz-Beitrag auf dem UNGEGATETEN `_build`-Signal mit einem alten
Exit (`backtest_confluence.py`). Nie gemessen wurde er auf der Population, die
live tatsaechlich handelt: den Signalen, die den VOLLEN Gate-Stack passieren
(Totband · M30-Filter · Anti-Ueberdehnung · Winkel · min_conf 55 ·
Breakout-Bestaetigung · Entry-Raum). Das sind nur ~300-500 je Halbjahr statt
~800 - eine andere Grundgesamtheit, und damit eine faire neue Achse.
DIE HYPOTHESE STAMMT AUS DEM PROJEKT SELBST und ist kontraintuitiv: dokumentiert
ist "RSI *bestaetigend* ist kontraproduktiv, RSI *gegen* die Richtung = doppelter
Edge" - der Pullback-Effekt. Gemessen wird deshalb BEIDES, gegen dieselbe Basis.
BASIS alle gegateten Signale, kein Filter
MIT nur wenn RSI die Richtung BESTAETIGT (LONG: RSI > x)
GEGEN nur wenn RSI DAGEGEN steht (LONG: RSI < 100-x) <- Pullback
x in 50 / 60 / 70 = Nachbar-Robustheit. Beide Ausfuehrungen (Markt am Bar-Close /
ruhende Order am Bestaetigungs-Level mit Fill bei BERUEHRUNG).
⚠ KEIN NACHBAU: der Gate-Stack kommt aus `backtest_entryroom_level.sammle`
(dieselbe Pipeline wie `backtest_conf_gated.py`), die RSI-Serie ist gegen die
echte `core.analysis.calc_rsi` verifiziert (s. `backtest_rsi_v2.pruefe`).
REGEL VORAB FIXIERT: ein Filter traegt nur, wenn die BEHALTENE Gruppe die Basis
in BEIDEN Halbjahren im OeR schlaegt, ihr 95-%-KI die Null ausschliesst UND die
beiden Nachbar-Schwellen dasselbe Vorzeichen zeigen. Zusaetzlich muss die
ENTFERNTE Gruppe schlechter sein als die behaltene - sonst wirft der Filter
nicht die schlechten Signale weg, sondern nur irgendwelche.
⚠ GRENZE: ein Filter verkleinert die Stichprobe zwangslaeufig. Ein besseres OeR
bei halber Trade-Zahl ist noch kein Ertrag - deshalb steht SR (Summe) daneben.
Aufruf: python backtest_rsi_filter.py [bars]
"""
import sys
import MetaTrader5 as mt5
import core.wave_rec as WR
from backtest_breakout_gated import _PIV_K, _TU, _atr_series, _htf_sign
from backtest_cost_gate import kennzahlen
from backtest_entryroom_level import _GATE, _K, handeln, sammle
from backtest_rsi_v2 import pruefe, rsi_serie
from core.analysis import calc_trend_angle
from core.exit_model import LIVE
from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW,
_ema_series)
SCHWELLEN = [50, 60, 70]
def zeig(lbl, k1, k2, b1=None, b2=None):
def f(k, b):
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"))
dlt = ""
if b:
dlt = " Δ{:+.3f}".format(k["oer"] - b["oer"])
return "n={:>4} OeR={:+.3f} PF={:.2f} SR={:+7.1f}{}{}".format(
k["n"], k["oer"], k["pf"], k["sr"], m, dlt)
print(" {:<20} H1 {}".format(lbl, f(k1, b1)))
print(" {:<20} H2 {}".format("", f(k2, b2)))
if not (k1 and k2 and b1 and b2):
return False
return (k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"]
and k1["oer"] > 0 and k2["oer"] > 0
and (k1.get("ci") or (0, 0))[0] > 0 and (k2.get("ci") or (0, 0))[0] > 0)
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_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
R = rsi_serie(C)
N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1
fehler = pruefe(C, R)
assert fehler < 1e-6, "RSI-Serie weicht von calc_rsi ab (%.2e)" % fehler
print("Vorberechnung ...", flush=True)
AD = [90.0] * N
for i in range(_ANGLE_LR + 2, N):
AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR)
HTF = [0] * N; H1 = [0] * N
for i in range(N):
if A[i]:
HTF[i] = _htf_sign(C, i, 6, A[i]); H1[i] = _htf_sign(C, i, 12, A[i])
PH = [j for j in range(_PIV_K, N - _PIV_K)
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
PL = [j for j in range(_PIV_K, N - _PIV_K)
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
w.set_reversal_enabled(False)
w.set_entry_room(_GATE) # ⚠ ohne das ist das Gate ein No-op (Lehre 05.08.)
w.set_dead_hours([])
w.set_breakout_k(_K)
s1 = sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, 0, mid)
s2 = sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, mid, ende)
print("=" * 96)
print(" RSI als FILTER auf dem GEGATETEN Signal — {} M5 ({} Bars)".format(sym, N))
print(" Gegatete Signale: H1 {} H2 {} (WR/PF/KI je Gruppe)".format(len(s1), len(s2)))
print("=" * 96)
treffer = []
for modus, titel in (("markt", "A) MARKT am Bar-Close"),
("level", "B) ruhende Order am Level, Fill bei BERUEHRUNG")):
print("\n " + titel)
print(" " + "-" * 92)
def rs(sigs, keep):
out = []
for s in sigs:
if not keep(s):
continue
r = handeln(s, H, L, C, SP, ende, modus, 0.0, 12)
if r is not None:
out.append(r)
return out
b1 = kennzahlen(rs(s1, lambda s: True))
b2 = kennzahlen(rs(s2, lambda s: True))
zeig("BASIS (kein Filter)", b1, b2)
print("")
for x in SCHWELLEN:
for lbl, keep in (
("MIT RSI>{}".format(x),
lambda s, x=x: (R[s["i"]] > x) if s["d"] > 0 else (R[s["i"]] < 100 - x)),
("GEGEN RSI<{}".format(100 - x),
lambda s, x=x: (R[s["i"]] < 100 - x) if s["d"] > 0 else (R[s["i"]] > x)),
):
k1 = kennzahlen(rs(s1, keep)); k2 = kennzahlen(rs(s2, keep))
if zeig(lbl, k1, k2, b1, b2):
treffer.append((modus, lbl))
print("")
print(" " + "=" * 92)
print(" Zellen, die die vorab fixierte Regel erfuellen: {}".format(
len(treffer) or "KEINE"))
for t in treffer:
print(" {}".format(t))
print(" ⚠ SR (Summe) mitlesen: ein besseres OeR bei halber Trade-Zahl ist")
print(" Handelsvermeidung, kein Ertrag.")
print(" " + "=" * 92)
if __name__ == "__main__":
main()