from __future__ import annotations import logging from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from .api import router as api_router from .config import get_settings from .market import MarketGateway, set_gateway logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s", ) logger = logging.getLogger("eth_hedge_sim") def resolve_frontend_dist() -> Path: here = Path(__file__).resolve() # backend/app/main.py -> repo root is parents[2] repo_root = here.parents[2] return repo_root / "frontend" / "dist" @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() if not settings.is_sim: logger.warning("MODE=%s — still read-only market in current phase", settings.mode) gw = MarketGateway(settings) set_gateway(gw) try: await gw.start() logger.info("market gateway started (SIM read-only)") except Exception: logger.exception("market gateway failed to start") yield await gw.stop() set_gateway(None) app = FastAPI( title="eth_hedge_sim", version="0.2.0", description="ETH 自动对冲模拟盘", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(api_router) @app.get("/health") async def health() -> dict: from .market import get_gateway settings = get_settings() gw = get_gateway() snap = gw.snapshot() return { "ok": True, "mode": settings.mode, "env_name": settings.env_name, "sim": settings.is_sim, "market_connected": snap.connected, "pair": snap.pair.to_dict() if snap.pair else None, "updated_at_ms": snap.updated_at_ms, } _DIST = resolve_frontend_dist() if (_DIST / "assets").is_dir(): app.mount("/assets", StaticFiles(directory=str(_DIST / "assets")), name="assets") @app.get("/") async def index_page(): index = _DIST / "index.html" if index.exists(): return FileResponse(index) return { "ok": True, "msg": "frontend not built yet; run: cd frontend && npm ci && npm run build", "health": "/health", } @app.get("/app/{full_path:path}") @app.get("/plan") @app.get("/trades") @app.get("/stats") @app.get("/settings") @app.get("/login") async def spa_pages(full_path: str = ""): index = _DIST / "index.html" if not index.exists(): raise HTTPException(status_code=404, detail="frontend not built") return FileResponse(index)