"""Optionale REST-API + HTML-Dashboard (Befehl `trademind serve`). Endpoints: GET / → HTML-Dashboard GET /api/health → Liveness GET /api/status → Strategie-Konfiguration, zuletzt berechnetes Signal, letztes Paper-Ergebnis GET /api/signals → Signals der letzten Candles GET /api/equity → Equity-Kurve des letzten Paper-Laufs POST /api/paper → neuer Paper-Lauf (nutzt konfigurierte Datenquelle) Der Server ist read-only gegenüber dem Live-Handel: es werden hier nie echte Orders platziert. """ from __future__ import annotations import asyncio import html import json import logging from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, Optional import pandas as pd from . import __version__ from .config import Config, load as load_config from .engine import save_state from .exchange import MockBroker from .strategy import Strategy from .trader import Trader log = logging.getLogger("trademind.server") DASHBOARD_DIR = Path(__file__).parent / "static" def _safe_int(value: Any, default: int) -> int: try: return int(value) except (TypeError, ValueError): return default def _candles_to_signals(strategy: Strategy, candles: pd.DataFrame, limit: int = 50): prep = strategy.prepare(candles) signals = strategy.decide(prep) out = [] for i, sig in enumerate(signals[-limit:]): idx = len(signals) - len(signals[-limit:]) + i ts = None if "time" in candles.columns and len(candles) > idx: ts = str(pd.Timestamp(candles["time"].iloc[idx])) out.append( { "time": ts, "action": int(sig.action), "price": round(float(sig.price), 8), "score": round(float(sig.score), 4), "reason": sig.reason, } ) return out def make_app( cfg_path: str, data: str = "auto", exchange: Optional[str] = None, ) -> "FastAPI": """Erzeugt die FastAPI-App für eine Konfigurationsdatei.""" from fastapi import FastAPI from fastapi.responses import HTMLResponse, JSONResponse def _cfg() -> Config: return load_config(cfg_path) app = FastAPI(title="TradeMind", version=__version__) state: Dict[str, Any] = {} def _broker(cfg: Config): import os from .cli import _data_broker ex = exchange or (cfg.active_exchange().name if cfg.active_exchange() else "binance") return _data_broker(cfg, data, ex, int(os.environ.get("TM_SEED", "7"))) def _symbol(cfg: Config) -> str: return f"{cfg.trading.base_currency}/{cfg.trading.quote_currency}" @app.get("/api/health") def health() -> Dict[str, Any]: return {"status": "ok", "version": __version__} @app.get("/api/status") def status() -> Dict[str, Any]: cfg = _cfg() ex_names = [ { "name": name, "sandbox": entry.sandbox, "has_keys": bool(entry.api_key and entry.api_secret), } for name, entry in cfg.exchanges.items() ] last = state.get("last_run") return { "version": __version__, "symbol": _symbol(cfg), "trading": cfg.trading.__dict__, "strategy": cfg.strategy.__dict__, "exchanges": ex_names, "state_file": cfg.training.state_file, "last_run": last, } @app.get("/api/signals") def signals(limit: int = 50) -> Dict[str, Any]: cfg = _cfg() broker = _broker(cfg) candles = broker.fetch_ohlcv(_symbol(cfg), cfg.trading.timeframe, cfg.trading.candles) strat = Strategy(cfg.strategy) out = _candles_to_signals(strat, candles, limit) return { "broker": broker.name, "generated_at": datetime.now(timezone.utc).isoformat(), "signals": out, } @app.get("/api/equity") def equity() -> Dict[str, Any]: last = state.get("last_run") if last is None: return {"summary": None, "equity_curve": [], "hint": "POST /api/paper ausführen"} return { "summary": last["summary"], "equity_curve": last.get("equity_curve", []), } @app.post("/api/paper") async def paper() -> Dict[str, Any]: def _run() -> Dict[str, Any]: cfg = _cfg() broker = _broker(cfg) strat = Strategy(cfg.strategy) trader = Trader(cfg, broker, strat) res = trader.simulate() summary = res.summary() payload = { "summary": summary, "equity_curve": res.equity_curve, "broker": broker.name, "finished_at": datetime.now(timezone.utc).isoformat(), } save_state("state/paper.json", res, strat.parameters()) return payload payload = await asyncio.to_thread(_run) state["last_run"] = payload return payload @app.get("/", response_class=HTMLResponse) async def index() -> str: path = DASHBOARD_DIR / "dashboard.html" return path.read_text(encoding="utf-8") # Fehler-Fallback, damit auch unklare Requests JSON liefern @app.exception_handler(Exception) async def _exc(request, exc: Exception): # pragma: no cover log.exception("Fehler bei %s", request.url.path) return JSONResponse({"error": str(exc)}, status_code=500) app.state.cfg_path = cfg_path return app def run_server( cfg_path: str, host: str, port: int, data: str = "auto", exchange: Optional[str] = None, ) -> None: import uvicorn app = make_app(cfg_path, data=data, exchange=exchange) log.info("TradeMind-API läuft auf http://%s:%d (Dashboard: /)", host, port) uvicorn.run(app, host=host, port=port, log_level="info")