202 lines
6.6 KiB
Python
202 lines
6.6 KiB
Python
"""Lädt und validiert die YAML-Konfiguration (inkl. API-Keys aus Env)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import yaml
|
|
|
|
SUPPORTED_EXCHANGES = (
|
|
"binance",
|
|
"kraken",
|
|
"coinbase",
|
|
"kucoin",
|
|
"bitmex",
|
|
"okx",
|
|
"bybit",
|
|
)
|
|
|
|
|
|
def _env(value: Optional[str]) -> str:
|
|
"""Ersetzt ${ENV_VAR} Referenzen durch den jeweiligen Umgebungsvariable-Wert."""
|
|
if value and value.startswith("${") and value.endswith("}"):
|
|
return os.environ.get(value[2:-1], "")
|
|
return value or ""
|
|
|
|
|
|
@dataclass
|
|
class ExchangeConfig:
|
|
name: str
|
|
api_key: str = ""
|
|
api_secret: str = ""
|
|
password: str = "" # zB. binance passphrase / kucoin passkey
|
|
sandbox: bool = True
|
|
|
|
@classmethod
|
|
def from_dict(cls, name: str, data: Dict[str, Any]) -> "ExchangeConfig":
|
|
data = data or {}
|
|
return cls(
|
|
name=name,
|
|
api_key=_env(str(data.get("api_key", ""))),
|
|
api_secret=_env(str(data.get("api_secret", ""))),
|
|
password=_env(str(data.get("password", ""))),
|
|
sandbox=bool(data.get("sandbox", True)),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TradingConfig:
|
|
quote_currency: str = "USDT"
|
|
base_currency: str = "BTC"
|
|
initial_balance: float = 10_000.0
|
|
position_size_pct: float = 0.10 # Anteil des Portfolios pro Trade
|
|
max_open_positions: int = 1
|
|
fee_pct: float = 0.001 # 0.1 % pro Order (Spread/fee)
|
|
slippage_pct: float = 0.0005
|
|
timeframe: str = "1h"
|
|
candles: int = 500
|
|
dry_run: bool = True # True = Simulation / Paper-Trading
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TradingConfig":
|
|
data = data or {}
|
|
return cls(
|
|
quote_currency=data.get("quote_currency", "USDT"),
|
|
base_currency=data.get("base_currency", "BTC"),
|
|
initial_balance=float(data.get("initial_balance", 10_000.0)),
|
|
position_size_pct=float(data.get("position_size_pct", 0.10)),
|
|
max_open_positions=int(data.get("max_open_positions", 1)),
|
|
fee_pct=float(data.get("fee_pct", 0.001)),
|
|
slippage_pct=float(data.get("slippage_pct", 0.0005)),
|
|
timeframe=data.get("timeframe", "1h"),
|
|
candles=int(data.get("candles", 500)),
|
|
dry_run=bool(data.get("dry_run", True)),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class StrategyConfig:
|
|
fast_period: int = 12
|
|
slow_period: int = 26
|
|
signal_period: int = 9
|
|
rsi_period: int = 14
|
|
rsi_overbought: float = 70.0
|
|
rsi_oversold: float = 30.0
|
|
atr_period: int = 14
|
|
atr_stop_mult: float = 2.5
|
|
allow_long: bool = True
|
|
allow_short: bool = False
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "StrategyConfig":
|
|
data = data or {}
|
|
return cls(
|
|
fast_period=int(data.get("fast_period", 12)),
|
|
slow_period=int(data.get("slow_period", 26)),
|
|
signal_period=int(data.get("signal_period", 9)),
|
|
rsi_period=int(data.get("rsi_period", 14)),
|
|
rsi_overbought=float(data.get("rsi_overbought", 70.0)),
|
|
rsi_oversold=float(data.get("rsi_oversold", 30.0)),
|
|
atr_period=int(data.get("atr_period", 14)),
|
|
atr_stop_mult=float(data.get("atr_stop_mult", 2.5)),
|
|
allow_long=bool(data.get("allow_long", True)),
|
|
allow_short=bool(data.get("allow_short", False)),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TrainingConfig:
|
|
mode: str = "walk-forward" # walk-forward | full
|
|
train_ratio: float = 0.7
|
|
generations: int = 20
|
|
population: int = 40
|
|
mutation_rate: float = 0.2
|
|
crossover_rate: float = 0.4
|
|
fitness_weight_return: float = 0.6
|
|
fitness_weight_sharpe: float = 0.3
|
|
fitness_weight_drawdown: float = 0.1
|
|
seed: int = 42
|
|
state_file: str = "state/weights.json"
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TrainingConfig":
|
|
data = data or {}
|
|
return cls(
|
|
mode=data.get("mode", "walk-forward"),
|
|
train_ratio=float(data.get("train_ratio", 0.7)),
|
|
generations=int(data.get("generations", 20)),
|
|
population=int(data.get("population", 40)),
|
|
mutation_rate=float(data.get("mutation_rate", 0.2)),
|
|
crossover_rate=float(data.get("crossover_rate", 0.4)),
|
|
fitness_weight_return=float(data.get("fitness_weight_return", 0.6)),
|
|
fitness_weight_sharpe=float(data.get("fitness_weight_sharpe", 0.3)),
|
|
fitness_weight_drawdown=float(data.get("fitness_weight_drawdown", 0.1)),
|
|
seed=int(data.get("seed", 42)),
|
|
state_file=data.get("state_file", "state/weights.json"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ServerConfig:
|
|
"""HTTP-API/Dashboard (Befehl `trademind serve`)."""
|
|
|
|
host: str = "0.0.0.0"
|
|
port: int = 8080
|
|
public_base_url: str = "" # optional, zB. Reverse-Proxy-URL für den Dashboard-Link
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "ServerConfig":
|
|
data = data or {}
|
|
return cls(
|
|
host=data.get("host", "0.0.0.0"),
|
|
port=int(data.get("port", 8080)),
|
|
public_base_url=data.get("public_base_url", "") or "",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
trading: TradingConfig
|
|
strategy: StrategyConfig
|
|
training: TrainingConfig
|
|
server: ServerConfig = field(default_factory=ServerConfig)
|
|
exchanges: Dict[str, ExchangeConfig] = field(default_factory=dict)
|
|
|
|
def active_exchange(self) -> Optional[ExchangeConfig]:
|
|
"""Erste Konfiguration mit aktiver API-Anbindung (oder der ersten)."""
|
|
if not self.exchanges:
|
|
return None
|
|
for cfg in self.exchanges.values():
|
|
if cfg.api_key and cfg.api_secret:
|
|
return cfg
|
|
return next(iter(self.exchanges.values()))
|
|
|
|
|
|
def load(path: str) -> Config:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
raw = yaml.safe_load(fh) or {}
|
|
|
|
trading = TradingConfig.from_dict(raw.get("trading", {}))
|
|
strategy = StrategyConfig.from_dict(raw.get("strategy", {}))
|
|
training = TrainingConfig.from_dict(raw.get("training", {}))
|
|
server = ServerConfig.from_dict(raw.get("server", {}))
|
|
|
|
exchanges: Dict[str, ExchangeConfig] = {}
|
|
for name, data in (raw.get("exchanges") or {}).items():
|
|
name = name.lower()
|
|
if name not in SUPPORTED_EXCHANGES:
|
|
raise ValueError(
|
|
f"Unbekannte Exchange '{name}'. Erlaubt: {', '.join(SUPPORTED_EXCHANGES)}"
|
|
)
|
|
exchanges[name] = ExchangeConfig.from_dict(name, data)
|
|
|
|
return Config(
|
|
trading=trading,
|
|
strategy=strategy,
|
|
training=training,
|
|
server=server,
|
|
exchanges=exchanges,
|
|
)
|