Initial commit: Quantum Terminal — Free & Open Source Trading Platform
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user