Files
QuantumTerminal/backend/tradovate_routes.py
T

145 lines
6.0 KiB
Python

# version: v1
"""
tradovate_routes.py — HTTP API shell for the Tradovate POC.
Routes mirror the MT5 management routes so the SettingsPanel can drive
both providers with the same UX pattern. Tradovate state is held in the
singleton TradovateProvider from providers/tradovate_provider.py.
GET /api/tradovate/config → read saved creds + status
PATCH /api/tradovate/config → save creds (env, app_id, cid, sec,
username, password)
POST /api/tradovate/connect → authenticate with current creds
POST /api/tradovate/disconnect → clear tokens
GET /api/tradovate/bars/{ticker}?timeframe=&count= → fetch bars
Reversibility: this module is standalone. Nothing in MT5 or the base
terminal references it. Dropping the include_router() line from
consumer_startup.py removes every route above in one edit.
"""
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from providers.tradovate_provider import get_tradovate_provider
log = logging.getLogger("tradovate_routes")
class TradovateConfigBody(BaseModel):
env: Optional[str] = None # "demo" | "live"
app_id: Optional[str] = None
app_version:Optional[str] = None
cid: Optional[str] = None
sec: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
device_id: Optional[str] = None
def create_tradovate_router(cfg_manager=None) -> APIRouter:
router = APIRouter(tags=["tradovate"])
def _cfg_store() -> dict:
"""Slice of user_config where Tradovate creds live. Uses
cfg_manager's internal _user_config dict (same pattern mt5_routes
uses) so edits persist to disk via _save_user_config()."""
if cfg_manager is None or not hasattr(cfg_manager, "_user_config"):
# Fallback: in-memory only (creds lost on restart)
if not hasattr(_cfg_store, "_mem"):
_cfg_store._mem = {}
store = _cfg_store._mem
else:
store = cfg_manager._user_config
providers = store.setdefault("providers", {}) if isinstance(store, dict) else {}
accounts = providers.setdefault("accounts", {}) if isinstance(providers, dict) else {}
tv = accounts.setdefault("tradovate_default", {
"id": "tradovate_default",
"type": "tradovate",
"label": "Tradovate",
"enabled": False,
"env": "demo",
"app_id": "",
"app_version": "1.0",
"cid": "",
"sec": "",
"username": "",
"password": "",
"device_id": "QuantumTerminal-consumer-poc",
})
return tv
def _masked(cfg: dict) -> dict:
"""Never return secrets to the UI — mask them so we don't leak."""
out = dict(cfg or {})
for k in ("sec", "password"):
if out.get(k):
out[k] = "••••••"
return out
# ── GET /api/tradovate/config ──────────────────────────────
@router.get("/api/tradovate/config")
async def get_config():
cfg = _cfg_store()
prov = get_tradovate_provider(cfg)
return {"config": _masked(cfg), "status": prov.status_dict}
# ── PATCH /api/tradovate/config ────────────────────────────
@router.patch("/api/tradovate/config")
async def patch_config(body: TradovateConfigBody):
cfg = _cfg_store()
updates = {k: v for k, v in body.model_dump().items() if v is not None and v != ""}
cfg.update(updates)
# Persist via the same internal save hook config_manager uses.
try:
if cfg_manager and hasattr(cfg_manager, "_save_user_config"):
cfg_manager._save_user_config()
except Exception as e:
log.warning(f"_save_user_config failed: {e}")
# Push new values into the live provider instance (clears token cache)
get_tradovate_provider().update_config(cfg)
return {"config": _masked(cfg), "status": get_tradovate_provider().status_dict}
# ── POST /api/tradovate/connect ────────────────────────────
@router.post("/api/tradovate/connect")
async def connect():
cfg = _cfg_store()
prov = get_tradovate_provider(cfg)
result = await prov.authenticate()
return result
# ── POST /api/tradovate/disconnect ─────────────────────────
@router.post("/api/tradovate/disconnect")
async def disconnect():
get_tradovate_provider().disconnect()
return {"success": True, "status": get_tradovate_provider().status_dict}
# ── GET /api/tradovate/bars/{ticker} ───────────────────────
@router.get("/api/tradovate/bars/{ticker}")
async def get_bars(ticker: str, timeframe: str = "M15", count: int = 200):
prov = get_tradovate_provider()
if not prov.connected:
raise HTTPException(503, {"error": "tradovate_not_connected",
"message": "Connect to Tradovate in Settings → Providers first."})
try:
bars = await prov.get_bars(ticker, timeframe, count)
except ValueError as e:
# Unmapped ticker — clean 404
raise HTTPException(404, {"error": "symbol_not_supported",
"message": str(e)})
except Exception as e:
raise HTTPException(500, {"error": "tradovate_bars_failed",
"message": str(e)})
return {
"ticker": ticker.upper(),
"timeframe": timeframe,
"bars": bars,
"delayed": prov.is_delayed,
"count": len(bars),
}
return router