Files
AH-Oil-Trader/tests/test_system_status.py
Axel HocksandClaude Opus 5 0dd2e524b7 S/R-Auto-Close komplett entfernt + Circuit-Breaker als runder CB-Knopf (v=210)
(1) S/R-AUTO-CLOSE RAUS (User-Vorgabe: "die brauche ich nicht mehr").
Der Schalter stand seit dem 19.08. ohnehin auf AUS - abgeschaltet, nachdem
analyze_pbreak_live.py das P(break)-Modell live als INVERTIERT gemessen hatte
(AUC 0,452, Klassen monoton rueckwaerts). Ohne funktionierendes Gate ist der
S/R-Close faktisch der PAUSCHALE, und der ist 2x gemessen und verworfen
(Gewinner-Kappen). Die Entfernung vollzieht den gemessenen Stand nach.

WEG: _check_sr_close, set_sr_autoclose, set_sr_close_min_gain, die
Konstruktor-Felder, die %-Armierung des Gewinn-Close, drei Snapshot-Felder,
beide runtime_state-Schluessel, die Reichweiten- und Systemstatus-Zeilen,
/api/srclose und /api/srclose_min, der Knopf S/R, das Gewinn-Close-Feld, der
Toast "Bot hat am Level geschlossen".
BLEIBT: `_sr_close_hint` (App-Hinweis, P(break)-Chartlinien, Copilot-Kontext,
pbreak_predictions-Tracking), `stop_approach`, das Modell selbst und die
closed_by=sr_close-Historie in der DB. Live gegengeprueft: die drei Felder sind
aus dem Snapshot verschwunden, sr_close_hint und stop_approach sind da, beide
Endpunkte antworten 405.
Sicherung: .removed_backup/sr_auto_close_2026-08-21.py

⚠ Der Close-Alarm gilt jetzt schlicht ab jedem Gewinn (`minGain = 0`) - die
Schwelle gab es nur fuer den Auto-Close. Das ist der Zustand von vor 2026-07.

⚠ Der Test test_automatik_zeile_nennt_alle_schalter war MITGEZAEHLT und faellt
korrekt - er ist angepasst, nicht entfernt: jetzt zwei Schalter statt drei, und
die Negativ-Pruefungen ("SIG"/"S/R-Close" duerfen NICHT auftauchen) sind der
eigentliche Wert, denn ein wieder auftauchender Pfad waere ein stiller Rueckbau.

(2) CIRCUIT BREAKER als runder Knopf "CB" rechts neben BALANCE.
⚠ Farb-Logik wie vorgegeben: GRUEN = inaktiv (aus), ROT = aktiv (scharf).
Bewusst NICHT die uebliche "gruen = geschuetzt"-Lesart - hier signalisiert Rot,
dass eine Automatik eingreifen kann. Im Code und im Markup steht das dran, damit
es niemand spaeter "korrigiert".
⚠ VERSCHOBEN, nicht kopiert: zwei Elemente mit derselben ID waeren ein stiller
Fehler, weil getElementById nur das erste findet.

⚠ Beim Entfernen zwei eigene Fehler: (a) ein Slice ueber `rindex("try:")`
schnitt durch fremde Konstruktor-Bloecke und riss Kommentar-Fragmente heraus -
zurueckgesetzt und zeilenbasiert von hinten neu gemacht; (b) beim Toast-Block
eine Klammer zu viel entfernt, von `node --check` sofort gefangen.

Pipeline A-G gruen, 97 Tests, 14 render*-Funktionen, deployt und am
ausgelieferten HTML/Snapshot verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:47:44 +02:00

110 lines
4.3 KiB
Python

"""Der Systemzustand im Tagesreport — sagt er die Wahrheit, und faellt er nie aus?
ANLASS (2026-08-11). Das Projekt hat sieben Waechter, und sie funktionieren. Was
fehlte, war der letzte Meter: die Marker-Datei `cfg_auto_sr_close` traegt den
Zeitstempel **06.08. 18:10** und wurde danach NIE wieder gemeldet — fuenf Tage
lang stand ein Exit aus, der am 11.08. mit **+350,81 EUR ueber 43 Trades**
gemessen wurde. Ursache ist ein Kategorienfehler: die Marker-Logik behandelt
einen DAUERZUSTAND wie ein EINMAL-EREIGNIS.
Deshalb die Trennung: Popup = „etwas Neues", Tagesreport = „so steht es gerade".
⚠ Die wichtigste Eigenschaft dieses Blocks ist, dass er NIE den Report sprengt.
Ein Report, der wegen der Statuszeile gar nicht ankommt, waere schlimmer als
gar keine Statuszeile — deshalb prueft der Test vor allem die Ausfallpfade.
"""
from __future__ import annotations
import pytest
from core.engine import TradingEngine
from core.history import HistoryLogger
class _Hist:
def __init__(self, puls):
self._puls = puls
def telemetrie_puls(self):
if isinstance(self._puls, Exception):
raise self._puls
return self._puls
class _Stub:
_system_status = TradingEngine._system_status
def __init__(self, *, brk=False, src=True, flip=False, puls=None):
self._auto_squeeze = brk
self._auto_sr_close = src
self._flip_close = flip
self.history = _Hist(puls if puls is not None else [])
def _text(z):
return "\n".join(z)
def test_automatik_zeile_nennt_alle_schalter():
"""⚠ Seit dem 2026-08-21 sind es ZWEI. Entfernt wurden der Auto-Signal-Pfad
(SIG, am 12.08.) und der S/R-Auto-Close (am 21.08., User-Vorgabe — er stand
seit dem 19.08. ohnehin aus, weil das P(break)-Modell live INVERTIERT misst).
Der Test zaehlt sie bewusst einzeln auf: verschwindet einer stillschweigend
aus der Zeile, faellt es hier auf und nicht erst, wenn monatelang niemand
merkt, dass ein Schalter aus ist.
⚠ Die beiden Negativ-Pruefungen sind der eigentliche Wert: ein entfernter
Pfad, der wieder auftaucht, waere ein stiller Rueckbau."""
z = _text(_Stub(brk=True, src=True, flip=False)._system_status())
assert "BRK AN" in z and "Flip aus" in z
assert "SIG" not in z, "der entfernte Pfad darf nicht wieder auftauchen"
assert "S/R-Close" not in z, "der S/R-Auto-Close ist entfernt (21.08.)"
def test_toter_logger_wird_gemeldet():
"""Der reale Fund: rec_outcomes stand 5 Tage bei 0 Zeilen."""
z = _text(_Stub(puls=["rec_outcomes: 0 Zeilen — Logger hat NIE geschrieben"])
._system_status())
assert "⚠ Puls:" in z and "rec_outcomes" in z
def test_stiller_puls_wird_ausdruecklich_bestaetigt():
"""⚠ Kein Befund muss SICHTBAR sein — eine fehlende Zeile liest sich sonst
wie „nicht geprueft"."""
z = _text(_Stub(puls=[])._system_status())
assert "alle Telemetrie-Tabellen schreiben" in z
def test_puls_fehler_wird_gemeldet_nicht_verschluckt():
"""⚠ Genau die Fehlerklasse, die rec_outcomes 5 Tage verbarg: ein stiller
Fallback saehe aus wie „alles gut"."""
z = _text(_Stub(puls=RuntimeError("DB weg"))._system_status())
assert "⚠ Puls: Prüfung fehlgeschlagen" in z and "DB weg" in z
def test_ohne_history_kein_absturz():
s = _Stub()
s.history = None
z = _text(s._system_status())
assert "Automatik:" in z # der Rest laeuft weiter
def test_liefert_immer_zeilen_und_wirft_nie():
"""Die Kern-Invariante: der Tagesreport darf an dieser Zeile nicht scheitern."""
for kw in ({}, {"brk": True}, {"puls": RuntimeError("x")}):
z = _Stub(**kw)._system_status()
assert isinstance(z, list) and z, "muss immer mindestens eine Zeile liefern"
assert all(isinstance(x, str) for x in z)
def test_puls_tabellenliste_ist_vollstaendig():
"""Jede Telemetrie-Tabelle, die das Projekt schreibt, muss ueberwacht sein —
sonst entsteht genau die Luecke wieder, die rec_outcomes hatte."""
ueberwacht = {t for t, *_ in HistoryLogger.PULS}
for t in ("recommendations", "candles_m1", "verdict_votes", "m15_states",
"cone_checks", "pbreak_predictions", "rec_outcomes"):
assert t in ueberwacht, f"{t} wird nicht ueberwacht"
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-v"]))