Gesamtempfehlung: Selbst-Kalibrierung, voller Ausrichtungs-Split, Block-Gruende,
Elliott entmachtet (v=144)
Vier Verbesserungen, alle aus dem Gemessenen abgeleitet — kein neues Signal.
Ausgangspunkt: die Karte hat genau eine belegte Funktion, das Veto; als Prognose
ist sie ein Muenzwurf (48-51 % ueber 1.071 Episoden).
(1) SELBST-KALIBRIERUNG. Neue Tabelle rec_outcomes: eine Zeile je EPISODE
(Richtungswechsel, nicht je Minute), Auswertung ~alle 5 min gegen candles_m1
nach 30/60 min, Snapshot rec_track, Zeile #vd-selfcal. Gegen 50 % zu lesen.
Warum der groesste Hebel: die Qualitaet war bis heute unsichtbar und musste
auf Nachfrage rueckwirkend gemessen werden. Dasselbe Muster
(pbreak_predictions) hat den Ausfall des P(break)-Modells LIVE aufgedeckt.
Mit 4 Szenarien getestet (tests_rec_outcomes.py). Zwei Fehlschlaege dabei
waren Testfehler, keine Code-Fehler — dokumentiert.
(2) alignment_stats auf die VOLLE Historie (1.216 statt 40 Trades) und
zusaetzlich je Lot. Absolute Euro-Vergleiche sind bei wechselnder
Positionsgroesse ungueltig (Lehre 04.08.). Damit steht die einzige
beidhaelftig robuste Aussage der Karte auf ihrer vollen Stichprobe.
(3) Block-Gruende sichtbar (block_mix -> #vd-blocks): "heute X % ohne Block ·
entry_room … · min_conf …". Bisher nur per DB-Abfrage zu beantworten.
(4) ELLIOTT: Verdict-Gewicht 1,0 -> 0. Gemessen ueber 15.544 Zeilen hatte es mit
1.384 Episoden die groesste Stichprobe der Tabelle und ist darin flach
(-0,075/+0,032, 50 % Treffer) — bei 25,9 % Einfluss auf die Nadel, weil es
in nur 1 % der Zeilen schweigt. Chip bleibt, Stimme entfaellt.
⚠ Folge: die Nadel schlaegt staerker aus (real +0,33 -> +1,00).
⚠ Namenskollision beim Bau gefunden und behoben: die neue Klasse hiess zuerst
.vd-track — so heisst bereits die Schiene der Bias-Nadel; sie waere
ueberschrieben worden. Jetzt .vd-selfcal.
Live verifiziert: alignment je Lot, block_mix (93 von 1381 ohne Block),
rec_outcomes angelegt, Elliott weight=0 bei erhaltenem Chip, v=144 ausgeliefert,
eine Instanz auf Port 8000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4f85d2ce75
commit
874ee16e00
@@ -3608,7 +3608,60 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
Anzeige zu einer Erwartung ein, die die Messung nicht deckt.
|
Anzeige zu einer Erwartung ein, die die Messung nicht deckt.
|
||||||
⚠ Bewusst **statisches Markup, kein JS** — es ist ein Dauerhinweis, kein Zustand;
|
⚠ Bewusst **statisches Markup, kein JS** — es ist ein Dauerhinweis, kein Zustand;
|
||||||
damit entfällt auch jedes Null-Risiko beim Render (s. Cache-Regel oben).
|
damit entfällt auch jedes Null-Risiko beim Render (s. Cache-Regel oben).
|
||||||
- Asset-Version aktuell **v=143** (in `web/index.html` hochzählen, siehe Workflows).
|
- **⚠⚠ VIER VERBESSERUNGEN AN DER GESAMTEMPFEHLUNG (User-Vorgabe 2026-08-06,
|
||||||
|
v=144) — alle aus dem Gemessenen abgeleitet, KEIN neues Signal.** Ausgangspunkt:
|
||||||
|
die Karte hat genau eine belegte Funktion, das **Veto** (s. Ausrichtungs-Split);
|
||||||
|
als Prognose ist sie ein Münzwurf. Verbessert wird also das Veto und die
|
||||||
|
Sichtbarkeit — nicht die Vorhersage.
|
||||||
|
**(1) SELBST-KALIBRIERUNG — die Karte zeigt ihre eigene Trefferquote.**
|
||||||
|
Neue Tabelle **`rec_outcomes`** (DB-Backup `…bak-2026-08-06-recoutcomes`):
|
||||||
|
`history.log_rec_outcome` schreibt **eine Zeile je EPISODE** (Richtungswechsel,
|
||||||
|
nicht je Minute — sonst zählt man dieselbe Marktlage dutzendfach);
|
||||||
|
`engine._evaluate_rec_outcomes` trägt ~alle 5 min im Trend-Loop den
|
||||||
|
Forward-Return nach 30/60 min aus `candles_m1` nach (⚠ Broker-Offset, wie bei
|
||||||
|
`_evaluate_pbreak_predictions`). `history.rec_accuracy(200)` → Snapshot
|
||||||
|
**`rec_track`** → Zeile **`#vd-selfcal`**.
|
||||||
|
⚠ **Warum das der grösste Hebel ist:** die Qualität der Empfehlung war bis heute
|
||||||
|
unsichtbar — sie musste auf Nachfrage rückwirkend gemessen werden. Dasselbe
|
||||||
|
Muster (`pbreak_predictions`) hat den Ausfall des P(break)-Modells LIVE
|
||||||
|
aufgedeckt, was kein Backtest konnte. Die Zeile ist **gegen 50 % zu lesen**,
|
||||||
|
nicht gegen 0. Fehlen Bars (Wochenende), bleibt die Zeile offen statt falsch
|
||||||
|
bewertet; nach 6 h wird sie geschlossen. **Mit 4 Szenarien getestet**
|
||||||
|
(`tests_rec_outcomes.py`): Episoden-Dedup · WARTEN re-armt · Auswertung inkl.
|
||||||
|
Offset in beide Richtungen · fehlende Bar → bleibt offen · Aggregation.
|
||||||
|
⚠ Zwei Fehler dabei waren MEINE Testfehler, nicht Code-Fehler: die Test-Kerzen
|
||||||
|
aus dem Vorabschnitt lagen im Fenster des nächsten, und die DB-Kopie enthält
|
||||||
|
**echte** `candles_m1` — der Fall „keine Bar" entsteht erst, wenn das Fenster
|
||||||
|
wirklich geleert wird.
|
||||||
|
**(2) AUSRICHTUNGS-SPLIT auf die volle Historie + je Lot.** `alignment_stats`
|
||||||
|
lief auf den **letzten 40** Trades und zeigte absolute Euro. Jetzt über **alle
|
||||||
|
1.216** (Default `last_n=0`) und zusätzlich **`pnl_lot`** — absolute
|
||||||
|
Euro-Vergleiche sind bei wechselnder Positionsgrösse ungültig (Lehre 04.08.).
|
||||||
|
Der Order-Dialog zeigt beides. Damit steht die einzige beidhälftig robuste
|
||||||
|
Aussage der Karte auf ihrer vollen Stichprobe: **gegen das Signal −6,60 €/Lot**.
|
||||||
|
**(3) BLOCK-GRUND SICHTBAR** (`engine._block_mix_cached` → Snapshot
|
||||||
|
`block_mix` → `#vd-blocks`): „heute X % ohne Block · entry_room … · min_conf …".
|
||||||
|
Auf die Frage „warum gab es so wenige Empfehlungen?" war bisher eine DB-Abfrage
|
||||||
|
nötig. ~120 s gecacht, reine Anzeige.
|
||||||
|
**(4) ELLIOTT ENTMACHTET — Verdict-Gewicht 1,0 → 0** (gemessen
|
||||||
|
`analyze_verdict_modules.py`, 15.544 Zeilen). Elliott hatte mit **1.384
|
||||||
|
Episoden die grösste Stichprobe der ganzen Tabelle** und ist darin **flach**:
|
||||||
|
Überschuss −0,075/+0,032 ×ATR, **50 % Treffer**. Gleichzeitig war es mit
|
||||||
|
**25,9 % die zweitlauteste Stimme** — weil es in nur 1 % der Zeilen schweigt.
|
||||||
|
Mit H1 (38,1 %) bildete es einen Dauerhintergrund von ~64 %. **Der Chip bleibt**
|
||||||
|
(das Ziel ist als Kontext nützlich), nur die Stimme entfällt.
|
||||||
|
⚠ **Sichtbare Folge: die Nadel schlägt stärker aus.** Real beim Umbau
|
||||||
|
+0,33 → **+1,00**, weil nur noch H1 (1,5) und Liq-Trend (0,5) sprachen. Mit
|
||||||
|
wenigen sprechenden Modulen kann ein einzelnes die Nadel auf ±1 ziehen — das ist
|
||||||
|
ehrlicher als eine künstlich gedämpfte Nadel, sieht aber dramatischer aus.
|
||||||
|
⚠ **Reine Anzeige-Änderung** — die Order steuert die Welle über die Headline,
|
||||||
|
nicht der Konsens. Zurück: `ew = 1.0 if ev != 0 else 0.0` in `_verdict`.
|
||||||
|
⚠ **Nicht angefasst, aber auffällig** (für die 25.000-Verdict-Messung):
|
||||||
|
**Orderbuch** ist über 2.688 Episoden beidhälftig NEGATIV (−0,054/−0,066,
|
||||||
|
49 % Treffer) bei Gewicht 0,5 — der einzige konsistent falsche Beitrag.
|
||||||
|
**Muster** (+1,27/+0,67, 58 %, n=88) und **Liq-Trend** (+0,20/+0,32, n=132)
|
||||||
|
sehen gut aus, sind aber zu dünn. Alle übrigen kippen zwischen den Hälften.
|
||||||
|
- Asset-Version aktuell **v=144** (in `web/index.html` hochzählen, siehe Workflows).
|
||||||
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
||||||
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
||||||
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
||||||
|
|||||||
@@ -96,8 +96,18 @@ def _htf_sign(C, i, n, atr):
|
|||||||
|
|
||||||
|
|
||||||
def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi,
|
def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi,
|
||||||
min_conf, nacht, am_level, slip=0.0):
|
min_conf, nacht, am_level, slip=0.0, fill="close"):
|
||||||
"""Ein Durchlauf: sequentiell, EIN Positions-Slot, Live-Dedup."""
|
"""Ein Durchlauf: sequentiell, EIN Positions-Slot, Live-Dedup.
|
||||||
|
|
||||||
|
`fill` (2026-08-06 ergaenzt, Default = bisheriges Verhalten, bitgenau):
|
||||||
|
"close" — die Bestaetigung gilt, wenn der Bar-CLOSE das Level bricht.
|
||||||
|
"touch" — die ruhende Stop-Order fuellt bei der ERSTEN BERUEHRUNG, auch
|
||||||
|
wenn der Bar wieder zurueckfaellt. Das ist, was live passiert.
|
||||||
|
⚠ Der Unterschied ist kein Detail: beim Momentum betrug er 0,45 R und drehte
|
||||||
|
+0,40 auf −0,06 (`backtest_momentum_v2.py`), beim Squeeze 0,32 R
|
||||||
|
(`backtest_squeeze_touchfill.py`). "close" konditioniert auf den Bar-Schluss,
|
||||||
|
den eine liegende Order nicht kennen kann.
|
||||||
|
"""
|
||||||
def kosten(i, atr):
|
def kosten(i, atr):
|
||||||
return (SP[i] if SP[i] > 0 else 0.0225) / atr
|
return (SP[i] if SP[i] > 0 else 0.0225) / atr
|
||||||
|
|
||||||
@@ -135,7 +145,13 @@ def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi,
|
|||||||
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
pend = {"dir": d, "level": C[i] + d * _K * atr,
|
||||||
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
|
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
|
||||||
if not pend["confirmed"]:
|
if not pend["confirmed"]:
|
||||||
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
|
if fill == "touch":
|
||||||
|
# Beruehrung genuegt — die Order liegt dort und wird ausgeloest.
|
||||||
|
broke = ((H[i] >= pend["level"]) if d > 0
|
||||||
|
else (L[i] <= pend["level"]))
|
||||||
|
else:
|
||||||
|
broke = ((C[i] >= pend["level"]) if d > 0
|
||||||
|
else (C[i] <= pend["level"]))
|
||||||
against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
|
against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
|
||||||
if broke:
|
if broke:
|
||||||
pend["confirmed"] = True
|
pend["confirmed"] = True
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Haelt der SIG-Pending-Pfad auch mit realistischem Fill? — die offene Zahl.
|
||||||
|
|
||||||
|
ANLASS (2026-08-06): `signal_pending_entry` laeuft seit dem 05.08. mit ECHTEM
|
||||||
|
Geld. Die Erwartung dahinter (ØR +0,39/+0,36) stammt aus
|
||||||
|
`backtest_auto_signal_v3.py` und unterstellt: Einstieg zum Level-Preis, aber
|
||||||
|
**nur dort, wo der Bar-CLOSE das Level gebrochen hat**. Eine liegende Stop-Order
|
||||||
|
weiss das nicht — sie fuellt bei der ERSTEN BERUEHRUNG, auch bei jeder, die
|
||||||
|
wieder zurueckfaellt.
|
||||||
|
|
||||||
|
Wie teuer dieser Unterschied ist, wurde heute zweimal gemessen:
|
||||||
|
Momentum +0,40 → −0,06 (0,45 R Aufschlag) — Level WANDERT
|
||||||
|
Squeeze +0,46 → +0,13 (0,32 R Aufschlag) — Level STEHT
|
||||||
|
Der Squeeze haelt, das Momentum nicht. **Und `_pend` gehoert zur wandernden
|
||||||
|
Sorte:** der Anker wird bei jeder k×ATR-Gegenbewegung, nach Timeout und bei
|
||||||
|
Richtungswechsel neu gesetzt. Es ist also mit dem schlechteren Fall zu rechnen.
|
||||||
|
|
||||||
|
Benutzt dieselbe `lauf()` wie v3, nur mit `fill="touch"` — nichts nachgebaut,
|
||||||
|
damit die Zahlen vergleichbar bleiben. Der Default `fill="close"` reproduziert
|
||||||
|
v3 bitgenau (verifiziert).
|
||||||
|
|
||||||
|
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
|
||||||
|
`signal_pending_entry` bleibt nur gerechtfertigt, wenn der Pfad mit
|
||||||
|
`fill="touch"` in BEIDEN Haelften ØR > 0 UND PF > 1 haelt UND besser ist als
|
||||||
|
der Markt-Einstieg. Sonst gehoert er abgeschaltet.
|
||||||
|
|
||||||
|
Aufruf: python backtest_signal_touchfill.py [bars]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
import backtest_auto_signal_v3 as V
|
||||||
|
from core.wave_rec import WaveRecommender, calc_trend_angle
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
n_bars = 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_bars)
|
||||||
|
point = mt5.symbol_info(sym).point
|
||||||
|
mt5.shutdown()
|
||||||
|
# ⚠ Aufbau 1:1 wie in v3.main() — aber ueber die IMPORTIERTEN Helfer, nicht
|
||||||
|
# ueber Kopien. Waere hier ein eigener EMA/ATR drin, waere der Vergleich
|
||||||
|
# zwischen den beiden Fill-Annahmen nicht mehr sauber.
|
||||||
|
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
|
||||||
|
C = [float(b["close"]) for b in bars]; T = [int(b["time"]) for b in bars]
|
||||||
|
SP = [float(b["spread"]) * point for b in bars]
|
||||||
|
EF = V._ema_series(C, V._EMA_FAST); ES = V._ema_series(C, V._EMA_SLOW)
|
||||||
|
A = V._atr_series(H, L, C)
|
||||||
|
AD = [0.0] * len(C)
|
||||||
|
for i in range(V._N_BARS, len(C)):
|
||||||
|
AD[i] = calc_trend_angle(C[i - V._N_BARS:i + 1], V._ANGLE_LR) - 90.0
|
||||||
|
if abs(AD[i]) < V._ANGLE_DEAD:
|
||||||
|
AD[i] = 0.0
|
||||||
|
HTF = [V._htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
|
||||||
|
H1S = [V._htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
|
||||||
|
PH = [j for j in range(V._PIV_K, len(C) - V._PIV_K)
|
||||||
|
if H[j] == max(H[j - V._PIV_K:j + V._PIV_K + 1])]
|
||||||
|
PL = [j for j in range(V._PIV_K, len(C) - V._PIV_K)
|
||||||
|
if L[j] == min(L[j - V._PIV_K:j + V._PIV_K + 1])]
|
||||||
|
|
||||||
|
w = WaveRecommender(lambda: None)
|
||||||
|
w.set_reversal_enabled(False)
|
||||||
|
w.set_entry_room(0.6) # ⚠ ohne das ist das Raum-Gate ein No-op
|
||||||
|
w.set_dead_hours("")
|
||||||
|
mid = len(C) // 2
|
||||||
|
|
||||||
|
print("=" * 104)
|
||||||
|
print(f" SIG-PENDING — Fill bei BAR-CLOSE gegen Fill bei BERUEHRUNG "
|
||||||
|
f"({sym}, {len(C)} M5-Bars)")
|
||||||
|
print("=" * 104)
|
||||||
|
print(f" {V._berlin(T[0]):%d.%m.%Y} bis {V._berlin(T[-1]):%d.%m.%Y} · "
|
||||||
|
f"voller Live-Gate-Stack · kanonischer Exit · Echtkosten · EIN Slot")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def paar(mc, am_level, fill):
|
||||||
|
r1, _ = V.lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1S, PH, PL,
|
||||||
|
0, mid, mc, V._NACHT_LIVE, am_level, 0.0, fill)
|
||||||
|
r2, _ = V.lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1S, PH, PL,
|
||||||
|
mid, len(C), mc, V._NACHT_LIVE, am_level, 0.0, fill)
|
||||||
|
return V.kenn(r1), V.kenn(r2)
|
||||||
|
|
||||||
|
print(f" {'Variante':<38}{'H1 n WR OeR PF SigmaR':>33}"
|
||||||
|
f" |{'H2 n WR OeR PF SigmaR':>33}")
|
||||||
|
print(" " + "-" * 102)
|
||||||
|
print(" A) MARKT am Bar-Close (der Ist-Zustand vor dem 05.08.)")
|
||||||
|
for mc in (55, 65, 75):
|
||||||
|
V.zeile(f" Konf>={mc}", *paar(mc, False, "close"))
|
||||||
|
print(" " + "-" * 102)
|
||||||
|
print(" B) am Level, Fill nur bei CLOSE-Bruch (die Zahl, auf der live steht)")
|
||||||
|
for mc in (55, 65, 75):
|
||||||
|
V.zeile(f" Konf>={mc}", *paar(mc, True, "close"))
|
||||||
|
print(" " + "-" * 102)
|
||||||
|
print(" C) am Level, Fill bei BERUEHRUNG (was live wirklich passiert)")
|
||||||
|
haelt = []
|
||||||
|
for mc in (55, 65, 75):
|
||||||
|
k1, k2 = paar(mc, True, "touch")
|
||||||
|
if V.zeile(f" Konf>={mc}", k1, k2):
|
||||||
|
haelt.append(mc)
|
||||||
|
print()
|
||||||
|
print("=" * 104)
|
||||||
|
if haelt:
|
||||||
|
print(f" → Variante C haelt bei Konf {haelt} — `signal_pending_entry` ist gedeckt.")
|
||||||
|
else:
|
||||||
|
print(" → Variante C haelt in KEINER Konfidenz-Stufe.")
|
||||||
|
print(" Damit steht `signal_pending_entry=true` auf einer Zahl, die die")
|
||||||
|
print(" realistische Fill-Annahme nicht ueberlebt → abschalten.")
|
||||||
|
print(" ⚠ B gegen C gelesen = der Look-ahead-Aufschlag (Squeeze 0,32 R,")
|
||||||
|
print(" Momentum 0,45 R).")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+161
-10
@@ -690,6 +690,8 @@ class TradingEngine:
|
|||||||
except Exception as ce: log.warning(f"candles.log: {ce}")
|
except Exception as ce: log.warning(f"candles.log: {ce}")
|
||||||
try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled
|
try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled
|
||||||
except Exception as pe: log.warning(f"pbreak_predictions: {pe}")
|
except Exception as pe: log.warning(f"pbreak_predictions: {pe}")
|
||||||
|
try: self._evaluate_rec_outcomes() # ~alle 5 min, self-throttled
|
||||||
|
except Exception as re_: log.warning(f"rec_outcomes: {re_}")
|
||||||
# Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock
|
# Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock
|
||||||
# unkritisch — reiner HTTP-Fetch mit Timeout)
|
# unkritisch — reiner HTTP-Fetch mit Timeout)
|
||||||
try: self.calendar.maybe_refresh()
|
try: self.calendar.maybe_refresh()
|
||||||
@@ -1675,6 +1677,30 @@ class TradingEngine:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"log_recommendation: {e}")
|
log.warning(f"log_recommendation: {e}")
|
||||||
|
|
||||||
|
# ── Selbst-Kalibrierung: eine Zeile je EPISODE (2026-08-06) ──────────
|
||||||
|
# WARUM: die Qualitaet der Empfehlung war bis heute unsichtbar — sie musste
|
||||||
|
# auf Nachfrage rueckwirkend gemessen werden (`analyze_signal_live.py`:
|
||||||
|
# 48–51 % Treffer ueber 1.071 Episoden). Dasselbe Muster wie
|
||||||
|
# `pbreak_predictions` hat den Ausfall des P(break)-Modells LIVE aufgedeckt,
|
||||||
|
# was kein Backtest konnte. Reine Messung, kein Eingriff in die Logik.
|
||||||
|
# ⚠ Nur bei RICHTUNGSWECHSEL, nicht je Minute — sonst zaehlt man dieselbe
|
||||||
|
# Marktlage dutzendfach (der Fehler, den die Episoden-Bildung behebt).
|
||||||
|
try:
|
||||||
|
_sig = rec.get("signal")
|
||||||
|
if _sig in ("LONG", "SHORT") and _sig != getattr(self, "_rec_epi_sig", None):
|
||||||
|
_atr = float((s.get("pb_feats") or {}).get("atr") or 0.0)
|
||||||
|
_px = float((self.data.snapshot() or {}).get("bid") or 0.0)
|
||||||
|
if _atr > 0 and _px > 0 and self.history:
|
||||||
|
self.history.log_rec_outcome(
|
||||||
|
signal=_sig, conf_pct=int(rec.get("conf_pct") or 0),
|
||||||
|
tf=str(rec.get("tf") or self.wave.timeframe_name or ""),
|
||||||
|
price=_px, atr=_atr)
|
||||||
|
if _sig in ("LONG", "SHORT", "WARTEN"):
|
||||||
|
# WARTEN re-armt: die naechste Richtung ist eine NEUE Episode.
|
||||||
|
self._rec_epi_sig = _sig if _sig != "WARTEN" else None
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"log_rec_outcome: {e}")
|
||||||
|
|
||||||
# Verdict-Stimmen-Logging (2026-07-24, Idee 2b): Modul-Votes 1×/min in die
|
# Verdict-Stimmen-Logging (2026-07-24, Idee 2b): Modul-Votes 1×/min in die
|
||||||
# DB (`verdict_votes`), damit die UNBELEGTEN Stimmen (KI-Copilot, Elliott)
|
# DB (`verdict_votes`), damit die UNBELEGTEN Stimmen (KI-Copilot, Elliott)
|
||||||
# nach ein paar Wochen datenbasiert bewertet werden können (behalten /
|
# nach ein paar Wochen datenbasiert bewertet werden können (behalten /
|
||||||
@@ -2856,11 +2882,20 @@ class TradingEngine:
|
|||||||
if tgt is not None and bid:
|
if tgt is not None and bid:
|
||||||
ev = -1 if tgt < bid else 1 if tgt > bid else 0
|
ev = -1 if tgt < bid else 1 if tgt > bid else 0
|
||||||
edet = f"{(elliott or {}).get('wave', '—')} → {tgt:.3f}"
|
edet = f"{(elliott or {}).get('wave', '—')} → {tgt:.3f}"
|
||||||
# ⚠ Gewicht nur bei GERICHTETEM Ziel (2026-07-31, Modul-Audit): liegt das
|
# ⚠⚠ GEWICHT AUF 0 GESETZT (2026-08-06, gemessen
|
||||||
# Ziel exakt auf dem Kurs, ist ev=0 — das ist keine Aussage und darf die
|
# `analyze_verdict_modules.py` über 15.544 Zeilen). Elliott hatte mit
|
||||||
# Nadel nicht dämpfen. Betraf 1,3 % der Zeilen (klein, aber dieselbe Regel
|
# **1.384 Episoden die grösste Stichprobe** der ganzen Tabelle und ist
|
||||||
# wie bei Welle/Squeeze/M30/H1/KI — Konsistenz statt Sonderfall).
|
# darin **flach**: Überschuss −0,075 / +0,032 ×ATR, **50 % Treffer**.
|
||||||
ew = 1.0 if ev != 0 else 0.0
|
# Gleichzeitig war es mit **25,9 % die zweitlauteste Stimme** der
|
||||||
|
# Bias-Nadel — weil es praktisch nie schweigt (stumm in 1 % der Zeilen).
|
||||||
|
# Zusammen mit H1 (38,1 %) bildete es einen Dauerhintergrund von ~64 %,
|
||||||
|
# gegen den die episodischen Module erst anschieben mussten.
|
||||||
|
# Ein Modul mit der grössten Stichprobe und ohne messbaren Inhalt darf
|
||||||
|
# den Konsens nicht dominieren. Der CHIP bleibt (reine Anzeige, das Ziel
|
||||||
|
# ist als Kontext nützlich) — nur die Stimme entfällt.
|
||||||
|
# ⚠ Reine ANZEIGE-Änderung: die Order steuert die Welle über die
|
||||||
|
# Headline, nicht der Konsens. Zurück: `ew = 1.0 if ev != 0 else 0.0`.
|
||||||
|
ew = 0.0
|
||||||
add("Elliott", ev, ew, edet)
|
add("Elliott", ev, ew, edet)
|
||||||
|
|
||||||
# 7) Squeeze-Breakout — Event-Signal: stimmt NUR bei `active` mit (dann 2,0,
|
# 7) Squeeze-Breakout — Event-Signal: stimmt NUR bei `active` mit (dann 2,0,
|
||||||
@@ -3037,20 +3072,72 @@ class TradingEngine:
|
|||||||
return getattr(self, "_manual_cache", None)
|
return getattr(self, "_manual_cache", None)
|
||||||
|
|
||||||
def _alignment_cached(self) -> dict | None:
|
def _alignment_cached(self) -> dict | None:
|
||||||
"""Ausrichtungs-Split (mit/gegen/ohne Empfehlung, letzte 40 Trades) für den
|
"""Ausrichtungs-Split (mit/gegen/ohne Empfehlung) für den Order-Dialog —
|
||||||
Order-Bestätigungsdialog — ehrliche eigene Zahlen statt generischem Hinweis
|
ehrliche eigene Zahlen statt generischem Hinweis (2026-07-24; Gegen-Signal-
|
||||||
(2026-07-24, Verhaltens-Hebel: Gegen-Signal-Trades sind die gemessene
|
Trades sind die gemessene Kern-Leckage).
|
||||||
Kern-Leckage). DB-Query gecacht (~120 s), Snapshot bleibt billig."""
|
⚠ Seit 2026-08-06 über die **volle Historie** (1.216 Trades) statt der
|
||||||
|
letzten 40 — der Befund ist dort beidhälftig robust, über 40 war er eine
|
||||||
|
Momentaufnahme. Zusätzlich je Lot normiert (`pnl_lot`), weil absolute
|
||||||
|
Euro-Vergleiche bei wechselnder Positionsgröße ungültig sind.
|
||||||
|
DB-Query gecacht (~120 s), Snapshot bleibt billig."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - getattr(self, "_align_ts", 0.0) > 120:
|
if now - getattr(self, "_align_ts", 0.0) > 120:
|
||||||
try:
|
try:
|
||||||
self._align_cache = self.history.alignment_stats(40)
|
self._align_cache = self.history.alignment_stats(0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"alignment_stats: {e}")
|
log.debug(f"alignment_stats: {e}")
|
||||||
self._align_cache = None
|
self._align_cache = None
|
||||||
self._align_ts = now
|
self._align_ts = now
|
||||||
return getattr(self, "_align_cache", None)
|
return getattr(self, "_align_cache", None)
|
||||||
|
|
||||||
|
def _block_mix_cached(self) -> dict | None:
|
||||||
|
"""WELCHE Gates blocken heute, und wie oft? (2026-08-06)
|
||||||
|
|
||||||
|
WARUM SICHTBAR: auf die Frage „warum gab es heute so wenige Empfehlungen?"
|
||||||
|
liess sich das nur per DB-Abfrage beantworten. Real am 06.08.: von 646
|
||||||
|
Zeilen waren **6 ungeblockt** — entry_room 274 · min_conf 225 ·
|
||||||
|
breakout_pending 88 · htf_counter 50. Die Karte sagt das jetzt selbst.
|
||||||
|
Reine Anzeige, ~120 s gecacht."""
|
||||||
|
now = time.time()
|
||||||
|
if now - getattr(self, "_blockmix_ts", 0.0) > 120:
|
||||||
|
try:
|
||||||
|
import sqlite3
|
||||||
|
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
|
||||||
|
rows = con.execute(
|
||||||
|
"SELECT COALESCE(block_reason,'') b, COUNT(*) n FROM recommendations "
|
||||||
|
"WHERE date(timestamp,'unixepoch','localtime') = date('now','localtime') "
|
||||||
|
"GROUP BY b ORDER BY n DESC").fetchall()
|
||||||
|
con.close()
|
||||||
|
ges = sum(r[1] for r in rows)
|
||||||
|
self._blockmix_cache = {
|
||||||
|
"total": ges,
|
||||||
|
"frei": next((r[1] for r in rows if not r[0]), 0),
|
||||||
|
"top": [{"grund": r[0], "n": r[1]} for r in rows if r[0]][:4],
|
||||||
|
} if ges else None
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"block_mix: {e}")
|
||||||
|
self._blockmix_cache = None
|
||||||
|
self._blockmix_ts = now
|
||||||
|
return getattr(self, "_blockmix_cache", None)
|
||||||
|
|
||||||
|
def _rec_track_cached(self) -> dict | None:
|
||||||
|
"""Selbst-Kalibrierung: Trefferquote der letzten Empfehlungs-Episoden.
|
||||||
|
|
||||||
|
⚠ Gegen 50 % zu lesen, nicht gegen 0 — es ist eine Richtungsaussage.
|
||||||
|
Gemessener Ausgangsstand (`analyze_signal_live.py`, 1.071 Episoden):
|
||||||
|
48–51 %. Diese Zeile macht laufend sichtbar, ob sich das aendert, statt
|
||||||
|
dass es erst auf Nachfrage rueckwirkend gemessen wird. ~120 s gecacht."""
|
||||||
|
now = time.time()
|
||||||
|
if now - getattr(self, "_rectrack_ts", 0.0) > 120:
|
||||||
|
try:
|
||||||
|
self._rectrack_cache = (self.history.rec_accuracy(200)
|
||||||
|
if self.history else None)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"rec_accuracy: {e}")
|
||||||
|
self._rectrack_cache = None
|
||||||
|
self._rectrack_ts = now
|
||||||
|
return getattr(self, "_rectrack_cache", None)
|
||||||
|
|
||||||
def _loss_streak_today(self) -> tuple[int, int]:
|
def _loss_streak_today(self) -> tuple[int, int]:
|
||||||
"""(Verlust-Trades heute, Trades heute) — für den Mental-Check der Entry-
|
"""(Verlust-Trades heute, Trades heute) — für den Mental-Check der Entry-
|
||||||
Checkliste („nach 2 Losses Session beenden"). DB-Query ~120 s gecacht."""
|
Checkliste („nach 2 Losses Session beenden"). DB-Query ~120 s gecacht."""
|
||||||
@@ -3276,6 +3363,68 @@ class TradingEngine:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(f"pbreak_predictions auswerten: {e}")
|
log.debug(f"pbreak_predictions auswerten: {e}")
|
||||||
|
|
||||||
|
def _evaluate_rec_outcomes(self):
|
||||||
|
"""Traegt fuer offene Empfehlungs-Episoden den Forward-Return nach.
|
||||||
|
|
||||||
|
⚠ `candles_m1.time` ist ROHE Broker-Zeit (UTC+3), `rec_outcomes.ts` ist
|
||||||
|
lokale Epoch — der Offset muss drauf (dieselbe Falle wie bei
|
||||||
|
`_evaluate_pbreak_predictions`). Genommen wird der letzte M1-Close bis
|
||||||
|
zum Zielzeitpunkt; fehlen Bars (Wochenende, Broker-Ausfall), bleibt die
|
||||||
|
Zeile offen statt falsch bewertet zu werden.
|
||||||
|
|
||||||
|
`hit` = 1, wenn der Kurs in Signalrichtung lief. Die Quote ist gegen 50 %
|
||||||
|
zu lesen, nicht gegen 0."""
|
||||||
|
now = time.time()
|
||||||
|
if now - getattr(self, "_rec_eval_ts", 0.0) < 300:
|
||||||
|
return
|
||||||
|
self._rec_eval_ts = now
|
||||||
|
try:
|
||||||
|
import sqlite3
|
||||||
|
off = self.trader._broker_offset_s()
|
||||||
|
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
|
||||||
|
con.row_factory = sqlite3.Row
|
||||||
|
offen = con.execute(
|
||||||
|
"SELECT id, ts, signal, price, atr, ret30, ret60 FROM rec_outcomes "
|
||||||
|
"WHERE done IS NULL OR done = 0").fetchall()
|
||||||
|
for r in offen:
|
||||||
|
d = 1 if r["signal"] == "LONG" else -1
|
||||||
|
if not r["atr"] or r["atr"] <= 0:
|
||||||
|
con.execute("UPDATE rec_outcomes SET done=1 WHERE id=?", (r["id"],))
|
||||||
|
continue
|
||||||
|
werte = {}
|
||||||
|
for feld, minuten in (("ret30", 30), ("ret60", 60)):
|
||||||
|
if r[feld] is not None:
|
||||||
|
werte[feld] = r[feld]
|
||||||
|
continue
|
||||||
|
ziel = r["ts"] + minuten * 60
|
||||||
|
if now < ziel:
|
||||||
|
continue # noch nicht faellig
|
||||||
|
row = con.execute(
|
||||||
|
"SELECT c FROM candles_m1 WHERE time <= ? AND time > ? "
|
||||||
|
"ORDER BY time DESC LIMIT 1",
|
||||||
|
(ziel + off, ziel + off - 900)).fetchone()
|
||||||
|
if row:
|
||||||
|
werte[feld] = (row["c"] - r["price"]) * d / r["atr"]
|
||||||
|
sets, args = [], []
|
||||||
|
for feld in ("ret30", "ret60"):
|
||||||
|
if feld in werte and r[feld] is None:
|
||||||
|
sets.append(f"{feld}=?"); args.append(round(werte[feld], 4))
|
||||||
|
sets.append(f"hit{feld[3:]}=?"); args.append(1 if werte[feld] > 0 else 0)
|
||||||
|
faellig60 = now >= r["ts"] + 60 * 60
|
||||||
|
if faellig60 and ("ret60" in werte or r["ret60"] is not None):
|
||||||
|
sets.append("done=?"); args.append(1)
|
||||||
|
elif now >= r["ts"] + 6 * 3600:
|
||||||
|
# Nach 6 h keine Bars mehr zu erwarten (Wochenende/Ausfall) →
|
||||||
|
# schliessen, damit die Zeile die Auswertung nicht ewig belegt.
|
||||||
|
sets.append("done=?"); args.append(1)
|
||||||
|
if sets:
|
||||||
|
args.append(r["id"])
|
||||||
|
con.execute(f"UPDATE rec_outcomes SET {', '.join(sets)} WHERE id=?",
|
||||||
|
args)
|
||||||
|
con.commit(); con.close()
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"rec_outcomes auswerten: {e}")
|
||||||
|
|
||||||
def squeeze_monitor(self) -> dict:
|
def squeeze_monitor(self) -> dict:
|
||||||
"""Auto-Squeeze-Monitor (B4) für das Dashboard: (1) LIVE-Trades (echt, DB —
|
"""Auto-Squeeze-Monitor (B4) für das Dashboard: (1) LIVE-Trades (echt, DB —
|
||||||
das Gesamtsystem inkl. realer Exits) und (2) die MECHANISCHE Neuberechnung
|
das Gesamtsystem inkl. realer Exits) und (2) die MECHANISCHE Neuberechnung
|
||||||
@@ -3576,6 +3725,8 @@ class TradingEngine:
|
|||||||
"news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)(
|
"news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)(
|
||||||
(self.news.sentiment or {}).get("score")),
|
(self.news.sentiment or {}).get("score")),
|
||||||
"alignment": self._alignment_cached(),
|
"alignment": self._alignment_cached(),
|
||||||
|
"block_mix": self._block_mix_cached(),
|
||||||
|
"rec_track": self._rec_track_cached(),
|
||||||
# Kennzahlen der MANUELLEN Trades für die gleichnamige Karte
|
# Kennzahlen der MANUELLEN Trades für die gleichnamige Karte
|
||||||
# (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße
|
# (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße
|
||||||
# Snapshot-Pfad nicht je Abruf die DB aggregiert.
|
# Snapshot-Pfad nicht je Abruf die DB aggregiert.
|
||||||
|
|||||||
+114
-30
@@ -145,6 +145,22 @@ SCHEMA = [
|
|||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
"""
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS rec_outcomes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ts INTEGER, -- lokale Epoch, Beginn der EPISODE
|
||||||
|
signal TEXT, -- 'LONG' | 'SHORT'
|
||||||
|
conf_pct INTEGER,
|
||||||
|
tf TEXT,
|
||||||
|
price REAL, -- Kurs bei Ausgabe der Empfehlung
|
||||||
|
atr REAL, -- ATR_M5 zur Normierung
|
||||||
|
ret30 REAL, -- (Kurs[+30min] - price) x Richtung / atr
|
||||||
|
ret60 REAL,
|
||||||
|
hit30 INTEGER, -- 1 = Kurs lief in Signalrichtung
|
||||||
|
hit60 INTEGER,
|
||||||
|
done INTEGER -- 1 = beide Horizonte ausgewertet
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS verdict_votes (
|
CREATE TABLE IF NOT EXISTS verdict_votes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ts INTEGER, -- lokale Epoch
|
ts INTEGER, -- lokale Epoch
|
||||||
@@ -420,6 +436,52 @@ class HistoryLogger:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return cur.lastrowid
|
return cur.lastrowid
|
||||||
|
|
||||||
|
def log_rec_outcome(self, *, signal: str, conf_pct: int, tf: str,
|
||||||
|
price: float, atr: float) -> int:
|
||||||
|
"""Loggt EINE Empfehlungs-EPISODE zur spaeteren Selbst-Kalibrierung.
|
||||||
|
|
||||||
|
⚠ WARUM EPISODEN UND NICHT ZEILEN: die Empfehlung wird ~1x/Minute
|
||||||
|
geloggt; eine Marktlage erzeugt dutzende identische Zeilen. Wer die zaehlt,
|
||||||
|
haelt 20.000 Faelle fuer eine Stichprobe, obwohl es 1.000 sind
|
||||||
|
(`analyze_signal_live.py`). Hier wird genau EINE Zeile je Richtungswechsel
|
||||||
|
geschrieben.
|
||||||
|
|
||||||
|
Ausgewertet wird separat (`engine._evaluate_rec_outcomes`) gegen
|
||||||
|
`candles_m1` — dasselbe Muster wie `pbreak_predictions`, das den Ausfall
|
||||||
|
des P(break)-Modells live aufgedeckt hat. Rueckgabe = Zeilen-ID."""
|
||||||
|
with self._lock, self._connect() as conn:
|
||||||
|
cur = conn.execute("""
|
||||||
|
INSERT INTO rec_outcomes (ts, signal, conf_pct, tf, price, atr)
|
||||||
|
VALUES (?,?,?,?,?,?)
|
||||||
|
""", (int(time.time()), signal, int(conf_pct or 0), tf or "",
|
||||||
|
float(price or 0.0), float(atr or 0.0)))
|
||||||
|
conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
|
||||||
|
def rec_accuracy(self, last_n: int = 200) -> dict:
|
||||||
|
"""Trefferquote der letzten `last_n` ausgewerteten Empfehlungs-Episoden.
|
||||||
|
|
||||||
|
⚠ Die Trefferquote ist gegen 50 % zu lesen, nicht gegen 0 — und der
|
||||||
|
Ø-Return gegen die Drift des Zeitraums. Gemessen ueber 1.071 Episoden lag
|
||||||
|
die Richtungstreffer-Quote bei 48–51 % (`analyze_signal_live.py`); diese
|
||||||
|
Zeile macht laufend sichtbar, ob sich daran etwas aendert."""
|
||||||
|
with self._connect() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT signal, ret30, ret60, hit30, hit60 FROM rec_outcomes
|
||||||
|
WHERE done = 1 ORDER BY ts DESC LIMIT ?
|
||||||
|
""", (int(last_n),)).fetchall()
|
||||||
|
if not rows:
|
||||||
|
return {"n": 0}
|
||||||
|
def q(feld, hit):
|
||||||
|
v = [r[feld] for r in rows if r[feld] is not None]
|
||||||
|
h = [r[hit] for r in rows if r[hit] is not None]
|
||||||
|
return ((round(100.0 * sum(h) / len(h), 1) if h else None),
|
||||||
|
(round(sum(v) / len(v), 3) if v else None), len(h))
|
||||||
|
wr30, avg30, n30 = q("ret30", "hit30")
|
||||||
|
wr60, avg60, n60 = q("ret60", "hit60")
|
||||||
|
return {"n": len(rows), "wr30": wr30, "avg30": avg30, "n30": n30,
|
||||||
|
"wr60": wr60, "avg60": avg60, "n60": n60}
|
||||||
|
|
||||||
def pbreak_accuracy(self, period: str = "all") -> dict:
|
def pbreak_accuracy(self, period: str = "all") -> dict:
|
||||||
"""Trefferquote der Abprall/Durchbruch-Prognose (nur ausgewertete Zeilen)."""
|
"""Trefferquote der Abprall/Durchbruch-Prognose (nur ausgewertete Zeilen)."""
|
||||||
since, until = self._range_to_ts(period)
|
since, until = self._range_to_ts(period)
|
||||||
@@ -521,38 +583,60 @@ class HistoryLogger:
|
|||||||
"pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)}
|
"pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def alignment_stats(self, last_n: int = 40) -> dict:
|
def alignment_stats(self, last_n: int = 0) -> dict:
|
||||||
"""Ausrichtungs-Split der letzten N geschlossenen Trades: liefen sie MIT,
|
"""Ausrichtungs-Split: liefen die Trades MIT, GEGEN oder OHNE die Empfehlung?
|
||||||
GEGEN oder OHNE die Empfehlung (rec_signal beim Entry)? Für die ehrliche
|
|
||||||
Verhaltens-Anzeige im Order-Bestätigungsdialog (B0 als Software — die
|
⚠⚠ ZWEI AENDERUNGEN AM 2026-08-06, beide gemessen begruendet:
|
||||||
gemessene Kern-Leckage sind diskretionäre Gegen-Signal-Trades)."""
|
(1) **Volle Historie statt der letzten 40 Trades** (`last_n=0`). Ueber
|
||||||
|
1.216 geschlossene Trades ist der Befund beidhaelftig robust; ueber 40
|
||||||
|
war er eine Momentaufnahme. Das ist die einzige Richtungsaussage ueber
|
||||||
|
die Gesamtempfehlung, die den 2-Stichproben-Test besteht — sie sollte
|
||||||
|
auf der ganzen Stichprobe stehen.
|
||||||
|
(2) **Zusaetzlich JE LOT normiert** (`pnl_lot`). Absolute Euro-Vergleiche
|
||||||
|
ueber Zeitraeume mit unterschiedlicher Positionsgroesse sind ungueltig
|
||||||
|
— am 04.08. sah ein reiner Groessenzuwachs wie ein Strategie-Effekt aus
|
||||||
|
(Ø +55 € gegen +19 €, je Lot aber identisch). Das Order-Dialog-Feld
|
||||||
|
zeigt deshalb beides.
|
||||||
|
|
||||||
|
Gemessener Stand (06.08., ganze Historie, je Lot):
|
||||||
|
MIT n=354 WR 56,5 % +3,22 €/Lot (H1 +0,37 / H2 +5,62)
|
||||||
|
GEGEN n= 87 WR 40,2 % −6,60 €/Lot (H1 −7,01 / H2 −5,68)
|
||||||
|
OHNE n=775 WR 56,6 % +0,36 €/Lot (kippt zwischen den Haelften)
|
||||||
|
MIT und OHNE sind praktisch gleich — der Empfehlung zu FOLGEN bringt
|
||||||
|
gemessen nichts, nur sie zu VERLETZEN kostet. Sie ist ein Veto, keine
|
||||||
|
Prognose.
|
||||||
|
"""
|
||||||
|
sql = ("SELECT direction, rec_signal, pnl, lots FROM trades "
|
||||||
|
"WHERE exit_time IS NOT NULL AND pnl IS NOT NULL AND lots > 0 "
|
||||||
|
"ORDER BY exit_time DESC")
|
||||||
|
args: tuple = ()
|
||||||
|
if last_n and last_n > 0:
|
||||||
|
sql += " LIMIT ?"
|
||||||
|
args = (last_n,)
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
rows = conn.execute("""
|
rows = conn.execute(sql, args).fetchall()
|
||||||
SELECT direction, rec_signal, pnl FROM trades
|
|
||||||
WHERE exit_time IS NOT NULL AND pnl IS NOT NULL
|
def fasse(grp: list) -> dict:
|
||||||
ORDER BY exit_time DESC LIMIT ?
|
|
||||||
""", (last_n,)).fetchall()
|
|
||||||
out = {}
|
|
||||||
for key, match in (("with", True), ("against", False)):
|
|
||||||
grp = []
|
|
||||||
for r in rows:
|
|
||||||
rec = (r["rec_signal"] or "").upper()
|
|
||||||
if rec not in ("LONG", "SHORT"):
|
|
||||||
continue
|
|
||||||
aligned = (rec == "LONG") == (r["direction"] == "BUY")
|
|
||||||
if aligned == match:
|
|
||||||
grp.append(r["pnl"])
|
|
||||||
n = len(grp)
|
n = len(grp)
|
||||||
out[key] = {"n": n,
|
if not n:
|
||||||
"wr": round(100 * sum(1 for p in grp if p > 0) / n) if n else None,
|
return {"n": 0, "wr": None, "pnl": 0.0, "pnl_lot": None}
|
||||||
"pnl": round(sum(grp), 2)}
|
pnl = sum(r["pnl"] for r in grp)
|
||||||
none_grp = [r["pnl"] for r in rows
|
lots = sum(r["lots"] for r in grp) or 0.0
|
||||||
if (r["rec_signal"] or "").upper() not in ("LONG", "SHORT")]
|
return {"n": n,
|
||||||
n = len(none_grp)
|
"wr": round(100 * sum(1 for r in grp if r["pnl"] > 0) / n),
|
||||||
out["none"] = {"n": n,
|
"pnl": round(pnl, 2),
|
||||||
"wr": round(100 * sum(1 for p in none_grp if p > 0) / n) if n else None,
|
"pnl_lot": round(pnl / lots, 2) if lots > 0 else None}
|
||||||
"pnl": round(sum(none_grp), 2)}
|
|
||||||
return out
|
mit, gegen, ohne = [], [], []
|
||||||
|
for r in rows:
|
||||||
|
rec = (r["rec_signal"] or "").upper()
|
||||||
|
if rec not in ("LONG", "SHORT"):
|
||||||
|
ohne.append(r)
|
||||||
|
elif (rec == "LONG") == (r["direction"] == "BUY"):
|
||||||
|
mit.append(r)
|
||||||
|
else:
|
||||||
|
gegen.append(r)
|
||||||
|
return {"with": fasse(mit), "against": fasse(gegen), "none": fasse(ohne)}
|
||||||
|
|
||||||
# ══════════════════════════════════════════
|
# ══════════════════════════════════════════
|
||||||
# STATISTIK-QUERIES
|
# STATISTIK-QUERIES
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""Szenario-Test der Selbst-Kalibrierung (rec_outcomes) — ohne MT5, auf DB-Kopie.
|
||||||
|
|
||||||
|
Geprueft werden die Faelle, die live schiefgehen koennen:
|
||||||
|
1) Episode wird EINMAL geloggt, nicht je Minute
|
||||||
|
2) WARTEN re-armt → die naechste Richtung ist eine neue Episode
|
||||||
|
3) Auswertung rechnet den Forward-Return korrekt (inkl. Broker-Offset)
|
||||||
|
4) fehlende M1-Bars → Zeile bleibt offen statt falsch bewertet
|
||||||
|
5) rec_accuracy aggregiert Trefferquote und Ø-Return richtig
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
|
||||||
|
sys.path.insert(0, r"c:\Users\ah\Downloads\tradingrobot")
|
||||||
|
|
||||||
|
from core.engine import TradingEngine
|
||||||
|
from core.history import HistoryLogger
|
||||||
|
|
||||||
|
QUELL = r"c:\Users\ah\Downloads\tradingrobot\oil_widget_history.db"
|
||||||
|
TMP = os.path.join(tempfile.mkdtemp(), "t.db")
|
||||||
|
shutil.copy(QUELL, TMP)
|
||||||
|
con = sqlite3.connect(TMP)
|
||||||
|
con.execute("DELETE FROM rec_outcomes")
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
h = HistoryLogger(TMP)
|
||||||
|
fehler = []
|
||||||
|
|
||||||
|
|
||||||
|
def pruef(lbl, ist, soll):
|
||||||
|
ok = ist == soll
|
||||||
|
if not ok:
|
||||||
|
fehler.append(lbl)
|
||||||
|
print(f" {'OK ' if ok else 'FEHLER'} {lbl:<50} ist={ist!r:<14} soll={soll!r}")
|
||||||
|
|
||||||
|
|
||||||
|
print("=" * 86)
|
||||||
|
print(" 1) Episoden-Logging: eine Zeile je Richtungswechsel, nicht je Minute")
|
||||||
|
print("=" * 86)
|
||||||
|
|
||||||
|
|
||||||
|
class E:
|
||||||
|
"""Nur der Episoden-Teil aus `_run_analysis`, isoliert nachgestellt."""
|
||||||
|
def __init__(self):
|
||||||
|
self._rec_epi_sig = None
|
||||||
|
self.history = h
|
||||||
|
|
||||||
|
def tick(self, sig):
|
||||||
|
if sig in ("LONG", "SHORT") and sig != self._rec_epi_sig:
|
||||||
|
self.history.log_rec_outcome(signal=sig, conf_pct=70, tf="M5",
|
||||||
|
price=75.0, atr=0.2)
|
||||||
|
if sig in ("LONG", "SHORT", "WARTEN"):
|
||||||
|
self._rec_epi_sig = sig if sig != "WARTEN" else None
|
||||||
|
|
||||||
|
|
||||||
|
e = E()
|
||||||
|
for s in ["LONG"] * 5: # dieselbe Episode, 5 Minuten lang
|
||||||
|
e.tick(s)
|
||||||
|
pruef("5x LONG hintereinander → 1 Zeile",
|
||||||
|
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 1)
|
||||||
|
for s in ["SHORT"] * 3: # Flip = neue Episode
|
||||||
|
e.tick(s)
|
||||||
|
pruef("Flip auf SHORT → 2 Zeilen",
|
||||||
|
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 2)
|
||||||
|
e.tick("WARTEN"); e.tick("WARTEN")
|
||||||
|
pruef("WARTEN erzeugt keine Zeile",
|
||||||
|
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 2)
|
||||||
|
e.tick("SHORT")
|
||||||
|
pruef("nach WARTEN ist SHORT eine NEUE Episode",
|
||||||
|
con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 3)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 86)
|
||||||
|
print(" 2) Auswertung gegen candles_m1 (mit Broker-Offset)")
|
||||||
|
print("=" * 86)
|
||||||
|
con.execute("DELETE FROM rec_outcomes")
|
||||||
|
con.execute("DELETE FROM candles_m1 WHERE symbol='TESTSYM'")
|
||||||
|
OFF = 3 * 3600
|
||||||
|
t0 = int(time.time()) - 7200 # vor 2 h → beide Horizonte faellig
|
||||||
|
# LONG @ 75.00, ATR 0.20; nach 30 min steht 75.10 (+0,5 ATR), nach 60 min 74.90
|
||||||
|
con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) "
|
||||||
|
"VALUES (?,?,?,?,?,?)", (t0, "LONG", 70, "M5", 75.00, 0.20))
|
||||||
|
for minuten, kurs in ((30, 75.10), (60, 74.90)):
|
||||||
|
con.execute("INSERT OR REPLACE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol)"
|
||||||
|
" VALUES (?,?,?,?,?,?,?,?)",
|
||||||
|
(t0 + minuten * 60 + OFF, kurs, kurs, kurs, kurs, 20, 1, "TESTSYM"))
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
eng = TradingEngine.__new__(TradingEngine)
|
||||||
|
eng._rec_eval_ts = 0.0
|
||||||
|
eng.trader = types.SimpleNamespace(_broker_offset_s=lambda: OFF)
|
||||||
|
import core.engine as CE
|
||||||
|
_alt = CE.HISTORY_DB_FILE
|
||||||
|
CE.HISTORY_DB_FILE = TMP
|
||||||
|
eng._evaluate_rec_outcomes()
|
||||||
|
CE.HISTORY_DB_FILE = _alt
|
||||||
|
|
||||||
|
r = con.execute("SELECT ret30,hit30,ret60,hit60,done FROM rec_outcomes").fetchone()
|
||||||
|
pruef("ret30 = (75.10-75.00)/0.20 = +0.5", round(r[0], 3), 0.5)
|
||||||
|
pruef("hit30 = 1 (lief in Signalrichtung)", r[1], 1)
|
||||||
|
pruef("ret60 = (74.90-75.00)/0.20 = -0.5", round(r[2], 3), -0.5)
|
||||||
|
pruef("hit60 = 0", r[3], 0)
|
||||||
|
pruef("done = 1", r[4], 1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 86)
|
||||||
|
print(" 3) SHORT-Richtung spiegelbildlich · fehlende Bars → bleibt offen")
|
||||||
|
print("=" * 86)
|
||||||
|
con.execute("DELETE FROM rec_outcomes")
|
||||||
|
# ⚠ Test-Kerzen aus Abschnitt 2 MUESSEN weg — sonst faellt die dortige
|
||||||
|
# 60-Minuten-Bar in das Fenster dieses Abschnitts (beide Anker liegen 2 h
|
||||||
|
# zurueck) und der Fall "keine Bar" waere gar nicht getestet.
|
||||||
|
con.execute("DELETE FROM candles_m1 WHERE symbol='TESTSYM'")
|
||||||
|
t1 = int(time.time()) - 7200
|
||||||
|
con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) "
|
||||||
|
"VALUES (?,?,?,?,?,?)", (t1, "SHORT", 70, "M5", 75.00, 0.20))
|
||||||
|
con.execute("INSERT OR REPLACE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol)"
|
||||||
|
" VALUES (?,?,?,?,?,?,?,?)",
|
||||||
|
(t1 + 1800 + OFF, 74.9, 74.9, 74.9, 74.9, 20, 1, "TESTSYM"))
|
||||||
|
# fuer 60 min BEWUSST keine Bar → Zeile darf nicht faelschlich bewertet werden.
|
||||||
|
# ⚠ Dafuer muss das 60-Minuten-Fenster WIRKLICH leer sein: die DB-Kopie enthaelt
|
||||||
|
# ECHTE candles_m1 aus dem Live-Betrieb, und die liegen genau hier. Ohne dieses
|
||||||
|
# DELETE testet man nicht "keine Bar", sondern "echte Bar" (erster Versuch lieferte
|
||||||
|
# ret60 = -14,985 = der reale Kurs ~78).
|
||||||
|
con.execute("DELETE FROM candles_m1 WHERE time > ? AND time <= ?",
|
||||||
|
(t1 + 3600 + OFF - 900, t1 + 3600 + OFF))
|
||||||
|
con.commit()
|
||||||
|
eng._rec_eval_ts = 0.0
|
||||||
|
CE.HISTORY_DB_FILE = TMP
|
||||||
|
eng._evaluate_rec_outcomes()
|
||||||
|
CE.HISTORY_DB_FILE = _alt
|
||||||
|
r = con.execute("SELECT ret30,hit30,ret60,done FROM rec_outcomes").fetchone()
|
||||||
|
pruef("SHORT: Kurs faellt → ret30 = +0.5", round(r[0], 3), 0.5)
|
||||||
|
pruef("SHORT: hit30 = 1", r[1], 1)
|
||||||
|
pruef("ret60 ohne Bar bleibt NULL", r[2], None)
|
||||||
|
pruef("done bleibt offen (0/None)", bool(r[3]), False)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 86)
|
||||||
|
print(" 4) rec_accuracy aggregiert")
|
||||||
|
print("=" * 86)
|
||||||
|
con.execute("DELETE FROM rec_outcomes")
|
||||||
|
for ret, hit in ((0.5, 1), (0.3, 1), (-0.4, 0), (-0.2, 0)):
|
||||||
|
con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr,"
|
||||||
|
"ret30,hit30,ret60,hit60,done) VALUES (?,?,?,?,?,?,?,?,?,?,1)",
|
||||||
|
(int(time.time()), "LONG", 70, "M5", 75.0, 0.2, ret, hit, ret, hit))
|
||||||
|
con.commit()
|
||||||
|
a = h.rec_accuracy(200)
|
||||||
|
pruef("n = 4", a["n"], 4)
|
||||||
|
pruef("Trefferquote 30' = 50 %", a["wr30"], 50.0)
|
||||||
|
pruef("Ø-Return 30' = +0.05", a["avg30"], 0.05)
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
print()
|
||||||
|
print("=" * 86)
|
||||||
|
print(" ERGEBNIS: " + ("alle Szenarien korrekt" if not fehler
|
||||||
|
else f"{len(fehler)} FEHLER: {fehler}"))
|
||||||
|
print("=" * 86)
|
||||||
|
sys.exit(1 if fehler else 0)
|
||||||
+56
-2
@@ -172,6 +172,52 @@ function renderVerdict(v, snap) {
|
|||||||
`<span class="m-vote">${a}</span></div>` +
|
`<span class="m-vote">${a}</span></div>` +
|
||||||
`<div class="m-det">${mod.detail || ""}</div></div>`;
|
`<div class="m-det">${mod.detail || ""}</div></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
|
// ── Selbst-Kalibrierung: die Karte zeigt ihre eigene Trefferquote ─────
|
||||||
|
// ⚠ Gegen 50 % zu lesen, nicht gegen 0 — es ist eine Richtungsaussage.
|
||||||
|
// Ausgangsstand (analyze_signal_live.py, 1.071 Episoden): 48–51 %.
|
||||||
|
// Bis genug Episoden ausgewertet sind, steht hier ehrlich „sammelt noch".
|
||||||
|
// Null-sicher (s. Cache-Regel): neue IDs treffen evtl. auf alte, gecachte HTML.
|
||||||
|
const trkEl = $("vd-selfcal");
|
||||||
|
if (trkEl) {
|
||||||
|
const tr = snap && snap.rec_track;
|
||||||
|
if (tr && tr.n >= 20 && tr.wr30 != null) {
|
||||||
|
const d = (x) => (x == null ? "—" : (x >= 0 ? "+" : "") + fmt(x, 3));
|
||||||
|
const cls = tr.wr30 >= 55 ? "up" : tr.wr30 <= 45 ? "down" : "";
|
||||||
|
trkEl.className = "vd-selfcal " + cls;
|
||||||
|
trkEl.textContent =
|
||||||
|
`🎯 eigene Trefferquote (letzte ${tr.n} Episoden): ${tr.wr30} % auf 30′`
|
||||||
|
+ (tr.wr60 != null ? ` · ${tr.wr60} % auf 60′` : "")
|
||||||
|
+ ` · Ø ${d(tr.avg30)}×ATR`;
|
||||||
|
trkEl.title = "Selbst-Kalibrierung: jede gerichtete Empfehlung wird nach "
|
||||||
|
+ "30 und 60 Minuten gegen den echten Kurs ausgewertet. Gegen 50 % lesen, "
|
||||||
|
+ "nicht gegen 0. Gemessener Ausgangsstand über 1.071 Episoden: 48–51 %.";
|
||||||
|
} else {
|
||||||
|
trkEl.className = "vd-selfcal dim";
|
||||||
|
trkEl.textContent = `🎯 eigene Trefferquote: sammelt noch`
|
||||||
|
+ (tr && tr.n ? ` (${tr.n} von 20 Episoden ausgewertet)` : "");
|
||||||
|
trkEl.title = "Jede gerichtete Empfehlung wird nach 30/60 Minuten gegen "
|
||||||
|
+ "den echten Kurs ausgewertet. Ab 20 ausgewerteten Episoden erscheint "
|
||||||
|
+ "die Quote hier.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Warum WARTEN? Verteilung der blockierenden Gates (heute) ───────────
|
||||||
|
const blkEl = $("vd-blocks");
|
||||||
|
if (blkEl) {
|
||||||
|
const bm = snap && snap.block_mix;
|
||||||
|
if (bm && bm.total) {
|
||||||
|
const pc = (n) => Math.round(100 * n / bm.total);
|
||||||
|
const top = (bm.top || []).map(x => `${x.grund} ${pc(x.n)} %`).join(" · ");
|
||||||
|
blkEl.textContent = `⛔ heute ${pc(bm.frei)} % ohne Block`
|
||||||
|
+ (top ? ` · ${top}` : "");
|
||||||
|
blkEl.title = "Welches Gate hält die Empfehlung gerade zurück — über den "
|
||||||
|
+ "heutigen Tag gezählt. Ohne diese Zeile war „warum gab es so wenige "
|
||||||
|
+ "Empfehlungen?" + '"' + " nur per Datenbank-Abfrage zu beantworten.";
|
||||||
|
} else {
|
||||||
|
blkEl.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1171,8 +1217,16 @@ document.querySelectorAll(".btn[data-side]").forEach(btn => {
|
|||||||
const isWait0 = (sigNow === "WARTEN" || sigNow === "—");
|
const isWait0 = (sigNow === "WARTEN" || sigNow === "—");
|
||||||
if (al && cl.items.some(x => x.key === "signal" && x.state === "fail")) {
|
if (al && cl.items.some(x => x.key === "signal" && x.state === "fail")) {
|
||||||
const grp = isWait0 ? al.none : al.against, ref = al.with;
|
const grp = isWait0 ? al.none : al.against, ref = al.with;
|
||||||
const f = g => (g && g.n >= 5)
|
// ⚠ JE LOT ist die belastbare Zahl: absolute Euro-Vergleiche sind bei
|
||||||
? `${g.wr}% Treffer · ${g.pnl >= 0 ? "+" : ""}${fmt(g.pnl, 0)} € (n${g.n})` : null;
|
// wechselnder Positionsgroesse ungueltig (Lehre 04.08. — ein reiner
|
||||||
|
// Groessenzuwachs sah wie ein Strategie-Effekt aus). Seit 06.08. laeuft
|
||||||
|
// der Split ueber die VOLLE Historie (1.216 Trades) statt ueber 40.
|
||||||
|
const f = g => {
|
||||||
|
if (!g || g.n < 5) return null;
|
||||||
|
const lot = (g.pnl_lot === null || g.pnl_lot === undefined) ? ""
|
||||||
|
: ` · ${g.pnl_lot >= 0 ? "+" : ""}${fmt(g.pnl_lot, 2)} €/Lot`;
|
||||||
|
return `${g.wr}% Treffer${lot} · ${g.pnl >= 0 ? "+" : ""}${fmt(g.pnl, 0)} € (n${g.n})`;
|
||||||
|
};
|
||||||
const own = f(grp), rf = f(ref);
|
const own = f(grp), rf = f(ref);
|
||||||
if (own) body += `\n\n📊 Deine Trades ${isWait0 ? "OHNE" : "GEGEN"} Signal: ${own}`;
|
if (own) body += `\n\n📊 Deine Trades ${isWait0 ? "OHNE" : "GEGEN"} Signal: ${own}`;
|
||||||
if (rf) body += `\n📊 Mit Signal: ${rf}`;
|
if (rf) body += `\n📊 Mit Signal: ${rf}`;
|
||||||
|
|||||||
+11
-2
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
<title>Oil · MT5</title>
|
<title>Oil · MT5</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="stylesheet" href="style.css?v=143">
|
<link rel="stylesheet" href="style.css?v=144">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -106,6 +106,15 @@
|
|||||||
Einstiegspreis und Exit, nicht aus der Trefferquote — lies das hier als
|
Einstiegspreis und Exit, nicht aus der Trefferquote — lies das hier als
|
||||||
<b>Auslöser für eine Order am Level</b>, nicht als Kursprognose.
|
<b>Auslöser für eine Order am Level</b>, nicht als Kursprognose.
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Warum WARTEN? Verteilung der blockierenden Gates über den heutigen Tag.
|
||||||
|
Ohne die Zeile war „warum gab es so wenige Empfehlungen?" nur per
|
||||||
|
DB-Abfrage zu beantworten (real 06.08.: 6 von 646 Zeilen ohne Block). -->
|
||||||
|
<div class="vd-blocks" id="vd-blocks"></div>
|
||||||
|
<!-- SELBST-KALIBRIERUNG (2026-08-06): die Karte zeigt ihre eigene
|
||||||
|
Trefferquote. Muster wie `pbreak_predictions` — das hat den Ausfall des
|
||||||
|
P(break)-Modells LIVE aufgedeckt, was kein Backtest konnte. ⚠ Gegen
|
||||||
|
50 % zu lesen, nicht gegen 0. -->
|
||||||
|
<div class="vd-selfcal dim" id="vd-selfcal"></div>
|
||||||
<div class="vd-meter">
|
<div class="vd-meter">
|
||||||
<span class="vd-end short">SHORT</span>
|
<span class="vd-end short">SHORT</span>
|
||||||
<div class="vd-track"><div class="vd-needle" id="vd-needle"></div></div>
|
<div class="vd-track"><div class="vd-needle" id="vd-needle"></div></div>
|
||||||
@@ -331,6 +340,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="app.js?v=143"></script>
|
<script src="app.js?v=144"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -221,6 +221,16 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
|
|||||||
border:1px dashed var(--line);border-radius:var(--r-sm);
|
border:1px dashed var(--line);border-radius:var(--r-sm);
|
||||||
padding:7px 10px;margin:0 0 11px}
|
padding:7px 10px;margin:0 0 11px}
|
||||||
.vd-note b{color:var(--bright);font-weight:700}
|
.vd-note b{color:var(--bright);font-weight:700}
|
||||||
|
/* Block-Verteilung + Selbst-Kalibrierung: beide bewusst leise (Kontext, keine
|
||||||
|
Handlungsaufforderung). Die Trefferquote faerbt nur bei klarer Abweichung von
|
||||||
|
den gemessenen ~50 %, damit Rauschen nicht wie ein Signal aussieht. */
|
||||||
|
.vd-blocks{font-size:12px;color:var(--dim);margin:0 0 4px;font-family:var(--mono)}
|
||||||
|
/* ⚠ NICHT `.vd-track` nennen — so heisst bereits die Schiene der Bias-Nadel
|
||||||
|
(`.vd-meter .vd-track`). Eine gleichnamige Klasse haette sie ueberschrieben
|
||||||
|
und die Nadel unsichtbar gemacht. */
|
||||||
|
.vd-selfcal{font-size:12px;color:var(--dim);margin:0 0 9px;font-family:var(--mono)}
|
||||||
|
.vd-selfcal.up{color:var(--up)} .vd-selfcal.down{color:var(--down)}
|
||||||
|
.vd-selfcal.dim{color:var(--dim);opacity:.75}
|
||||||
.vd-main{flex:1;min-width:0}
|
.vd-main{flex:1;min-width:0}
|
||||||
.vd-signal{font-size:30px;font-weight:800;color:var(--dim);line-height:1.1}
|
.vd-signal{font-size:30px;font-weight:800;color:var(--dim);line-height:1.1}
|
||||||
.vd-signal.long{color:var(--up)} .vd-signal.short{color:var(--down)}
|
.vd-signal.long{color:var(--up)} .vd-signal.short{color:var(--down)}
|
||||||
|
|||||||
Reference in New Issue
Block a user