Initial commit: Oil Trading Bot (MT5, WTI)
Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/), Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+418
@@ -0,0 +1,418 @@
|
||||
"""
|
||||
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",
|
||||
# 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 (User-Vorgabe 2026-07-23, 1%→3% am selben Tag
|
||||
# nachgezogen zusammen mit dem margin-%-Notfall-Stop). 0 = aus (dann bleibt
|
||||
# der manuell gesetzte/gemerkte `sr_close_min_gain` unverändert 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 (User-Vorgabe 2026-07-23,
|
||||
# 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.
|
||||
# ⚠ Gleiche Kopplungs-Warnung wie beim Balance-%-Modus: unter Margin-Sizing kann
|
||||
# ein enger %-Stop schneller greifen als der 2×ATR-SL — bewusste User-Vorgabe.
|
||||
"auto_emergency_margin_pct": "3.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.
|
||||
"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": "false",
|
||||
},
|
||||
# (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}")
|
||||
Reference in New Issue
Block a user