from __future__ import annotations
from contextlib import asynccontextmanager
import asyncio
from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastmcp import FastMCP
from .models import AccountView
from . import repo
from .services_bitstamp import fetch_account_info as fetch_remote_account_info, fetch_account_fees as fetch_remote_account_fees
from .bitstamp_metadata import METADATA_REFRESH_SECONDS, refresh_metadata, list_markets as bitstamp_list_markets, list_currencies as bitstamp_list_currencies
from .bitstamp_fx import FX_REFRESH_SECONDS, refresh_eur_usd
from .bitstamp_private_ws import private_ws_main
from .services_orders import place_order as service_place_order, query_order as service_query_order, cancel_order as service_cancel_order, get_open_orders as service_get_open_orders, cancel_all_orders as service_cancel_all_orders
from .storage import init_db
mcp = FastMCP("exec-mcp")
async def _metadata_refresh_loop() -> None:
while True:
try:
refresh_metadata()
except Exception:
pass
await asyncio.sleep(METADATA_REFRESH_SECONDS)
async def _fx_refresh_loop() -> None:
while True:
try:
refresh_eur_usd()
except Exception:
pass
await asyncio.sleep(FX_REFRESH_SECONDS)
@asynccontextmanager
async def lifespan(_: FastAPI):
init_db()
try:
refresh_metadata()
except Exception:
pass
stop_event = asyncio.Event()
metadata_task = asyncio.create_task(_metadata_refresh_loop())
fx_task = asyncio.create_task(_fx_refresh_loop())
private_ws_task = asyncio.create_task(private_ws_main(stop_event))
try:
yield
finally:
stop_event.set()
metadata_task.cancel()
fx_task.cancel()
private_ws_task.cancel()
app = FastAPI(title="exec-mcp", lifespan=lifespan)
app.mount("/mcp", mcp.http_app(path="/sse", transport="sse"))
SUPPORTED_VENUES = {"bitstamp"}
REPORT_ORDER_STATUSES = {"all", "open", "cancelled", "finished"}
def _normalize_report_status(status: str) -> str:
value = str(status or "").strip().lower()
if value not in REPORT_ORDER_STATUSES:
raise HTTPException(status_code=400, detail="status must be one of all, open, cancelled, finished")
return value
def _normalize_report_limit(limit: int) -> int:
try:
value = int(limit)
except Exception as exc:
raise HTTPException(status_code=400, detail="limit must be an integer") from exc
if value < 1:
raise HTTPException(status_code=400, detail="limit must be at least 1")
if value > 500:
raise HTTPException(status_code=400, detail="limit must be at most 500")
return value
@app.get("/", response_class=HTMLResponse)
def http_root() -> str:
return """
exec-mcp
Execution layer for Trader27, with a dashboard for account management.