Initial commit: Quantum Terminal — Free & Open Source Trading Platform
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
# version: v3
|
||||
"""
|
||||
================================================================================
|
||||
Quantum Terminal Consumer — Startup Gate & Route Wiring
|
||||
================================================================================
|
||||
v3 — consumer_gate() now accepts `skip_sync=False`. data_server's lifespan
|
||||
calls it with `skip_sync=True` so the auth phase runs synchronously
|
||||
(needed for login routing) while the slow sync_all() is kicked off in
|
||||
a background thread after lifespan returns. HTTP routes become
|
||||
available immediately; frontend polls /api/sync/status for the SYNCING
|
||||
pill. Eliminates the "15-minute loading screen" symptom caused by
|
||||
persistent 5xx responses on specific data endpoints.
|
||||
|
||||
v2 — Wire /api/version (version_info.py) so the app can report its running
|
||||
version and check for updates via /api/version endpoint.
|
||||
Bridges the consumer-specific modules (data_sync_client)
|
||||
into the existing data_server.py with MINIMAL changes to the PRO codebase.
|
||||
|
||||
Usage in data_server.py lifespan:
|
||||
from consumer_startup import consumer_gate, wire_consumer_routes
|
||||
|
||||
# In lifespan, BEFORE provider connect:
|
||||
gate_result = consumer_gate()
|
||||
if not gate_result["ok"]:
|
||||
# Terminal won't render — gate_result has details
|
||||
pass
|
||||
|
||||
# After app creation:
|
||||
wire_consumer_routes(app)
|
||||
|
||||
This module does NOT introduce any calculation triggers.
|
||||
================================================================================
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
log = logging.getLogger("mk.consumer_startup")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 1. CONSUMER STARTUP GATE (Rule C2)
|
||||
# ============================================================
|
||||
|
||||
def consumer_gate(skip_sync: bool = False) -> dict:
|
||||
"""
|
||||
Run the consumer startup gate (Rule C2).
|
||||
|
||||
Three conditions must be met before terminal renders:
|
||||
1. Auth always passes (Quantum Terminal is free)
|
||||
2. data_sync_client completes download OR loads from cache
|
||||
3. MT5 connection established (handled by existing data_server code)
|
||||
|
||||
This function handles conditions 1 and 2.
|
||||
|
||||
v3: when `skip_sync=True`, only the auth phase runs (fast). Caller is
|
||||
responsible for running sync in a background thread so the FastAPI
|
||||
lifespan isn't blocked by a slow/unhealthy server. `sync_summary`
|
||||
will be returned as {"in_progress": True} in that case — populate it
|
||||
from the thread when sync completes.
|
||||
Returns dict with gate status:
|
||||
{
|
||||
"ok": bool,
|
||||
"auth_result": AuthResult dict,
|
||||
"sync_summary": dict,
|
||||
"needs_login": bool,
|
||||
"needs_update": bool,
|
||||
"offline_mode": bool,
|
||||
}
|
||||
"""
|
||||
result = {
|
||||
"ok": False,
|
||||
"auth_result": None,
|
||||
"sync_summary": None,
|
||||
"needs_login": False,
|
||||
"needs_update": False,
|
||||
"offline_mode": False,
|
||||
}
|
||||
|
||||
# ── Gate 1: Auth validation ──
|
||||
# Removed in Quantum Terminal: Always authenticated
|
||||
result["auth_result"] = {"status": "ok", "message": "Quantum Terminal - Free Edition"}
|
||||
result["needs_login"] = False
|
||||
|
||||
# ── Gate 2: Data sync ──
|
||||
# v3: skip_sync=True → lifespan will run sync_all() in a background
|
||||
# thread instead, so the HTTP server is responsive immediately.
|
||||
if skip_sync:
|
||||
result["sync_summary"] = {"in_progress": True}
|
||||
else:
|
||||
try:
|
||||
from data_sync_client import get_sync_client
|
||||
sync_client = get_sync_client()
|
||||
sync_summary = sync_client.sync_all()
|
||||
result["sync_summary"] = sync_summary
|
||||
|
||||
# Rule C6: format version mismatch blocks terminal
|
||||
if not sync_summary.get("format_version_ok", True):
|
||||
log.error("Startup gate: FORMAT VERSION MISMATCH — update required")
|
||||
result["needs_update"] = True
|
||||
result["ok"] = False
|
||||
return result
|
||||
|
||||
if sync_summary.get("offline_mode", False):
|
||||
result["offline_mode"] = True
|
||||
|
||||
except ImportError:
|
||||
log.warning("data_sync_client not available — skipping data sync (dev mode)")
|
||||
result["sync_summary"] = {"note": "dev mode — no sync"}
|
||||
except Exception as e:
|
||||
log.error(f"Data sync error: {e}")
|
||||
result["sync_summary"] = {"error": str(e)}
|
||||
|
||||
result["ok"] = True
|
||||
log.info("Consumer startup gate PASSED")
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2. ROUTE WIRING
|
||||
# ============================================================
|
||||
|
||||
def wire_consumer_routes(app):
|
||||
"""
|
||||
Wire consumer-specific FastAPI routes into the existing app.
|
||||
Call this AFTER app = FastAPI() in data_server.py.
|
||||
|
||||
Adds:
|
||||
/api/auth/* — login, register, logout, status, refresh
|
||||
/api/sync/* — sync status, cone/options data, staleness, refresh
|
||||
/api/consumer/* — consumer-specific status endpoints
|
||||
"""
|
||||
routes_added = []
|
||||
|
||||
# Auth routes
|
||||
# Removed in Quantum Terminal
|
||||
|
||||
# Data sync routes
|
||||
try:
|
||||
from data_sync_client import create_data_sync_routes
|
||||
app.include_router(create_data_sync_routes())
|
||||
routes_added.append("data_sync")
|
||||
log.info("Consumer data sync routes wired: /api/sync/*")
|
||||
except ImportError:
|
||||
log.warning("data_sync_client not available — sync routes not wired")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to wire data sync routes: {e}")
|
||||
|
||||
# v2: version/update-check route
|
||||
try:
|
||||
from version_info import create_version_router, CURRENT_VERSION
|
||||
app.include_router(create_version_router())
|
||||
routes_added.append("version")
|
||||
log.info(f"Consumer version route wired: /api/version (running v{CURRENT_VERSION})")
|
||||
except ImportError:
|
||||
log.warning("version_info not available — /api/version not wired")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to wire version route: {e}")
|
||||
|
||||
# Consumer status endpoint
|
||||
from fastapi import APIRouter
|
||||
consumer_router = APIRouter(tags=["consumer"])
|
||||
|
||||
@consumer_router.get("/api/consumer/status")
|
||||
async def consumer_status():
|
||||
"""Consumer terminal status — used by frontend to decide what to show."""
|
||||
status = {
|
||||
"is_consumer": True,
|
||||
"routes_available": routes_added,
|
||||
}
|
||||
|
||||
# Auth status
|
||||
status["authenticated"] = True
|
||||
status["user_email"] = "free@quantumterminal"
|
||||
status["subscription_status"] = "active"
|
||||
|
||||
# Sync status
|
||||
try:
|
||||
from data_sync_client import get_sync_client
|
||||
sc = get_sync_client()
|
||||
status["synced_tickers"] = sc.synced_tickers
|
||||
status["offline_mode"] = sc.is_offline
|
||||
status["format_version_ok"] = sc.format_version_ok
|
||||
status["sync_in_progress"] = sc.is_syncing # drives the top-bar pill
|
||||
status["last_synced_at"] = sc.last_synced_at
|
||||
|
||||
staleness = sc.get_overall_staleness()
|
||||
status["analysis_date"] = staleness.display_date
|
||||
status["staleness_level"] = staleness.level.value
|
||||
except Exception:
|
||||
status["synced_tickers"] = []
|
||||
status["offline_mode"] = True
|
||||
status["sync_in_progress"] = False
|
||||
|
||||
return status
|
||||
|
||||
@consumer_router.post("/api/consumer/restart")
|
||||
async def consumer_restart():
|
||||
"""
|
||||
Restart the terminal backend process. Spawns a fresh instance and
|
||||
exits the current one after a brief delay so the HTTP response can
|
||||
flush before the process exits.
|
||||
|
||||
Rule C1 compliance: this restarts the display server only — no
|
||||
calculation modules are involved.
|
||||
"""
|
||||
import sys, os, subprocess, threading
|
||||
import time as _time
|
||||
|
||||
def _do_restart():
|
||||
_time.sleep(1.0)
|
||||
try:
|
||||
creationflags = 0
|
||||
if os.name == "nt":
|
||||
creationflags = getattr(subprocess, "DETACHED_PROCESS", 0) | \
|
||||
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
||||
subprocess.Popen(
|
||||
[sys.executable] + sys.argv,
|
||||
close_fds=True,
|
||||
creationflags=creationflags,
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Restart spawn failed: {e}")
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=_do_restart, daemon=True).start()
|
||||
log.warning("Consumer restart requested via /api/consumer/restart")
|
||||
return {"status": "restarting", "delay_ms": 1000}
|
||||
|
||||
app.include_router(consumer_router)
|
||||
log.info("Consumer status route wired: /api/consumer/status")
|
||||
|
||||
return routes_added
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3. BLOCKED ENDPOINTS (Rule C1)
|
||||
# ============================================================
|
||||
|
||||
# These endpoint prefixes must NEVER exist in the consumer terminal.
|
||||
# consumer_verify.py test script checks for them.
|
||||
BLOCKED_ENDPOINT_PREFIXES = [
|
||||
"/api/calculate",
|
||||
"/api/calibrate",
|
||||
"/api/calibration/run",
|
||||
"/api/run-backtest",
|
||||
"/api/wf/run",
|
||||
"/api/flow/calibrate",
|
||||
"/api/anchor/calibrate",
|
||||
"/api/dev/clear-cache", # calc target clears calculation data
|
||||
]
|
||||
Reference in New Issue
Block a user