Initial release: TradeMind crypto trading bot with paper/live modes and strategy training
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""Command-line-Schnittstelle (click)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
from . import __version__
|
||||
from .config import Config, load as load_config
|
||||
from .engine import save_state
|
||||
from .exchange import MockBroker, build_exchange
|
||||
from .strategy import Strategy
|
||||
from .trader import Trader
|
||||
from .trainer import Trainer, apply_params
|
||||
|
||||
log = logging.getLogger("trademind.cli")
|
||||
|
||||
|
||||
def _setup_logging(verbose: bool) -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if verbose else logging.INFO,
|
||||
format="%(asctime)s %(levelname)-7s %(name)s %(message)s",
|
||||
stream=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def get_strategy(cfg: Config, weights_path: str | None = None) -> Strategy:
|
||||
import os
|
||||
|
||||
st = Strategy(cfg.strategy)
|
||||
if weights_path and os.path.exists(weights_path):
|
||||
with open(weights_path, "r", encoding="utf-8") as fh:
|
||||
wp = json.load(fh)
|
||||
st.weights = {**st.weights, **{k: float(v) for k, v in wp.get("weights", {}).items()}}
|
||||
return st
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(__version__)
|
||||
def cli() -> None:
|
||||
"""TradeMind – Krypto-Tradingbot (Paper/Sim + Live + Training)."""
|
||||
|
||||
|
||||
@cli.command("paper")
|
||||
@click.option("--config", "cfg_path", default="config.yaml", show_default=True)
|
||||
@click.option("--symbol", default=None, help="zB. BTC/USDT")
|
||||
@click.option("--candles", type=int, default=None)
|
||||
@click.option("--data", type=click.Choice(["auto", "live", "mock"]), default="auto",
|
||||
show_default=True, help="Kursdatenquelle: echte (live) oder generierte (mock)")
|
||||
@click.option("--exchange", default=None, help="Exchange für Kursdaten (zB. binance, kraken)")
|
||||
@click.option("--seed", type=int, default=None, help="Seed für deterministische Simulationsdaten")
|
||||
@click.option("--state", default="state/paper.json")
|
||||
def paper(cfg_path, symbol, candles, data, exchange, seed, state) -> None:
|
||||
"""Paper-/Simulationslauf: nutzt ECHTE Marktkurse (Standard), Orders bleiben simuliert.
|
||||
|
||||
Falls keine Kursdaten abrufbar sind (Offline), wird automatisch auf
|
||||
generierte mock-Daten zurückgefallen.
|
||||
"""
|
||||
cfg = load_config(cfg_path)
|
||||
if symbol:
|
||||
base, _, quote = symbol.partition("/")
|
||||
cfg.trading.base_currency = base
|
||||
cfg.trading.quote_currency = quote or "USDT"
|
||||
if candles:
|
||||
cfg.trading.candles = candles
|
||||
|
||||
broker = _data_broker(cfg, data, exchange, seed)
|
||||
log.info("Paper-Modus: Kursdaten-Quelle = %s", broker.name)
|
||||
strat = get_strategy(cfg, cfg.training.state_file)
|
||||
trader = Trader(cfg, broker, strat)
|
||||
res = trader.simulate()
|
||||
save_state(state, res, strat.parameters())
|
||||
click.echo(f"\n=== Paper-/Simulationslauf (Daten: {broker.name}) ===")
|
||||
click.echo(json.dumps(res.summary(), indent=2))
|
||||
|
||||
|
||||
def _data_broker(cfg: Config, data: str, exchange: str | None, seed: int | None):
|
||||
"""Live-Marktdaten via ccxt (ohne Keys). Fallback auf MockBroker."""
|
||||
from .exchange import build_data_broker
|
||||
|
||||
ex_name = exchange or (cfg.active_exchange().name if cfg.active_exchange() else "binance")
|
||||
if data != "mock":
|
||||
try:
|
||||
broker = build_data_broker(ex_name)
|
||||
broker.fetch_ticker(f"{cfg.trading.base_currency}/{cfg.trading.quote_currency}")
|
||||
return broker
|
||||
except Exception as e: # offline, Exchange down etc.
|
||||
if data == "live":
|
||||
raise SystemExit(f"Fehler beim Abruf der Live-Daten: {e}")
|
||||
click.echo(f"Warnung: Live-Daten nicht erreichbar ({e}). Fallback: mock-Daten.", err=True)
|
||||
return MockBroker(seed=seed if seed is not None else 7)
|
||||
|
||||
|
||||
@cli.command("live")
|
||||
@click.option("--config", "cfg_path", default="config.yaml", show_default=True)
|
||||
def live(cfg_path) -> None:
|
||||
"""Ein Live-Zyklus: Bewertung + echte Order über die konfigurierte Exchange."""
|
||||
cfg = load_config(cfg_path)
|
||||
ex = cfg.active_exchange()
|
||||
if not ex or not (ex.api_key and ex.api_secret):
|
||||
click.echo(
|
||||
"Keine Exchange mit API-Keys konfiguriert.\n"
|
||||
"Trage api_key/api_secret in config.yaml ein (oder über Env) und setze sandbox: false.",
|
||||
err=True,
|
||||
)
|
||||
sys.exit(2)
|
||||
try:
|
||||
broker = build_exchange(ex)
|
||||
except Exception as e: # pragma: no cover
|
||||
click.echo(f"Exchange-Init fehlgeschlagen: {e}", err=True)
|
||||
sys.exit(1)
|
||||
strat = get_strategy(cfg, cfg.training.state_file)
|
||||
trader = Trader(cfg, broker, strat)
|
||||
order = trader.live_cycle()
|
||||
if order:
|
||||
click.echo("Order: " + json.dumps(order, indent=2))
|
||||
else:
|
||||
click.echo("Kein Handels-Signal (HOLD).")
|
||||
|
||||
|
||||
@cli.command("train")
|
||||
@click.option("--config", "cfg_path", default="config.yaml", show_default=True)
|
||||
@click.option("--generations", type=int, default=None)
|
||||
@click.option("--population", type=int, default=None)
|
||||
@click.option("--data", type=click.Choice(["auto", "live", "mock"]), default="auto",
|
||||
show_default=True, help="Trainingsdatenquelle: echte (live) oder generierte (mock)")
|
||||
@click.option("--exchange", default=None, help="Exchange für Trainingsdaten (zB. binance, kraken)")
|
||||
def train(cfg_path, generations, population, data, exchange) -> None:
|
||||
"""Trainiert die Strategie-Parameter auf Kursdaten (Standard: echte Marktkurse)."""
|
||||
cfg = load_config(cfg_path)
|
||||
if generations:
|
||||
cfg.training.generations = generations
|
||||
if population:
|
||||
cfg.training.population = population
|
||||
|
||||
broker = _data_broker(cfg, data, exchange, 42)
|
||||
base_strat = get_strategy(cfg, cfg.training.state_file)
|
||||
symbol = f"{cfg.trading.base_currency}/{cfg.trading.quote_currency}"
|
||||
candles = broker.fetch_ohlcv(symbol, cfg.trading.timeframe, max(cfg.trading.candles, 500))
|
||||
log.info("Trainingsdaten: %s (%d Candles)", broker.name, len(candles))
|
||||
|
||||
def progress(gen: int, best: float) -> None:
|
||||
click.echo(f"Generation {gen:>3}: beste Fitness = {best:.4f}")
|
||||
|
||||
trainer = Trainer(cfg.training, base_strat)
|
||||
params = trainer.train(candles, cfg.trading, base_strat, progress=progress)
|
||||
|
||||
apply_params(cfg.strategy, params)
|
||||
import os
|
||||
|
||||
os.makedirs(os.path.dirname(cfg.training.state_file) or ".", exist_ok=True)
|
||||
with open(cfg.training.state_file, "w", encoding="utf-8") as fh:
|
||||
json.dump(
|
||||
{
|
||||
"params": params,
|
||||
"weights": {
|
||||
"ema_cross": params["w_ema_cross"],
|
||||
"rsi_long": params["w_rsi_long"],
|
||||
"rsi_exit": params["w_rsi_exit"],
|
||||
},
|
||||
},
|
||||
fh,
|
||||
indent=2,
|
||||
)
|
||||
click.echo("\n=== Training abgeschlossen ===")
|
||||
click.echo("Optimierte Parameter: " + json.dumps(params, indent=2))
|
||||
click.echo(f"Gewichte gespeichert in: {cfg.training.state_file}")
|
||||
|
||||
|
||||
@cli.command("show")
|
||||
@click.argument("path", default="config.yaml")
|
||||
def show(path) -> None:
|
||||
"""Gibt die geladene Konfiguration aus (API-Keys werden maskiert)."""
|
||||
cfg = load_config(path)
|
||||
out = {
|
||||
"trading": cfg.trading.__dict__,
|
||||
"strategy": cfg.strategy.__dict__,
|
||||
"training": cfg.training.__dict__,
|
||||
"exchanges": {
|
||||
n: {
|
||||
"api_key": "***" if e.api_key else "",
|
||||
"api_secret": "***" if e.api_secret else "",
|
||||
"sandbox": e.sandbox,
|
||||
}
|
||||
for n, e in cfg.exchanges.items()
|
||||
},
|
||||
}
|
||||
click.echo(json.dumps(out, indent=2))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user