""" core/config.py — Globale Konstanten + Config-Loader ===================================================== Zentrale Stelle für alle Magic-Numbers, Farben, Zeitintervalle und das Laden der `oil_widget_config.ini`. """ from __future__ import annotations import configparser from pathlib import Path import MetaTrader5 as mt5 from core.logger import get_logger log = get_logger("config") # ══════════════════════════════════════════════ # PFADE # ══════════════════════════════════════════════ PROJECT_ROOT = Path(__file__).parent.parent CONFIG_FILE = PROJECT_ROOT / "oil_widget_config.ini" HISTORY_DB_FILE = PROJECT_ROOT / "oil_widget_history.db" # ══════════════════════════════════════════════ # TRADING-KONSTANTEN # ══════════════════════════════════════════════ SYMBOL_CANDIDATES = [ # WTI / USOIL — gehandeltes Instrument (SpotCrude). Reine WTI-Kandidatenliste. # (Brent-Umstellung 2026-07-09 wurde am selben Tag zurückgenommen.) "SpotCrude", "USOIL", "WTI", "XTIUSD", "WTICOUSD", "OIL.WTI", "CRUDE.WTI", "USOilSpot", "SpotWTI", "WTISpot", "USCrude", "USOilCash", ] MARGIN_BUFFER = 0.90 # Default; Laufzeitwert aus `[trading] margin_buffer_pct` # (aktuell 90). Lot-Größe = ~90 % der freien Margin # (User-Vorgabe) via `calc_lots`. RISK_PER_TRADE = 0.015 # AKTIV: Verlust beim Initial-SL ≈ 1,5 % der Equity je # Trade (`calc_lots_risk`). Laufzeitwert aus # `[trading] risk_pct`. 0 = aus → Fallback margin-basiert # (MARGIN_BUFFER). Behebt die großen EUR-Verluste # (90 %-Margin × 2×ATR-SL) — `docs/risiko-management.md`. DEVIATION = 30 MAGIC = 230002 def set_margin_buffer(pct: float) -> float: """Setzt den globalen Margin-Buffer zur Laufzeit (1-99 %).""" global MARGIN_BUFFER MARGIN_BUFFER = max(0.01, min(0.99, pct / 100.0)) log.info(f"Margin-Buffer geändert: {MARGIN_BUFFER*100:.0f}%") return MARGIN_BUFFER def get_margin_buffer() -> float: """Aktuellen Margin-Buffer abrufen (Module-Level globals sind Snapshot-frei).""" return MARGIN_BUFFER def set_risk_per_trade(pct: float) -> float: """Setzt das Risiko pro Trade zur Laufzeit (% der Equity). 0 = aus (Fallback margin-basiert). Sinnvoll 0,5–3 %.""" global RISK_PER_TRADE RISK_PER_TRADE = max(0.0, min(0.10, pct / 100.0)) log.info(f"Risiko/Trade geändert: {RISK_PER_TRADE*100:.2f}% " f"({'risiko-basiert' if RISK_PER_TRADE > 0 else 'aus → margin-basiert'})") return RISK_PER_TRADE def get_risk_per_trade() -> float: return RISK_PER_TRADE # ══════════════════════════════════════════════ # STOP-LOSS / TAKE-PROFIT # ══════════════════════════════════════════════ SL_TF = mt5.TIMEFRAME_M15 SL_LOOKBACK = 60 SL_WINDOW = 4 SL_BUFFER_TICKS = 5 INIT_SL_FALLBACK = 0.012 # 1.2 % INIT_SL_MIN_ATR = 1.8 # Initial-SL-Distanz MIN 1.8×ATR(M15) — verhindert # zu enge Stops, wenn der nächste Pivot direkt am # Einstieg liegt (sonst Stop nach Sekunden auf Rauschen). INIT_SL_MAX_ATR = 2.2 # Initial-SL-Distanz MAX 2.2×ATR(M15). Band [1.8 … 2.2] # (Ziel 2,0): Exit-Simulation (backtest_exit.py) zeigte, # dass 1,2–1,5 zu eng war (MAE ~1,7×ATR, 31–37 % Früh- # stopps); 2,0 hebt Ø-R +32 %, PF 1,33→1,39, Worst-Case # auf 2,0×ATR begrenzt. Doku: docs/exit-simulation.md. INIT_TP_RR = 2.0 # ══════════════════════════════════════════════ # ANALYSE # ══════════════════════════════════════════════ M15_BARS = 60 EMA_FAST = 5 EMA_SLOW = 13 PIVOT_WINDOW = 3 ANGLE_LR_BARS = 14 SR_LOOKBACK = 150 SR_PIVOT_WIN = 4 SR_MIN_TOUCHES = 3 SR_TOL_ATR_FACTOR = 0.5 SR_MAX_LINES = 2 TL_MIN_PIVOTS = 2 # ══════════════════════════════════════════════ # ANALYSE-DATENFENSTER # ══════════════════════════════════════════════ CHART_BARS = 50 # Bar-Fenster der M15-Analyse (Trendlinien-Mapping) # ══════════════════════════════════════════════ # TIMINGS # ══════════════════════════════════════════════ PRICE_REFRESH_MS = 500 TREND_REFRESH_MS = 5_000 POS_REFRESH_MS = 1_000 BLINK_INTERVAL_MS = 380 BLINK_COUNT_MAX = 3 AUTO_BTN_ON_BG = "#1f6feb" # Blau – AUTO-Button aktiv AUTO_BTN_ON_FG = "#ffffff" AUTO_BTN_ON_ACT = "#3081f0" AUTO_BTN_OFF_BG = "#1a1e24" AUTO_BTN_OFF_FG = "#8b949e" WIDGET_X = 20 WIDGET_Y = 55 # ══════════════════════════════════════════════ # FARBEN # ══════════════════════════════════════════════ BG = "#0d1117" BORDER_NEUTRAL = "#30363d" BORDER_POS = "#1a4731" BORDER_NEG = "#3d1a1a" BORDER_BLINK = "#1f6feb" BLINK_BG = "#0d1f3c" ACCENT = "#e8a000" UP_COLOR = "#3fb950" DOWN_COLOR = "#f85149" SPREAD_COLOR = "#58a6ff" TEXT_DIM = "#8b949e" TEXT_BRIGHT = "#e6edf3" GRID_COLOR = "#161b22" EMA_F_COLOR = "#e8a000" EMA_S_COLOR = "#58a6ff" # Buttons BTN_LONG_BG = "#1a6e35"; BTN_LONG_FG = "#ffffff"; BTN_LONG_ACT = "#26a050" BTN_SHORT_BG = "#8b1a1a"; BTN_SHORT_FG = "#ffffff"; BTN_SHORT_ACT = "#b02020" BTN_CLOSE_BG = "#2d2000"; BTN_CLOSE_FG = "#e8a000"; BTN_CLOSE_ACT = "#4a3600" BTN_DIS_BG = "#1a1e24"; BTN_DIS_FG = "#3a4049" # ══════════════════════════════════════════════ # CONFIG-FILE LOADER # ══════════════════════════════════════════════ DEFAULT_CONFIG = { "openai": { "api_key": "", "model": "gpt-4o-mini-search-preview", "auto_refresh": "true", "refresh_min": "60", "search_context": "low", }, "gemini": { # Key gehört NUR in die lokale oil_widget_config.ini, nie in den Code "api_key": "", "model": "gemini-2.0-flash", "enabled": "true", }, "anthropic": { # Claude-Key (sk-ant-…) — nur in die lokale config.ini, nie in den Code "api_key": "", "model": "claude-opus-4-8", }, "ollama": { # Lokales LLM via Ollama (kein Key, kein Quota, läuft offline) "base_url": "http://localhost:11434", "model": "qwen2.5:7b", # Modell zwischen den Ticks geladen halten (GPU-resident, ~4,7 GB VRAM). # "0" entlädt sofort nach jedem Aufruf, "-1" hält unbegrenzt. "keep_alive": "30m", }, "agent": { "enabled": "true", # KI-Copilot automatisch laufen lassen "provider": "local", # local (Ollama) | claude | gemini | openai | zai | kimi "model": "", # leer = Provider-Default "refresh_min": "5", # Intervall der Lagebeurteilung "telegram": "false", # Beurteilung zusätzlich per Telegram pushen }, "kimi": { # Kimi / Moonshot AI (OpenAI-kompatibel). Key gehört in die ini (Secret). # Endpoint = international .ai (der .cn-Endpoint akzeptiert diesen Key NICHT). # kimi-k2.6 = Reasoning-Modell → verbraucht ~400 reasoning_tokens VOR dem # content; _call_kimi setzt max_tokens großzügig + temperature=1 (Pflicht). "api_key": "", "base_url": "https://api.moonshot.ai/v1", "model": "kimi-k2.6", }, "deepseek": { # DeepSeek (OpenAI-kompatibel, api.deepseek.com). deepseek-v4-flash ist ein # Reasoning-Modell (content nach reasoning_content → max_tokens großzügig). # ⚠ KEINE Web-Suche (rein Chat) → NICHT für daily_levels geeignet, nur Copilot. "api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash", }, "web": { # Mobile-Web-Backend (server.py). Lese-Endpoints (Snapshot/WebSocket) # sind offen; Trade-Aktionen brauchen diesen Token im X-Auth-Token-Header. # Leer = wird beim ersten Start automatisch generiert und geloggt. "api_token": "", "require_confirm": "true", # Bestätigungs-Dialog vor jeder Order im UI # Token-Pflicht für Trade-Aktionen. "false" = kein Token nötig # (nur sinnvoll, wenn der Zugang anderweitig abgesichert ist, z.B. WireGuard). "require_token": "true", }, "news": { "enabled": "true", "refresh_min": "10", }, "translation": { "enabled": "true", "target_language": "de", "provider": "google", "show_original": "false", }, "telegram": { "enabled": "false", "bot_token": "", "chat_id": "", }, "trading": { "margin_buffer_pct": "90", # Risiko-basiertes Sizing: Verlust beim Initial-SL ≈ risk_pct % der Equity. # 0 = aus → margin-basiert (margin_buffer_pct). Behebt große EUR-Verluste. "risk_pct": "1.5", "last_symbol": "", "atr_tf": "Auto", # Quellensteuer auf Gewinn-Trades (Broker behält pro Gewinn ein). # DE: Abgeltungsteuer 25% + Soli 5,5% = 26.375. 0 = keine WHT. "wht_pct": "0", # Wer wählt den Analyse-Timeframe der Empfehlung? # heuristic = schnelle Volatilitäts-/Trend-Heuristik (jede Minute, kein LLM) # agent = der KI-Agent (LLM) wählt # M1/M5/M15/M30/H1 = fest "tf_select": "heuristic", # Band, aus dem die Heuristik die Basis-TF wählt. Daytrading = M1–M15 # (M30/H1 bleiben nur Kontext: Filter/Konfluenz). tf_min = niedrigste, # tf_max = höchste erlaubte TF. "tf_min": "M5", "tf_max": "H1", # ATR-Breakout-Bestätigung: Signal erst handeln, wenn der Kurs k×ATR in # Signalrichtung lief (gemessen ~2× Edge/Trade). 0 = aus (Sofort-Einstieg). "breakout_k": "1.0", # Fixwert-Notfall-Stop = aus (0). Der frühere 2%-Auto-Arm war ~0,76×ATR = 3× # enger als der Squeeze-SL und schüttelte validierte Ausbrüche raus (real # 2026-07-17: −48,94 €). Seit 2026-07-20 läuft stattdessen das %-GAP-NETZ # (auto_emergency_pct=3.0, s. u.) — nur in Kombination mit risk_pct=1.5 sinnvoll. "auto_emergency_loss": "0", # Mindest-Notfall-Stop (Kontowährung): ein manuell gesetzter Wert DARUNTER # wird als „aus" behandelt (Vertipper-Schutz — ein Sub-Spread-Wert schließt # sofort und vererbt sich per _emergency_remember auf alle Folge-Trades; # real 2026-07-27 killte ein −1-€-Stop jeden Trade in Sekunden). 0 = kein Floor. "emergency_min_eur": "3", # Circuit Breaker (Tagesverlust-Stopp, User-Vorgabe 2026-07-27): erreicht der # Tages-P&L (realisiert + offen) −pct% der Balance, schließt der Bot die # Position und sperrt neue Auto-Trades bis zum nächsten Tag. Überlebens-Netz # gegen Verlust-Kaskaden (früher abgelehnt, mit dem Autonomie-Ziel reaktiviert). # ⚠ Unter 80 %-Margin schützt es v. a. gegen die Serie, nicht den Einzeltrade. # 0 = aus. "daily_loss_limit_pct": "0", # Gewinn-mitnehmen automatisch beim Öffnen setzen (Kontowährung). 0 = aus # (dann nur manuell im UI). Merkt den zuletzt gesetzten Wert. "auto_takeprofit": "0", # Automatischer S/R-Close (gemessen `backtest_srclose_prob.py`): schließt eine # Position im PLUS selbstständig, wenn der Kurs am gegenüberliegenden Level ist # und das kalibrierte P(Durchbruch) unter `sr_close_pbreak` liegt (Level prallt # wahrscheinlich ab). true/false. "auto_sr_close": "true", # Schwelle der User-Regel „laufen lassen wenn P(Durchbruch) ≥ X, sonst close". # 0.60 = validiertes Optimum-Plateau (0,50–0,60 ~gleich). Band [0.30 … 0.90]. "sr_close_pbreak": "0.60", # S/R-Level als CSV nach MQL5\Files schreiben (für den SR_Levels.mq5-Indikator, # der sie im Desktop-Terminal zeichnet). true/false. "export_mql5_levels": "true", # Mindestgewinn (Kontowährung) für den S/R-Auto-Close: Trade wird am Level # nur geschlossen, wenn P&L ≥ diesem Betrag. Wird bei JEDER neuen Position # automatisch neu gesetzt, s. `sr_close_min_gain_pct` (0 dort = aus, dann bleibt # dieser Wert manuell/persistiert maßgeblich). ⚠ Gemessen ist ein Mindestgewinn # tendenziell SCHLECHTER als ohne (`backtest_srclose_prob.py`, Mindestgewinn- # Sektion: verliert monoton in beiden Hälften) — bewusster User-Opt-in. "sr_close_min_gain": "0", # Gewinn-Close automatisch auf X % des EINSATZES (Margin) der jeweiligen Position # setzen, sobald sie eröffnet wird. Verlauf: 1,0→3,0 (2026-07-23) → 0 (2026-07-24 # früh, „nichts beim Öffnen setzen") → **3,0 wieder AN (2026-07-24 abends, # User-Vorgabe — gilt NUR für den Gewinn-Close; der Notfall-Stop # `auto_emergency_margin_pct` bleibt 0/aus)**. 0 = aus (dann bleibt der manuell # gesetzte/gemerkte `sr_close_min_gain` maßgeblich). "sr_close_min_gain_pct": "3.0", # Auto-Notfall-Stop als % der Balance beim Öffnen (skaliert mit dem Konto). # >0 → Stop = pct% × Balance; 0 = aus. ⚠ Nur zusammen mit risk_pct>0 als # GAP-NETZ sinnvoll (dann pct ≈ 2× risk_pct → feuert nur bei Slippage/Gap # ÜBER den SL hinaus): bei Margin-Sizing ist ein %-Stop in hoher Vola enger # als der 2×ATR-SL und zerschießt die Squeeze-Strategie (gemessen 2026-07-17, # −48,94-€-Tag). 2026-07-20 kurz als 3%-Netz mit risk_pct=1.5 aktiv, mit der # Rückkehr zu Margin-Sizing (80 %) am selben Tag wieder AUS. Bleibt 0 — ersetzt # durch `auto_emergency_margin_pct` (s. u.), hat Vorrang wenn >0. "auto_emergency_pct": "0", # Notfall-Stop als % der EINSATZ-Margin der Position (analog zu # `sr_close_min_gain_pct`). >0 → Stop = pct% × Margin dieser Position, hat Vorrang # vor `auto_emergency_pct` (Balance) und dem Fixwert. 0 = AUS (User-Vorgabe # 2026-07-24: beim Öffnen NICHTS setzen). War kurz 3,0 (2026-07-23), wieder 0. # ⚠ Kopplungs-Warnung (falls reaktiviert): unter Margin-Sizing kann ein enger # %-Stop schneller greifen als der 2×ATR-SL. "auto_emergency_margin_pct": "0", # Entry-Raum-Gate: Signal → WARTEN, wenn das Gegenlevel (M5-Pivot in Trade- # Richtung) näher als X×ATR liegt (gemessen `backtest_entryroom.py` — Raum # <0,6 in beiden Hälften negativ: Ertrag am Level gedeckelt, Kosten fressen # den Rest). 0 = aus. **0,6→1,0 am 2026-07-24 (User: „baue alles ein"):** # 1,0 war von Anfang an in BEIDEN Hälften besser (H1 +415/H2 +266 R), Start # 0,6 war nur die konservative Stufe (1,0 filtert ~¾ der Signale → deutlich # mehr WARTEN-Phasen; gemessen trotzdem mehr Gesamtertrag). "entry_room_atr": "0.6", # Tageszeit-Gate: Komma-Liste der Stunden (Berlin), zu denen die Wellen- # Empfehlung WARTEN erzwingt. **Gemessener Default = 0–7,12,16** (Nacht- # Kostenfalle + 12/16 Uhr beidhälftig negativ, `backtest_hourly_split.py`). # LEER = Gate AUS. Die ini setzt es leer (User-Vorgabe 2026-07-22, bewusst # gegen die Messung). EIA-Blackout (Mi 15:30–16:30) ist separat, bleibt aktiv. "dead_hours": "0,1,2,3,4,5,6,7,12,16", # Autonomer Entry auf den Squeeze-Ausbruch (echte Order, nur flat). ⚠ Default # AUS — Autonomie nie stillschweigend an. true = Bot eröffnet selbständig. "auto_squeeze": "false", # Startup-Schonfrist (Sek.): nach Bot-Start schließt der Bot so lange KEINEN # laufenden Trade selbst (S/R-Close/Notfall/Time-Stop/Reverse) — verhindert # den Sofort-Close eines adoptierten Trades auf noch-instabilen Daten direkt # nach restart_server.bat. Broker-SL unberührt. 0 = aus. (User-Vorgabe 2026-07-20) "startup_close_grace_s": "60", # Stop-&-Reverse für den Auto-Squeeze = AUS (Default, User-Vorgabe 2026-07-17): # NUR der flat-Entry ist gemessen-validiert (2 Halbjahre, ØR +0,14…+0,23). Der # Reverse (Gegen-Trade bei Ausbruch sofort schließen + drehen) ist UNBELEGT, # verdoppelt Kosten und dreht am Ausbruch-Extrem (real 2026-07-17 18:55: Short # −29,64 geschlossen, dann LONG am Erschöpfungs-Top). true = Reverse an. "auto_squeeze_reverse": "false", # Dead-Hour-Guard für den Auto-Squeeze (keine Nacht-Entries 0–7 Uhr Berlin) = # AUS (User-Vorgabe 2026-07-17). Das live-Verlust-Argument war emergency-getrieben # und mit dem Notfall-Stop-Ausbau moot; der gemessene Nacht-Kosten-Befund # (`backtest_realcosts.py`, Spread÷Nacht-ATR 0,32–0,50) bleibt gültig → per # B4-Wochenmonitor prüfen, ob Nacht-Squeezes tragen. Code (`_SQUEEZE_NIGHT`, # `_squeeze_skip_night`) bleibt dormant/reaktivierbar (true = wieder an). "auto_squeeze_skip_night": "true", # ── Autonomer Entry auf die EMPFEHLUNG (Wave-Signal) — User-Wunsch 2026-07-30 # („probieren wir es trotzdem mal"). ⚠ GEMESSEN NICHT TRAGFÄHIG # (`backtest_auto_signal.py`, 80k M5, 2 Halbjahre): H1 durchgehend NEGATIV # (ØR −0,05…−0,18, PF 0,78–0,93), H2 positiv (+0,06…+0,15) = Regime-Kippen; # deckt sich mit dem Live-Dry-Run von 2026-07-23 (WR 29 %/PF 0,64). Default AUS. # Der User hat das nach vollständiger Information bewusst gewollt → Schalter # existiert, aber mit den gemessen BESTEN Parametern und Live-Monitor (B4/B5: # driftet es weg → zurückbauen). "auto_signal": "false", # Mindest-Konfidenz für den Auto-Signal-Entry. 75 = gemessen beste Kombi # (mit Nacht-Sperre: H1 −0,053 = mildester Verlust, H2 +0,141). ⚠ conf_pct ist # NICHT kalibriert (`analyze_verdict_calibration.py`) — höher ist nicht monoton # besser, 75 ist empirisch, nicht „mehr ist mehr". "auto_signal_min_conf": "75", # Nacht-Sperre 0–7 Uhr Berlin für den Auto-Signal-Entry (gemessene Kostenfalle, # verbessert JEDE Variante: H1 −0,157→−0,111 bzw. −0,127→−0,053). true = an. "auto_signal_skip_night": "true", # 15-Minuten-Regel (User-Wunsch 2026-07-30 „wenn der Trade nach 15 Minuten # gegen die Richtung läuft schließe ihn"): steht ein BOT-eröffneter Trade # 15 min nach Entry mehr als X×ATR im Minus → schließen (`closed_by=adverse15`). # ⚠ Wert GEMESSEN kalibriert (`backtest_auto_signal.py`): „sobald im Minus" # (=0,0) ist die SCHLECHTESTE Variante (WR bricht 39→31 %, ØR-Netto −0,067); # **0,5 ist die beste Regel-Variante** (H1 −0,111→−0,079). Netto über beide # Hälften bringt die Regel ~nichts (+0,001 vs +0,026 ohne) — sie ist ein # REGIME-Schutz (hilft im schlechten, kostet im guten). 2. Bestätigung von # `backtest_adverse_nosignal.py`. 0 = aus. "adverse_15min_atr": "0.5", # ECHTE Liquiditäts-Wände von Hyperliquid im MT5-Chart zeichnen (User-Wunsch # 2026-07-30). Holt `GET /api/walls` vom HL-Dashboard (Port 8001) — das größte # L2-Orderbuch-Level je Seite, BASIS-KORRIGIERT auf CFD-Niveau (HL notiert # ~0,45 $ tiefer). Anders als R/S nicht aus Pivots geschätzt, sondern echte # ruhende Orders. REINE ANZEIGE — Übertragbarkeit auf den CFD wird gerade # gemessen (`analyze_bookflow.py` im HL-Projekt). Fail-safe: HL-Server aus → # keine Linien, kein Fehler. false = aus. "export_hl_walls": "true", "hl_walls_url": "http://127.0.0.1:8001/api/walls", }, # (Der frühere `[setups]`-Block — Per-Setup-Toggles für den Dry-Run-Auto-Trader — # ist entfernt, 2026-07-23: verwaist seit dem Entfernen des Dry-Run-Traders, wurde # aber auch davor nirgends im Code gelesen. Die `[setups]`-Sektion kann in einer # bestehenden oil_widget_config.ini stehen bleiben, wird nur nicht mehr neu erzeugt.) # Stehende S/R-Zonen für den KI-Agenten — frei editierbar. # Format je Eintrag: name = lo, hi, kind (kind: support | demand | # resistance | supply | invalidation; Einzel-Level: lo == hi) "zones": { "demand": "74.98, 76.75, demand", "support": "76.40, 77.00, support", "res_low": "81.24, 82.64, resistance", "fvg": "88.00, 90.00, resistance", "res_top": "94.72, 94.72, resistance", "invalid": "75.40, 75.80, invalidation", }, } def parse_zones(cfg) -> list[dict]: """Liest die [zones]-Sektion in eine Liste {name, lo, hi, kind}.""" out: list[dict] = [] try: if not cfg.has_section("zones"): return out for name, val in cfg["zones"].items(): parts = [p.strip() for p in (val or "").split(",")] if len(parts) < 2: continue try: lo, hi = float(parts[0]), float(parts[1]) except ValueError: continue kind = parts[2].lower() if len(parts) >= 3 else "level" if lo > hi: lo, hi = hi, lo out.append({"name": name, "lo": lo, "hi": hi, "kind": kind}) except Exception: pass return out def load_config() -> configparser.ConfigParser: """Liest oil_widget_config.ini, ergänzt fehlende Defaults.""" cfg = configparser.ConfigParser() if CONFIG_FILE.exists(): cfg.read(CONFIG_FILE, encoding="utf-8") changed = False for section, defaults in DEFAULT_CONFIG.items(): if section not in cfg: cfg[section] = {} changed = True for k, v in defaults.items(): if k not in cfg[section]: cfg[section][k] = v changed = True if changed: with open(CONFIG_FILE, "w", encoding="utf-8") as f: f.write("# Config für den Oil Trading Server (server.py)\n") f.write("# Claude-Key (KI-Copilot): https://console.anthropic.com/\n\n") cfg.write(f) log.info(f"Config aktualisiert: {CONFIG_FILE}") if not cfg["anthropic"]["api_key"]: log.warning("Trage deinen Claude-Key in [anthropic] api_key ein") return cfg def save_config(cfg: configparser.ConfigParser): """Speichert die Config-Datei (nach Änderungen über das Widget).""" try: with open(CONFIG_FILE, "w", encoding="utf-8") as f: cfg.write(f) except Exception as e: log.error(f"Config-Save fehlgeschlagen: {e}")