191 lines
5.9 KiB
Python
191 lines
5.9 KiB
Python
"""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")
|