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 from .models.db import Database, set_db from .strategy import StrategyEngine, set_engine 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() repo_root = here.parents[2] return repo_root / "frontend" / "dist" @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() db = Database() set_db(db) engine = StrategyEngine() set_engine(engine) gw = MarketGateway(settings) set_gateway(gw) try: await gw.start() logger.info("market gateway started (SIM)") except Exception: logger.exception("market gateway failed to start") yield await engine.pause() if engine._task and not engine._task.done(): engine._task.cancel() try: await engine._task except Exception: pass await gw.stop() set_gateway(None) set_engine(None) db.close() set_db(None) app = FastAPI( title="eth_hedge_sim", version="0.3.0", description="ETH 自动对冲模拟盘 P1/P2", 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 from .strategy import get_engine settings = get_settings() gw = get_gateway() snap = gw.snapshot() try: st = get_engine().state() except Exception: st = None 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, "strategy": { "running": st.get("running") if st else None, "phase": st.get("phase") if st else None, "rounds_done": st.get("rounds_done") if st else None, }, } _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)