Automatischer Push: 8 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/abbau_alpaca_lesen.py - .vault-tools/trading-plattform-pruefung/abbau_glattstellen.sh - .vault-tools/trading-plattform-pruefung/abbau_ibkr_lesen.sh - .vault-tools/trading-plattform-pruefung/abbau_restbestand.py - .vault-tools/trading-plattform-pruefung/abbau_restbestand_angriffe.py - .vault-tools/trading-plattform-pruefung/abbau_restbestand_einrichten.ps1 - AI/Regelwerk/windows-shell.md Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
30ed04a643
commit
3784322a76
8 files changed
+1687
-3
No files matched your search
@@ -0,0 +1,817 @@
|
||||
"""Angriffsfaelle fuer den Abbau des Restbestands (Paket AB): abbau_restbestand.py und seine WSL-Teile.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-07
|
||||
Doku: LIESMICH.md in diesem Ordner, Abschnitt "Paket AB"
|
||||
|
||||
Koordinator-Werkzeug, nicht fuers Repo. Jeder Fall ist eine Funktion test_<was er angreift>; so laufen
|
||||
dieselben Faelle als Skript und unter pytest (mutanten_lauf.py mit mutanten.toml im Planordner).
|
||||
Abdichtung, doppelt: Alle Pfade nach draussen zeigen unten ins Leere (wsl.exe, die WSL-Skripte, die
|
||||
Instanzordner), und jeder Fall ersetzt wsl() durch die Attrappe Welt, die jeden Aufruf zaehlt und nichts
|
||||
startet; der Fall zu wsl() selbst ersetzt starte (subprocess.run). Kein Fall erreicht wsl.exe, das Netz
|
||||
oder ntfy. Die Attrappe lehnt wie CPython jedes Argument mit NUL ab.
|
||||
Die Faelle test_wsl_* fahren bash und die Venvs in WSL gegen Wegwerf-Instanzordner mit einem falschen
|
||||
scripts/killswitch.py, dem erfundenen Wort TESTWORT-NICHT-ECHT, der erfundenen Kontonummer DUQ123456 und
|
||||
erfundenen Betraegen, nie gegen einen echten Instanzordner. Unter Windows sind sie uebersprungen.
|
||||
Aufruf unter Windows: python -I -X utf8 abbau_restbestand_angriffe.py
|
||||
Aufruf in WSL: /home/claude/venvs/trading/bin/python -I abbau_restbestand_angriffe.py
|
||||
Exit 0, wenn jeder Fall wie erwartet ausgeht.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HIER))
|
||||
import abbau_restbestand as ab # noqa: E402
|
||||
|
||||
# Abdichtung (Vorfall 2026-09-26: ein Mutant schickte ueber den absoluten WSL_SKRIPT Meldungen ans
|
||||
# Telefon). Jeder Pfad nach draussen zeigt ins Leere, bevor ein Fall laeuft.
|
||||
ab.WSL = str(Path(tempfile.gettempdir()) / "abbau-angriffe-kein-wsl" / "wsl.exe")
|
||||
ab.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
|
||||
ab.ALPACA_LESEN = "/nicht-vorhanden/abbau_alpaca_lesen.py"
|
||||
ab.IBKR_LESEN = "/nicht-vorhanden/abbau_ibkr_lesen.sh"
|
||||
ab.GLATTSTELLEN = "/nicht-vorhanden/abbau_glattstellen.sh"
|
||||
ab.ORDNER_WSL = {"alpaca": "/nicht-vorhanden/alpaca", "ibkr": "/nicht-vorhanden/ibkr"}
|
||||
|
||||
WRAPPER = HIER / "abbau_glattstellen.sh"
|
||||
IBKR_SKRIPT = HIER / "abbau_ibkr_lesen.sh"
|
||||
NUL = chr(0)
|
||||
TESTWORT = "TESTWORT-NICHT-ECHT"
|
||||
KONTO = "DUQ123456"
|
||||
BETRAG = "12345.67"
|
||||
BETRAG_OHNE_GELDWORT = "98.76"
|
||||
JETZT = "2026-10-08T15:45:00"
|
||||
GRUND = (
|
||||
"Abbau Restbestand nach Netzschalter-Abriss 2026-10-07; Auftrag Pascal 2026-10-07, "
|
||||
"ausgefuehrt von der Aufgabe Abbau Restbestand"
|
||||
)
|
||||
LOG = "projekt/data/paper/abbau/abbau.log"
|
||||
ERGEBNIS = "projekt/data/paper/abbau/abbau.json"
|
||||
|
||||
OFFEN = {"offen": True, "sekunden_bis_schluss": 6 * 3600}
|
||||
ZU = {"offen": False, "sekunden_bis_schluss": 18 * 3600}
|
||||
FLACH = {"positionen": [], "orders": 0}
|
||||
AAPL10 = {"positionen": [("AAPL", 10)], "orders": 0}
|
||||
IBKR7 = {
|
||||
"positionen": [
|
||||
("AAPL.XNAS", 10), ("MSFT.XNAS", 10), ("NVDA.XNAS", 20), ("JPM.XNYS", 10),
|
||||
("CAT.XNYS", 10), ("KO.XNYS", 20), ("NEE.XNYS", 20),
|
||||
],
|
||||
"orders": 0,
|
||||
}
|
||||
WRAPPER_AUSGABE = (
|
||||
"Zustand vorher: angehalten\nBroker: alpaca\nErgebnis: glattstellen, erfolgreich=True\n"
|
||||
"Zustand nachher: gesperrt\n"
|
||||
)
|
||||
# Ein falsches scripts/killswitch.py fuer die Faelle test_wsl_*: schreibt seine Argumente (mit dem Wort)
|
||||
# in den Wegwerf-Ordner, druckt das Wort auf stderr und eine erfundene Kontonummer und erfundene Betraege
|
||||
# auf stdout. Nichts davon darf aus dem Wrapper herauskommen.
|
||||
FALSCHER_KILLSWITCH = """import sys
|
||||
from pathlib import Path
|
||||
Path("aufgerufen").write_text(" ".join(sys.argv[1:]), encoding="utf-8")
|
||||
print("Zustand vorher: aktiv")
|
||||
print("Interpreter: " + sys.executable)
|
||||
print("Argumente: " + " ".join(sys.argv[1:]))
|
||||
print("Ergebnis: glattstellen, erfolgreich=False")
|
||||
print(" Fehler: Kontowert 12345.67 USD nicht gedeckt")
|
||||
print(" Fehler: Order an DUQ123456 abgelehnt, limit 98.76")
|
||||
print("Zustand nachher: gesperrt", flush=True)
|
||||
print("Abgelehnt, es wurde nichts geaendert: " + sys.argv[-1], file=sys.stderr)
|
||||
sys.exit(int(Path("exit.txt").read_text(encoding="utf-8")))
|
||||
"""
|
||||
|
||||
Roh = namedtuple("Roh", "exit text")
|
||||
|
||||
|
||||
class Uebersprungen(Exception):
|
||||
"""Ein Fall, der bash und die Venvs in WSL braucht, unter Windows."""
|
||||
|
||||
|
||||
def nur_in_wsl() -> None:
|
||||
if sys.platform == "win32":
|
||||
raise Uebersprungen("nur in WSL")
|
||||
|
||||
|
||||
def alpaca_json(uhr, bestand, fehler="BrokerFehler: Test") -> str:
|
||||
"""Die JSON-Zeile, die abbau_alpaca_lesen.py drucken wuerde; fehler ist der bestand_fehler."""
|
||||
daten = {"uhr": uhr, "uhr_fehler": None, "positionen": None, "orders": None, "bestand_fehler": None}
|
||||
if uhr is None:
|
||||
daten["uhr_fehler"] = "BrokerFehler: Marktuhr nicht lesbar"
|
||||
if bestand is None:
|
||||
daten["bestand_fehler"] = fehler
|
||||
else:
|
||||
daten["positionen"] = [{"symbol": symbol, "menge": menge} for symbol, menge in bestand["positionen"]]
|
||||
daten["orders"] = bestand["orders"]
|
||||
return json.dumps(daten) + "\n"
|
||||
|
||||
|
||||
def ibkr_text(bestand, schluss="ERGEBNIS: gelesen") -> str:
|
||||
"""stdout von scripts/ibkr_pruefen.py lesen in der gemessenen Form, ohne Kontonummer."""
|
||||
zeilen = [
|
||||
"verbunden: Gateway 127.0.0.1:4004, Client-ID 12, Handelsmodus paper, Konto XX (8 Zeichen), bestaetigt",
|
||||
"Konto: Kontowert, Bargeld und Kaufkraft gelesen (Betraege werden nicht ausgegeben)",
|
||||
f"Positionen: {len(bestand['positionen'])}",
|
||||
]
|
||||
zeilen += [f" {kennung} {menge}" for kennung, menge in bestand["positionen"]]
|
||||
zeilen += [f"Offene Orders: {bestand['orders']}", schluss]
|
||||
return "\n".join(zeilen) + "\n"
|
||||
|
||||
|
||||
def unit_text(stand) -> str:
|
||||
"""Ausgabe von systemctl show --property=LoadState,ActiveState: Text als Roh, (LoadState, ActiveState)
|
||||
als Paar, oder nur ActiveState einer geladenen Unit."""
|
||||
if isinstance(stand, Roh):
|
||||
return stand.text
|
||||
laden, aktiv = stand if isinstance(stand, tuple) else ("loaded", stand)
|
||||
return f"LoadState={laden}\nActiveState={aktiv}\n"
|
||||
|
||||
|
||||
class Welt:
|
||||
"""Attrappe fuer alles hinter ab.wsl(): Units, Bestaende, Wrapper, Meldung. Zaehlt jeden Aufruf."""
|
||||
|
||||
def __init__(self, *, units=("inactive", "inactive"), uhr=OFFEN, alpaca=AAPL10, ibkr=IBKR7,
|
||||
alpaca_nachher=FLACH, ibkr_nachher=FLACH, alpaca_fehler="BrokerFehler: Test",
|
||||
wrapper_exit=0, wrapper_wirft=False, echter_wrapper=None):
|
||||
self.units = dict(zip(ab.INSTANZEN, units))
|
||||
self.uhr = uhr
|
||||
self.bestand = {"alpaca": alpaca, "ibkr": ibkr}
|
||||
self.nachher = {"alpaca": alpaca_nachher, "ibkr": ibkr_nachher}
|
||||
self.alpaca_fehler = alpaca_fehler
|
||||
self.wrapper_exit = wrapper_exit
|
||||
self.wrapper_wirft = wrapper_wirft
|
||||
self.echter_wrapper = echter_wrapper
|
||||
self.aufrufe = []
|
||||
self.glatt = []
|
||||
self.ordner = []
|
||||
self.meldungen = []
|
||||
self.schlaf = []
|
||||
self.gelesen = {"alpaca": 0, "ibkr": 0}
|
||||
|
||||
def schlafe(self, sekunden):
|
||||
self.schlaf.append(sekunden)
|
||||
|
||||
def __call__(self, argumente, timeout):
|
||||
argumente = list(argumente)
|
||||
if any(NUL in argument for argument in argumente):
|
||||
raise ValueError("embedded null character")
|
||||
self.aufrufe.append(argumente)
|
||||
if argumente[:2] == ["systemctl", "show"]:
|
||||
instanz = next(name for name, unit in ab.UNITS.items() if unit == argumente[-1])
|
||||
return 0, unit_text(self.units[instanz]), ""
|
||||
if argumente[:2] == [ab.ALPACA_PY, ab.ALPACA_LESEN]:
|
||||
self.gelesen["alpaca"] += 1
|
||||
return 0, alpaca_json(self.uhr, self.bestand["alpaca"], self.alpaca_fehler), ""
|
||||
if argumente[:2] == ["bash", ab.IBKR_LESEN]:
|
||||
self.gelesen["ibkr"] += 1
|
||||
stand = self.bestand["ibkr"]
|
||||
if stand is None:
|
||||
return 4, "", "Voraussetzung fehlt: Gateway nicht erreichbar\n"
|
||||
if isinstance(stand, Roh):
|
||||
return stand.exit, stand.text, ""
|
||||
return 0, ibkr_text(stand), ""
|
||||
if argumente[:2] == ["bash", ab.GLATTSTELLEN]:
|
||||
instanz = argumente[2]
|
||||
self.glatt.append(instanz)
|
||||
self.ordner.append(argumente[3])
|
||||
if self.wrapper_wirft:
|
||||
raise subprocess.TimeoutExpired(argumente, timeout)
|
||||
self.bestand[instanz] = self.nachher[instanz]
|
||||
if self.echter_wrapper is not None:
|
||||
return self.echter_wrapper(argumente[2:])
|
||||
return self.wrapper_exit, WRAPPER_AUSGABE, ""
|
||||
if argumente[:3] == [ab.WSL_PY, ab.WSL_SKRIPT, "--meldung"]:
|
||||
self.meldungen.append((argumente[3], argumente[4]))
|
||||
return 0, '{"meldung": "gesendet"}\n', ""
|
||||
raise AssertionError(f"unbekannter Aufruf: {argumente!r}")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def attrappe(welt):
|
||||
"""Ersetzt wsl() und schlafe() fuer die Dauer eines Falls und stellt beide danach zurueck."""
|
||||
alt = ab.wsl, ab.schlafe
|
||||
ab.wsl, ab.schlafe = welt, welt.schlafe
|
||||
try:
|
||||
yield welt
|
||||
finally:
|
||||
ab.wsl, ab.schlafe = alt
|
||||
|
||||
|
||||
def lies_json(text):
|
||||
try:
|
||||
return json.loads(text) if text else None
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def lauf_mit(welt, *, jetzt=JETZT, modus="scharf", ergebnis_vorher=None):
|
||||
"""Ein Lauf von ab.main() in Wegwerf-Ordnern fuer Projekt und Vault; gibt alles Sichtbare zurueck."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
projekt, vault = tmp / "projekt", tmp / "vault"
|
||||
projekt.mkdir()
|
||||
(vault / "Inbox").mkdir(parents=True)
|
||||
ablage = projekt / ab.ABLAGE_TEIL
|
||||
if ergebnis_vorher is not None:
|
||||
ablage.mkdir(parents=True)
|
||||
(ablage / ab.ERGEBNIS_NAME).write_text(ergebnis_vorher, encoding="utf-8")
|
||||
puffer = io.StringIO()
|
||||
with attrappe(welt), contextlib.redirect_stdout(puffer):
|
||||
code = ab.main(["--modus", modus, "--jetzt", jetzt, "--projekt", str(projekt), "--vault", str(vault)])
|
||||
ergebnis = ablage / ab.ERGEBNIS_NAME
|
||||
log = ablage / ab.LOG_NAME
|
||||
notizen = sorted((vault / "Inbox").glob("*.md"))
|
||||
text = ergebnis.read_text(encoding="utf-8") if ergebnis.exists() else None
|
||||
return SimpleNamespace(
|
||||
code=code,
|
||||
welt=welt,
|
||||
ausgabe=puffer.getvalue(),
|
||||
dateien=sorted(p.relative_to(tmp).as_posix() for p in tmp.rglob("*") if p.is_file()),
|
||||
ergebnis_text=text,
|
||||
json=lies_json(text),
|
||||
log=log.read_text(encoding="utf-8") if log.exists() else "",
|
||||
notiz=notizen[0].read_text(encoding="utf-8") if notizen else "",
|
||||
)
|
||||
|
||||
|
||||
def titel_von(e) -> list[str]:
|
||||
return [titel for titel, _ in e.welt.meldungen]
|
||||
|
||||
|
||||
def kurz(e) -> str:
|
||||
flach = None if e.json is None else e.json.get("flach")
|
||||
return (
|
||||
f"code={e.code} glatt={e.welt.glatt} ordner={e.welt.ordner} schlaf={e.welt.schlaf} "
|
||||
f"gelesen={e.welt.gelesen} meldungen={titel_von(e)} flach={flach} dateien={e.dateien} "
|
||||
f"ausgabe={e.ausgabe[-400:]!r}"
|
||||
)
|
||||
|
||||
|
||||
def instanzordner(pfad: Path, *, wort=TESTWORT, exit_code=0) -> Path:
|
||||
"""Ein Wegwerf-Instanzordner mit falschem scripts/killswitch.py und, wenn wort gesetzt ist, killswitch.toml."""
|
||||
(pfad / "scripts").mkdir(parents=True)
|
||||
(pfad / "config").mkdir()
|
||||
if wort is not None:
|
||||
(pfad / "config" / "killswitch.toml").write_text(
|
||||
f'[killswitch]\nbestaetigungswort = "{wort}"\n', encoding="utf-8"
|
||||
)
|
||||
(pfad / "scripts" / "killswitch.py").write_text(FALSCHER_KILLSWITCH, encoding="utf-8")
|
||||
(pfad / "exit.txt").write_text(str(exit_code), encoding="utf-8")
|
||||
return pfad
|
||||
|
||||
|
||||
def wrapper(tmp: Path, *argumente: str) -> tuple[int, str]:
|
||||
"""Der echte abbau_glattstellen.sh aus einem leeren Arbeitsordner; stdout und stderr zusammen."""
|
||||
leer = tmp / "leer"
|
||||
leer.mkdir(exist_ok=True)
|
||||
lauf = subprocess.run(
|
||||
["bash", str(WRAPPER), *argumente], cwd=leer, capture_output=True, text=True, timeout=120
|
||||
)
|
||||
return lauf.returncode, lauf.stdout + lauf.stderr
|
||||
|
||||
|
||||
class FalscherAlpaca:
|
||||
"""Ein Broker mit den drei Leseaufrufen; die Betraege sind erfunden und duerfen nie herauskommen."""
|
||||
|
||||
def __init__(self, jetzt, fehler=None):
|
||||
self.jetzt = jetzt
|
||||
self.fehler = fehler
|
||||
|
||||
def marktuhr(self):
|
||||
return SimpleNamespace(is_open=True, next_open=self.jetzt, next_close=self.jetzt + timedelta(minutes=20))
|
||||
|
||||
def positionen(self):
|
||||
if self.fehler is not None:
|
||||
raise RuntimeError(self.fehler)
|
||||
return [
|
||||
SimpleNamespace(symbol="AAPL", menge=10, seite="long", marktwert=1234.56, einstandskurs=98.76),
|
||||
SimpleNamespace(symbol="KO", menge=-20, seite="short", marktwert=-1111.11, einstandskurs=55.55),
|
||||
]
|
||||
|
||||
def offene_orders(self):
|
||||
return [SimpleNamespace(id="o-1", menge=10, limit=98.76)]
|
||||
|
||||
|
||||
# --- Bedienung und Grundlinie ---------------------------------------------------------------------
|
||||
|
||||
def test_ohne_modus_bricht_mit_exit_2_ab_und_ruft_nichts():
|
||||
welt = Welt()
|
||||
codes = []
|
||||
for argumente in (["--jetzt", JETZT], ["--modus", "probe", "--jetzt", JETZT]):
|
||||
with attrappe(welt), contextlib.redirect_stderr(io.StringIO()):
|
||||
try:
|
||||
ab.main(argumente)
|
||||
except SystemExit as ende:
|
||||
codes.append(ende.code)
|
||||
else:
|
||||
codes.append(None)
|
||||
assert codes == [2, 2], codes
|
||||
assert welt.aufrufe == [], welt.aufrufe
|
||||
|
||||
|
||||
def test_grundlinie_je_instanz_genau_ein_eingriff_dann_flach():
|
||||
e = lauf_mit(Welt())
|
||||
assert e.code == 0, kurz(e)
|
||||
assert e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
assert e.welt.ordner == [ab.ORDNER_WSL["alpaca"], ab.ORDNER_WSL["ibkr"]], kurz(e)
|
||||
assert e.welt.schlaf == [ab.WARTEN_S], kurz(e)
|
||||
assert e.welt.gelesen == {"alpaca": 2, "ibkr": 2}, kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: flach"], kurz(e)
|
||||
assert e.json["flach"] is True, kurz(e)
|
||||
assert e.json["instanzen"]["alpaca"]["vorher"] == {"positionen": {"AAPL": 10}, "orders": 0}, kurz(e)
|
||||
assert e.json["instanzen"]["ibkr"]["nachher"] == {"positionen": {}, "orders": 0}, kurz(e)
|
||||
assert "Abbau Restbestand (automatisch, Auftrag Pascal 2026-10-07): flach." in e.notiz, e.notiz
|
||||
assert "AAPL long 10" in e.notiz and "NEE long 20" in e.notiz, e.notiz
|
||||
assert "#needs-review" not in e.notiz, e.notiz
|
||||
|
||||
|
||||
# --- Fenster ---------------------------------------------------------------------------------------
|
||||
|
||||
def test_vor_dem_fenster_eine_sekunde_tut_nichts():
|
||||
e = lauf_mit(Welt(), jetzt="2026-10-08T15:39:59")
|
||||
assert e.code == 0 and e.welt.aufrufe == [] and e.dateien == [], kurz(e)
|
||||
|
||||
|
||||
def test_fensterbeginn_genau_1540_greift():
|
||||
e = lauf_mit(Welt(), jetzt="2026-10-08T15:40:00")
|
||||
assert e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
|
||||
|
||||
def test_fensterende_genau_2145_am_freitag_greift():
|
||||
e = lauf_mit(Welt(), jetzt="2026-10-09T21:45:00")
|
||||
assert e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
|
||||
|
||||
def test_nach_dem_fenster_eine_sekunde_tut_nichts():
|
||||
e = lauf_mit(Welt(), jetzt="2026-10-09T21:45:01")
|
||||
assert e.code == 0 and e.welt.aufrufe == [] and e.dateien == [], kurz(e)
|
||||
|
||||
|
||||
# --- Marke flach und Entwarnung --------------------------------------------------------------------
|
||||
|
||||
def test_zweiter_lauf_nach_flach_tut_nichts():
|
||||
vorher = json.dumps({"zeit": "2026-10-08T15:45:00", "flach": True})
|
||||
e = lauf_mit(Welt(), ergebnis_vorher=vorher)
|
||||
assert e.code == 0 and e.welt.aufrufe == [] and e.welt.meldungen == [], kurz(e)
|
||||
assert e.ergebnis_text == vorher and e.notiz == "", kurz(e)
|
||||
assert e.dateien == [ERGEBNIS, LOG], kurz(e)
|
||||
|
||||
|
||||
def test_ergebnisdatei_ohne_echtes_true_laeuft_weiter():
|
||||
for vorher in ('{"flach": false}', '{"flach": "ja"}', '{"flach": 1}', "{kaputt", "[true]"):
|
||||
e = lauf_mit(Welt(), ergebnis_vorher=vorher)
|
||||
assert e.welt.glatt == ["alpaca", "ibkr"], (vorher, kurz(e))
|
||||
|
||||
|
||||
def test_erstes_flach_ohne_eingriff_meldet_entwarnung():
|
||||
vorher = json.dumps({"flach": False, "befunde": ["ibkr: Bestand nachher nicht lesbar; Ergebnis offen"]})
|
||||
e = lauf_mit(Welt(alpaca=FLACH, ibkr=FLACH), ergebnis_vorher=vorher)
|
||||
assert e.code == 0 and e.welt.glatt == [] and e.welt.schlaf == [], kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: flach"], kurz(e)
|
||||
assert ": flach." in e.notiz and "#needs-review" not in e.notiz, e.notiz
|
||||
assert e.json["flach"] is True, kurz(e)
|
||||
|
||||
|
||||
def test_gesperrte_instanz_verhindert_flach_auch_bei_flachem_bestand():
|
||||
e = lauf_mit(Welt(units=("inactive", "active"), alpaca=FLACH, ibkr=FLACH))
|
||||
assert e.code == 1 and e.welt.glatt == [], kurz(e)
|
||||
assert e.json["flach"] is False, kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: Befund"], kurz(e)
|
||||
assert "**Befund** #needs-review" in e.notiz, e.notiz
|
||||
|
||||
|
||||
# --- Modus lesen -----------------------------------------------------------------------------------
|
||||
|
||||
def test_modus_lesen_ruft_den_wrapper_nie_und_schreibt_nichts():
|
||||
e = lauf_mit(Welt(), modus="lesen")
|
||||
assert e.code == 0, kurz(e)
|
||||
assert e.welt.glatt == [] and e.welt.meldungen == [] and e.welt.schlaf == [], kurz(e)
|
||||
assert e.dateien == [], kurz(e)
|
||||
for erwartet in (
|
||||
"Bestand alpaca: 1 Position(en): AAPL long 10; offene Orders 0",
|
||||
"Glattstellen im Modus scharf: alpaca, ibkr",
|
||||
"ERGEBNIS: gelesen, ohne Befund",
|
||||
):
|
||||
assert erwartet in e.ausgabe, (erwartet, e.ausgabe)
|
||||
|
||||
|
||||
def test_modus_lesen_nachts_bei_markt_zu_liest_beide_bestaende_ohne_eingriff():
|
||||
e = lauf_mit(Welt(uhr=ZU), modus="lesen", jetzt="2026-10-08T00:30:00")
|
||||
assert e.code == 0, kurz(e)
|
||||
assert e.welt.glatt == [] and e.welt.meldungen == [] and e.dateien == [], kurz(e)
|
||||
assert e.welt.gelesen == {"alpaca": 1, "ibkr": 1}, kurz(e)
|
||||
for erwartet in (
|
||||
": ausserhalb",
|
||||
"Runner alpaca (trading-paper.service): LoadState=loaded, ActiveState=inactive",
|
||||
"Markt zu",
|
||||
"Bestand ibkr: 7 Position(en): AAPL long 10, CAT long 10, JPM long 10, KO long 20, MSFT long 10, "
|
||||
"NEE long 20, NVDA long 20; offene Orders 0",
|
||||
"Glattstellen im Modus scharf: keine (Markt zu",
|
||||
):
|
||||
assert erwartet in e.ausgabe, (erwartet, e.ausgabe)
|
||||
|
||||
|
||||
def test_modus_lesen_liest_auch_nach_flach():
|
||||
e = lauf_mit(Welt(), modus="lesen", ergebnis_vorher='{"flach": true}')
|
||||
assert e.welt.gelesen == {"alpaca": 1, "ibkr": 1} and e.welt.glatt == [], kurz(e)
|
||||
assert "Ergebnisdatei sagt flach" in e.ausgabe, e.ausgabe
|
||||
assert e.dateien == [ERGEBNIS], kurz(e)
|
||||
|
||||
|
||||
# --- Marktuhr --------------------------------------------------------------------------------------
|
||||
|
||||
def test_markt_zu_scharf_still_ohne_eingriff():
|
||||
e = lauf_mit(Welt(uhr=ZU))
|
||||
assert e.code == 0 and e.welt.glatt == [] and e.welt.meldungen == [], kurz(e)
|
||||
assert e.welt.gelesen == {"alpaca": 1, "ibkr": 0}, kurz(e)
|
||||
assert e.dateien == [LOG] and e.notiz == "", kurz(e)
|
||||
assert "Markt zu" in e.log, e.log
|
||||
|
||||
|
||||
def test_markt_schliesst_in_599_s_still():
|
||||
e = lauf_mit(Welt(uhr={"offen": True, "sekunden_bis_schluss": 599}))
|
||||
assert e.code == 0 and e.welt.glatt == [] and e.dateien == [LOG], kurz(e)
|
||||
|
||||
|
||||
def test_markt_schliesst_in_genau_600_s_greift():
|
||||
e = lauf_mit(Welt(uhr={"offen": True, "sekunden_bis_schluss": 600}))
|
||||
assert e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
|
||||
|
||||
def test_marktuhr_unlesbar_befund_ohne_eingriff():
|
||||
e = lauf_mit(Welt(uhr=None))
|
||||
assert e.code == 1 and e.welt.glatt == [] and e.welt.schlaf == [], kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: Befund"], kurz(e)
|
||||
assert e.json["flach"] is False, kurz(e)
|
||||
assert "**Befund** #needs-review" in e.notiz and "Marktuhr nicht lesbar" in e.notiz, e.notiz
|
||||
|
||||
|
||||
# --- Runner-Units ----------------------------------------------------------------------------------
|
||||
|
||||
def test_laufender_runner_sperrt_genau_seine_instanz():
|
||||
e = lauf_mit(Welt(units=("active", "inactive")))
|
||||
assert e.code == 1 and e.welt.glatt == ["ibkr"], kurz(e)
|
||||
erwartet = "alpaca: Runner-Unit trading-paper.service laeuft (LoadState=loaded, ActiveState=active); nicht angefasst"
|
||||
assert erwartet in e.notiz, e.notiz
|
||||
assert "#needs-review" in e.notiz, e.notiz
|
||||
e = lauf_mit(Welt(units=("inactive", "deactivating")))
|
||||
assert e.code == 1 and e.welt.glatt == ["alpaca"], kurz(e)
|
||||
|
||||
|
||||
def test_runner_zustand_unlesbar_instanz_nicht_angefasst():
|
||||
for zustand in ("", "unknown", Roh(0, "")):
|
||||
e = lauf_mit(Welt(units=(zustand, "inactive")))
|
||||
assert e.code == 1 and e.welt.glatt == ["ibkr"], (zustand, kurz(e))
|
||||
|
||||
|
||||
def test_runner_unit_not_found_sperrt_die_instanz():
|
||||
e = lauf_mit(Welt(units=(("not-found", "inactive"), "inactive")))
|
||||
assert e.code == 1 and e.welt.glatt == ["ibkr"], kurz(e)
|
||||
assert "LoadState=not-found, ActiveState=inactive" in e.notiz and "#needs-review" in e.notiz, e.notiz
|
||||
|
||||
|
||||
def test_runner_failed_gilt_als_stehend():
|
||||
e = lauf_mit(Welt(units=("failed", "failed")))
|
||||
assert e.code == 0 and e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
|
||||
|
||||
# --- Bestaende -------------------------------------------------------------------------------------
|
||||
|
||||
def test_ibkr_ohne_ergebnis_zeile_gilt_als_unlesbar():
|
||||
roh = Roh(1, "Positionen: 1\n AAPL.XNAS 10\nOffene Orders: 0\n")
|
||||
e = lauf_mit(Welt(ibkr=roh))
|
||||
assert e.code == 1 and e.welt.glatt == ["alpaca"], kurz(e)
|
||||
assert "ibkr: Bestand nicht lesbar" in e.notiz, e.notiz
|
||||
|
||||
|
||||
def test_ibkr_exit_4_gilt_als_unlesbar():
|
||||
e = lauf_mit(Welt(ibkr=Roh(4, ibkr_text(IBKR7))))
|
||||
assert e.code == 1 and e.welt.glatt == ["alpaca"], kurz(e)
|
||||
|
||||
|
||||
def test_ibkr_zahl_und_zeilen_passen_nicht():
|
||||
roh = Roh(0, "Positionen: 2\n AAPL.XNAS 10\nOffene Orders: 0\nERGEBNIS: gelesen\n")
|
||||
e = lauf_mit(Welt(ibkr=roh))
|
||||
assert e.code == 1 and e.welt.glatt == ["alpaca"], kurz(e)
|
||||
|
||||
|
||||
def test_ibkr_exit_1_mit_ergebnis_gelesen_gilt_als_gelesen():
|
||||
roh = Roh(1, ibkr_text(IBKR7, schluss="ERGEBNIS: gelesen, mit Befund"))
|
||||
e = lauf_mit(Welt(ibkr=roh))
|
||||
assert e.code == 0 and e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
|
||||
|
||||
def test_alpaca_bestand_unlesbar_nicht_angefasst():
|
||||
e = lauf_mit(Welt(alpaca=None))
|
||||
assert e.code == 1 and e.welt.glatt == ["ibkr"], kurz(e)
|
||||
assert "alpaca: Bestand nicht lesbar (BrokerFehler: Test); nicht angefasst" in e.notiz, e.notiz
|
||||
|
||||
|
||||
def test_fremder_fehlertext_mit_zeilenumbruch_bleibt_einzeilig():
|
||||
e = lauf_mit(Welt(alpaca=None, alpaca_fehler="BrokerFehler: HTTP 500\n## Zweite Zeile"))
|
||||
assert e.code == 1 and e.welt.glatt == ["ibkr"], kurz(e)
|
||||
assert "BrokerFehler: HTTP 500 ## Zweite Zeile" in e.notiz, e.notiz
|
||||
assert not any(zeile.startswith("## Zweite") for zeile in e.notiz.splitlines()), e.notiz
|
||||
assert not any(zeile.startswith("## Zweite") for zeile in e.log.splitlines()), e.log
|
||||
for titel, text in e.welt.meldungen:
|
||||
assert "\n" not in text and "\r" not in text, text
|
||||
|
||||
|
||||
def test_nur_offene_order_ohne_position_wird_glattgestellt():
|
||||
e = lauf_mit(Welt(alpaca={"positionen": [], "orders": 1}, ibkr=FLACH))
|
||||
assert e.code == 0 and e.welt.glatt == ["alpaca"], kurz(e)
|
||||
|
||||
|
||||
def test_flache_instanz_wird_nicht_angefasst():
|
||||
e = lauf_mit(Welt(alpaca=FLACH))
|
||||
assert e.code == 0 and e.welt.glatt == ["ibkr"], kurz(e)
|
||||
assert e.json["instanzen"]["alpaca"]["glattstellen"] is None, kurz(e)
|
||||
|
||||
|
||||
# --- Ergebnis nach dem Eingriff --------------------------------------------------------------------
|
||||
|
||||
def test_rest_offen_nach_eingriff():
|
||||
e = lauf_mit(Welt(ibkr_nachher={"positionen": [("NEE.XNYS", 20)], "orders": 0}))
|
||||
assert e.code == 0 and e.json["flach"] is False, kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: Rest offen"], kurz(e)
|
||||
assert ": Rest offen." in e.notiz and "#needs-review" not in e.notiz, e.notiz
|
||||
|
||||
|
||||
def test_offene_order_nachher_ist_nicht_flach():
|
||||
e = lauf_mit(Welt(alpaca_nachher={"positionen": [], "orders": 1}))
|
||||
assert e.json["flach"] is False, kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: Rest offen"], kurz(e)
|
||||
|
||||
|
||||
def test_wrapper_exit_1_und_danach_flach_heisst_flach_mit_befund():
|
||||
e = lauf_mit(Welt(wrapper_exit=1))
|
||||
assert e.code == 1, kurz(e)
|
||||
assert e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
assert e.json["flach"] is True, kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: flach, mit Befund"], kurz(e)
|
||||
assert ": flach, mit Befund #needs-review." in e.notiz, e.notiz
|
||||
assert "alpaca: glattstellen endete mit Exit 1" in e.notiz, e.notiz
|
||||
|
||||
|
||||
def test_glattstellen_wirft_exit_none_befund_nachlesen_und_meldung():
|
||||
e = lauf_mit(Welt(wrapper_wirft=True))
|
||||
assert e.code == 1 and e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
assert e.welt.schlaf == [ab.WARTEN_S] and e.welt.gelesen == {"alpaca": 2, "ibkr": 2}, kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: Befund"], kurz(e)
|
||||
assert "alpaca: glattstellen endete mit Exit None" in e.notiz, e.notiz
|
||||
assert e.json["instanzen"]["alpaca"]["glattstellen"]["exit"] is None, kurz(e)
|
||||
|
||||
|
||||
def test_nachher_unlesbar_ist_befund_und_nicht_flach():
|
||||
e = lauf_mit(Welt(ibkr_nachher=None))
|
||||
assert e.code == 1 and e.json["flach"] is False, kurz(e)
|
||||
assert "ibkr: Bestand nachher nicht lesbar" in e.notiz, e.notiz
|
||||
|
||||
|
||||
def test_ausnahme_traegt_nur_den_typnamen_in_notiz_und_meldung():
|
||||
welt = Welt()
|
||||
|
||||
def wirft(sekunden):
|
||||
raise RuntimeError(f"Order an {KONTO} limit {BETRAG}")
|
||||
|
||||
welt.schlafe = wirft
|
||||
e = lauf_mit(welt)
|
||||
assert e.code == 1, kurz(e)
|
||||
assert "Befund: abgebrochen: RuntimeError" in e.notiz, e.notiz
|
||||
texte = [e.notiz, e.ergebnis_text or ""] + [titel + " " + text for titel, text in e.welt.meldungen]
|
||||
for text in texte:
|
||||
assert KONTO not in text and BETRAG not in text, text
|
||||
assert "Traceback" in e.log, e.log
|
||||
|
||||
|
||||
# --- wsl.exe-Fehler und NUL ------------------------------------------------------------------------
|
||||
|
||||
def test_aufruf_nach_wsl_entfernt_nul_und_setzt_wsl_utf8():
|
||||
gesehen = []
|
||||
|
||||
def falscher_start(befehl, **optionen):
|
||||
gesehen.append((befehl, optionen))
|
||||
return SimpleNamespace(
|
||||
returncode=4294967295,
|
||||
stdout="Es ist keine Distribution".encode("utf-16-le"),
|
||||
stderr="Fehler".encode("utf-16-le"),
|
||||
)
|
||||
|
||||
alt = ab.starte
|
||||
ab.starte = falscher_start
|
||||
try:
|
||||
code, ausgabe, fehler = ab.wsl(["systemctl", "show", "trading-paper.service"], 5)
|
||||
finally:
|
||||
ab.starte = alt
|
||||
assert code == 4294967295, code
|
||||
assert NUL not in ausgabe and NUL not in fehler, (ausgabe, fehler)
|
||||
assert ausgabe == "Es ist keine Distribution" and fehler == "Fehler", (ausgabe, fehler)
|
||||
assert len(gesehen) == 1 and gesehen[0][0][0] == ab.WSL, gesehen
|
||||
assert gesehen[0][1]["env"]["WSL_UTF8"] == "1", gesehen[0][1].get("env", {}).get("WSL_UTF8")
|
||||
|
||||
|
||||
def test_runner_zustand_mit_nul_kommt_sauber_in_notiz_log_und_meldung():
|
||||
roh = Roh(0, "LoadState=loaded\nActiveState=E" + NUL + "s" + NUL + "\n")
|
||||
e = lauf_mit(Welt(units=(roh, "inactive")))
|
||||
assert e.code == 1 and e.welt.glatt == ["ibkr"], kurz(e)
|
||||
assert titel_von(e) == ["Abbau Restbestand: Befund"], kurz(e)
|
||||
assert NUL not in e.notiz and NUL not in e.log, (e.notiz, e.log)
|
||||
welt = Welt()
|
||||
with attrappe(welt):
|
||||
antwort = ab.melde("Abbau Restbestand: Probe" + NUL, "Text" + NUL, ab.SCHARF)
|
||||
assert antwort == "gesendet", antwort
|
||||
assert welt.meldungen == [("Abbau Restbestand: Probe", "Text")], welt.meldungen
|
||||
|
||||
|
||||
# --- Abdichtung ------------------------------------------------------------------------------------
|
||||
|
||||
def test_glattstellen_und_melden_ohne_scharf_rufen_nie_wsl():
|
||||
welt = Welt()
|
||||
with attrappe(welt):
|
||||
try:
|
||||
ab.glattstelle("alpaca", ab.LESEN)
|
||||
except RuntimeError:
|
||||
abgelehnt = True
|
||||
else:
|
||||
abgelehnt = False
|
||||
antwort = ab.melde("Abbau Restbestand: Probe", "Text", ab.LESEN)
|
||||
assert abgelehnt, "glattstelle lief im Modus lesen"
|
||||
assert welt.aufrufe == [], welt.aufrufe
|
||||
assert antwort.startswith("nicht gesendet"), antwort
|
||||
|
||||
|
||||
def test_abdichtung_alle_pfade_nach_draussen_zeigen_ins_leere():
|
||||
assert not Path(ab.WSL).exists(), ab.WSL
|
||||
assert ab.starte is subprocess.run, ab.starte
|
||||
for pfad in (ab.WSL_SKRIPT, ab.ALPACA_LESEN, ab.IBKR_LESEN, ab.GLATTSTELLEN, *ab.ORDNER_WSL.values()):
|
||||
assert pfad.startswith("/nicht-vorhanden/"), pfad
|
||||
try:
|
||||
ab.wsl(["systemctl", "show", "trading-paper.service"], 5)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
raise AssertionError("wsl() startete einen Prozess, obwohl WSL ins Leere zeigt")
|
||||
|
||||
|
||||
# --- WSL: Wrapper, Kette, Helfer -------------------------------------------------------------------
|
||||
|
||||
def test_wsl_wrapper_waehlt_alpaca_ersetzt_das_wort_und_maskiert_konto_und_betraege():
|
||||
nur_in_wsl()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ordner = instanzordner(tmp / "alpaca", exit_code=1)
|
||||
code, text = wrapper(tmp, "alpaca", str(ordner))
|
||||
angekommen = (ordner / "aufgerufen").read_text(encoding="utf-8")
|
||||
assert code == 1, (code, text)
|
||||
assert angekommen == f"--broker alpaca glattstellen --grund {GRUND} --bestaetigung {TESTWORT}", angekommen
|
||||
assert TESTWORT not in text and "[wort]" in text, text
|
||||
assert BETRAG not in text and "Kontowert [x]" in text, text
|
||||
assert KONTO not in text and "Order an [konto]" in text, text
|
||||
assert BETRAG_OHNE_GELDWORT not in text and "limit [x]" in text, text
|
||||
assert "venvs/trading/bin/python" in text, text
|
||||
|
||||
|
||||
def test_wsl_wrapper_ibkr_mit_attrappe_nimmt_ihr_venv_und_broker_vor_dem_unterbefehl():
|
||||
nur_in_wsl()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ordner = instanzordner(tmp / "ibkr")
|
||||
code, text = wrapper(tmp, "ibkr", str(ordner), "--broker", "attrappe")
|
||||
angekommen = (ordner / "aufgerufen").read_text(encoding="utf-8")
|
||||
assert code == 0, (code, text)
|
||||
assert angekommen.startswith("--broker attrappe glattstellen --grund "), angekommen
|
||||
assert "venvs/trading-ibkr/bin/python" in text, text
|
||||
|
||||
|
||||
def test_wsl_wrapper_lehnt_anderen_broker_und_fremde_angaben_ab():
|
||||
nur_in_wsl()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ordner = instanzordner(tmp / "alpaca")
|
||||
faelle = (
|
||||
("alpaca", str(ordner), "--broker", "alpaca"),
|
||||
("alpaca", str(ordner), "--broker"),
|
||||
("alpaca", str(ordner), "--weiter"),
|
||||
("live", str(ordner)),
|
||||
("alpaca",),
|
||||
)
|
||||
codes = [wrapper(tmp, *fall)[0] for fall in faelle]
|
||||
aufgerufen = (ordner / "aufgerufen").exists()
|
||||
assert codes == [2, 2, 2, 2, 2], codes
|
||||
assert not aufgerufen, "killswitch.py wurde trotz Bedienfehler gerufen"
|
||||
|
||||
|
||||
def test_wsl_wrapper_ohne_wort_ruft_killswitch_nicht():
|
||||
nur_in_wsl()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ordner = instanzordner(tmp / "alpaca", wort=None)
|
||||
code, text = wrapper(tmp, "alpaca", str(ordner))
|
||||
aufgerufen = (ordner / "aufgerufen").exists()
|
||||
assert code == 4 and not aufgerufen, (code, aufgerufen, text)
|
||||
|
||||
|
||||
def test_wsl_kette_wort_konto_und_betrag_stehen_nirgends():
|
||||
nur_in_wsl()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ordner = {instanz: instanzordner(tmp / instanz, exit_code=1) for instanz in ab.INSTANZEN}
|
||||
|
||||
def echter_wrapper(argumente):
|
||||
assert Path(argumente[1]).resolve().is_relative_to(tmp.resolve()), argumente
|
||||
code, text = wrapper(tmp, *argumente)
|
||||
return code, text, ""
|
||||
|
||||
alt = ab.ORDNER_WSL
|
||||
ab.ORDNER_WSL = {instanz: str(pfad) for instanz, pfad in ordner.items()}
|
||||
try:
|
||||
e = lauf_mit(Welt(echter_wrapper=echter_wrapper))
|
||||
finally:
|
||||
ab.ORDNER_WSL = alt
|
||||
angekommen = (ordner["ibkr"] / "aufgerufen").read_text(encoding="utf-8")
|
||||
assert e.code == 1 and e.welt.glatt == ["alpaca", "ibkr"], kurz(e)
|
||||
assert angekommen.startswith("--broker ibkr glattstellen --grund "), angekommen
|
||||
assert angekommen.endswith("--bestaetigung " + TESTWORT), angekommen
|
||||
assert "[wort]" in e.ergebnis_text and "Kontowert [x]" in e.notiz, (e.ergebnis_text, e.notiz)
|
||||
assert "[konto]" in e.notiz, e.notiz
|
||||
texte = [e.ausgabe, e.ergebnis_text, e.log, e.notiz] + [titel + " " + text for titel, text in e.welt.meldungen]
|
||||
for text in texte:
|
||||
for geheim in (TESTWORT, KONTO, BETRAG, BETRAG_OHNE_GELDWORT):
|
||||
assert geheim not in text, (geheim, text)
|
||||
|
||||
|
||||
def test_wsl_alpaca_helfer_gibt_stueck_ohne_betrag():
|
||||
nur_in_wsl()
|
||||
import abbau_alpaca_lesen as helfer
|
||||
|
||||
jetzt = datetime(2026, 10, 8, 14, 0, tzinfo=timezone.utc)
|
||||
daten = helfer.lies(FalscherAlpaca(jetzt), jetzt)
|
||||
text = json.dumps(daten)
|
||||
assert daten["uhr"] == {"offen": True, "sekunden_bis_schluss": 1200}, daten
|
||||
assert daten["positionen"] == [{"symbol": "AAPL", "menge": 10}, {"symbol": "KO", "menge": -20}], daten
|
||||
assert daten["orders"] == 1 and daten["bestand_fehler"] is None, daten
|
||||
for betrag in ("1234.56", "98.76", "1111.11", "55.55"):
|
||||
assert betrag not in text, (betrag, text)
|
||||
|
||||
|
||||
def test_wsl_alpaca_helfer_maskiert_konto_und_betraege_im_fehlertext():
|
||||
nur_in_wsl()
|
||||
import abbau_alpaca_lesen as helfer
|
||||
|
||||
jetzt = datetime(2026, 10, 8, 14, 0, tzinfo=timezone.utc)
|
||||
fehler = f"Order an {KONTO}: limit {BETRAG_OHNE_GELDWORT}, Kontowert {BETRAG} USD"
|
||||
daten = helfer.lies(FalscherAlpaca(jetzt, fehler=fehler), jetzt)
|
||||
text = json.dumps(daten)
|
||||
assert daten["positionen"] is None and daten["orders"] is None, daten
|
||||
assert daten["bestand_fehler"].startswith("RuntimeError: "), daten
|
||||
assert "Order an [konto]" in daten["bestand_fehler"] and "limit [x]" in daten["bestand_fehler"], daten
|
||||
for geheim in (KONTO, BETRAG, BETRAG_OHNE_GELDWORT):
|
||||
assert geheim not in text, (geheim, text)
|
||||
assert daten["uhr"] == {"offen": True, "sekunden_bis_schluss": 1200}, daten
|
||||
|
||||
|
||||
def test_wsl_ibkr_lesen_ohne_erreichbaren_ordner_startet_nichts():
|
||||
nur_in_wsl()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
leer = Path(tmp)
|
||||
ohne = subprocess.run(["bash", str(IBKR_SKRIPT)], cwd=leer, capture_output=True, text=True, timeout=60)
|
||||
fehlt = subprocess.run(
|
||||
["bash", str(IBKR_SKRIPT), "/nicht-vorhanden/ibkr"], cwd=leer, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
assert ohne.returncode == 2, (ohne.returncode, ohne.stderr)
|
||||
assert fehlt.returncode == 4, (fehlt.returncode, fehlt.stderr)
|
||||
|
||||
|
||||
# --- Lauf als Skript -------------------------------------------------------------------------------
|
||||
|
||||
def main() -> int:
|
||||
faelle = [(name, wert) for name, wert in globals().items() if name.startswith("test_") and callable(wert)]
|
||||
abweichungen = 0
|
||||
uebersprungen = 0
|
||||
for name, fall in faelle:
|
||||
try:
|
||||
fall()
|
||||
except Uebersprungen as grund:
|
||||
uebersprungen += 1
|
||||
print(f"UEBERSPRUNGEN {name}: {grund}")
|
||||
continue
|
||||
except (Exception, SystemExit) as ausnahme:
|
||||
abweichungen += 1
|
||||
print(f"FEHL {name}: {type(ausnahme).__name__}: {str(ausnahme)[:400]!r}")
|
||||
continue
|
||||
print(f"OK {name}")
|
||||
print(f"ERGEBNIS: {len(faelle)} Faelle, {abweichungen} Abweichung(en), {uebersprungen} uebersprungen")
|
||||
return 1 if abweichungen else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user