Files
AH-Oil-Trader/core/config.py
T
Axel HocksandClaude Opus 4.8 c1994b369a Nacht-Guard für Auto-Squeeze wieder AN (0–7 Uhr keine Trades)
auto_squeeze_skip_night=true (User-Vorgabe): der Bot öffnet nachts keine
autonomen Squeeze-Trades mehr — löst „muss automatisch schließen wenn ich
nicht da bin" durch Nicht-Öffnen, und ist gemessen ohnehin unrentabel
(Nacht-Spread/ATR 0,32–0,50, beide Halbjahre negativ). Tagsüber bleibt der
Auto-Close-Stack (Broker-SL + Trailing + Time-Stop + S/R-Close).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:03:13 +02:00

420 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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,53 %."""
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,21,5 zu eng war (MAE ~1,7×ATR, 3137 % 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 = M1M15
# (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,500,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. 0 = AUS (User-Vorgabe 2026-07-24: NICHTS beim
# Öffnen setzen — der manuell gesetzte/gemerkte `sr_close_min_gain` bleibt dann
# maßgeblich). War kurz 1,0→3,0 (2026-07-23), auf User-Wunsch wieder 0.
"sr_close_min_gain_pct": "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.
"entry_room_atr": "0.6",
# Tageszeit-Gate: Komma-Liste der Stunden (Berlin), zu denen die Wellen-
# Empfehlung WARTEN erzwingt. **Gemessener Default = 07,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:3016: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 07 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,320,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",
},
# (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}")