Add REST API with dashboard and configurable port (server.host/port, TM_PORT, --port)
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user