Initial commit: Quantum Terminal — Free & Open Source Trading Platform
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# version: v2
|
||||
"""Pulse Room REST endpoints.
|
||||
|
||||
Wire into data_server.py:
|
||||
from pulse_room.routes import create_pulse_router
|
||||
app.include_router(create_pulse_router())
|
||||
|
||||
Endpoints:
|
||||
GET /api/pulse/state — full state (scanner, watchlist, snapshot, meta)
|
||||
POST /api/pulse/calculate — rebuild snapshot (sync; 409 if in-flight)
|
||||
PUT /api/pulse/watchlist — replace watchlist
|
||||
PUT /api/pulse/scanner — set scanner enabled flag
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from . import builder, state
|
||||
# Concurrency: only one Calculate at a time. Lock lives in state.py so
|
||||
# sync_hook.py shares the same instance and the two paths can't race.
|
||||
from .state import calc_lock as _calc_lock
|
||||
|
||||
|
||||
log = logging.getLogger("mk.pulse_room.routes")
|
||||
|
||||
|
||||
# ---------- request models ----------
|
||||
|
||||
class WatchlistAssetIn(BaseModel):
|
||||
symbol: str
|
||||
families: list[str]
|
||||
|
||||
|
||||
class WatchlistIn(BaseModel):
|
||||
assets: list[WatchlistAssetIn]
|
||||
|
||||
|
||||
class ScannerIn(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
# ---------- helpers ----------
|
||||
|
||||
def _read_snapshot() -> tuple[dict | None, dict]:
|
||||
"""Return (snapshot, meta). Snapshot may be None if not yet built."""
|
||||
path = state._pulse_dir() / "pulse_snapshot.json"
|
||||
if not path.exists():
|
||||
return None, {"exists": False}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
snap = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None, {"exists": False}
|
||||
meta = {
|
||||
"exists": True,
|
||||
"computed_at": snap.get("computed_at"),
|
||||
"source_sync_at": snap.get("source_sync_at"),
|
||||
"watchlist_hash": snap.get("watchlist_hash"),
|
||||
"asset_count": len(snap.get("assets", {})),
|
||||
"size_bytes": path.stat().st_size,
|
||||
}
|
||||
return snap, meta
|
||||
|
||||
|
||||
def _build_state_response() -> dict:
|
||||
snap, meta = _read_snapshot()
|
||||
return {
|
||||
"scanner_enabled": state.load_scanner().get("enabled", False),
|
||||
"watchlist": state.load_watchlist(),
|
||||
"snapshot_meta": meta,
|
||||
"snapshot": snap,
|
||||
}
|
||||
|
||||
|
||||
# ---------- router ----------
|
||||
|
||||
def create_pulse_router() -> APIRouter:
|
||||
router = APIRouter(prefix="/api/pulse", tags=["pulse"])
|
||||
|
||||
@router.get("/state")
|
||||
async def get_state():
|
||||
return _build_state_response()
|
||||
|
||||
@router.put("/scanner")
|
||||
async def put_scanner(body: ScannerIn):
|
||||
state.save_scanner({"enabled": body.enabled})
|
||||
return _build_state_response()
|
||||
|
||||
@router.put("/watchlist")
|
||||
async def put_watchlist(body: WatchlistIn):
|
||||
wl = {"assets": [a.model_dump() for a in body.assets]}
|
||||
state.save_watchlist(wl)
|
||||
return _build_state_response()
|
||||
|
||||
@router.post("/calculate")
|
||||
async def post_calculate():
|
||||
wl = state.load_watchlist()
|
||||
if not wl.get("assets"):
|
||||
raise HTTPException(400, detail="Watchlist is empty")
|
||||
|
||||
if not _calc_lock.acquire(blocking=False):
|
||||
raise HTTPException(409, detail="A calculate is already in progress")
|
||||
try:
|
||||
try:
|
||||
# build_snapshot does sync file I/O; offload to a worker
|
||||
# thread so we don't block the FastAPI event loop (which
|
||||
# would freeze /api/ticks, /ws/prices, etc. for 0.5–3 s).
|
||||
await asyncio.to_thread(builder.build_snapshot, wl, write=True)
|
||||
except Exception as e:
|
||||
log.exception("Calculate failed")
|
||||
raise HTTPException(500, detail=f"Calculate failed: {e}") from e
|
||||
finally:
|
||||
_calc_lock.release()
|
||||
|
||||
return _build_state_response()
|
||||
|
||||
return router
|
||||
Reference in New Issue
Block a user