f48ea5bbcc
Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
5.7 KiB
Python
204 lines
5.7 KiB
Python
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 .models.db import Database, set_db
|
||
from .strategy import StrategyEngine, set_engine
|
||
from .strategy.session import bootstrap_session
|
||
|
||
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):
|
||
db = Database()
|
||
set_db(db)
|
||
# 必须在 set_db 之后读 DB 覆盖,否则会按 .env 默认 okx 起会话,
|
||
# 而 health 显示 DB 里的 binance → 合约号/盘口错乱(期权一直 -/-)
|
||
from .exchange.runtime import load_runtime_settings
|
||
|
||
settings = load_runtime_settings()
|
||
engine = StrategyEngine()
|
||
set_engine(engine)
|
||
# LIVE:进程启动后不自动真下单,须人工点「启动」
|
||
if not get_settings().is_sim:
|
||
try:
|
||
db._conn.execute(
|
||
"UPDATE strategy_state SET running=0, phase=? WHERE id=1",
|
||
("paused",),
|
||
)
|
||
db._conn.commit()
|
||
logger.info("LIVE startup: forced strategy pause (manual start required)")
|
||
except Exception:
|
||
logger.exception("LIVE startup force-pause failed")
|
||
engine.ensure_loop()
|
||
|
||
session = bootstrap_session(settings)
|
||
try:
|
||
await session.start()
|
||
logger.info(
|
||
"exchange=%s strategy session started mode=%s",
|
||
settings.exchange,
|
||
"SIM" if get_settings().is_sim else "LIVE",
|
||
)
|
||
except Exception:
|
||
logger.exception("strategy session 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 session.stop()
|
||
from .exchange import set_exchange
|
||
from .strategy.session import set_session
|
||
|
||
set_session(None)
|
||
set_exchange(None)
|
||
set_engine(None)
|
||
db.close()
|
||
set_db(None)
|
||
|
||
|
||
app = FastAPI(
|
||
title="比特骆驼自动化对冲系统",
|
||
version="0.3.1",
|
||
description="比特骆驼自动化对冲系统(eth_hedge_sim)",
|
||
lifespan=lifespan,
|
||
docs_url=None if get_settings().disable_api_docs else "/docs",
|
||
redoc_url=None if get_settings().disable_api_docs else "/redoc",
|
||
openapi_url=None if get_settings().disable_api_docs else "/openapi.json",
|
||
)
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=[],
|
||
allow_credentials=False,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
app.include_router(api_router)
|
||
|
||
|
||
@app.get("/health")
|
||
async def health() -> dict:
|
||
from .strategy import get_engine
|
||
from .strategy.session import get_session
|
||
|
||
settings = get_settings()
|
||
try:
|
||
from .exchange.runtime import load_runtime_settings
|
||
|
||
rt = load_runtime_settings()
|
||
exchange_name = rt.exchange
|
||
except Exception:
|
||
exchange_name = settings.exchange
|
||
sess = get_session()
|
||
snap = sess.snapshot()
|
||
try:
|
||
st = get_engine().state()
|
||
except Exception:
|
||
st = None
|
||
return {
|
||
"ok": True,
|
||
"mode": settings.mode,
|
||
"env_name": settings.env_name,
|
||
"exchange": exchange_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")
|
||
|
||
_ICONS = _DIST / "icons"
|
||
if _ICONS.is_dir():
|
||
app.mount("/icons", StaticFiles(directory=str(_ICONS)), name="icons")
|
||
|
||
|
||
def _dist_file(name: str) -> Path:
|
||
return _DIST / name
|
||
|
||
|
||
@app.get("/manifest.webmanifest")
|
||
async def web_manifest():
|
||
path = _dist_file("manifest.webmanifest")
|
||
if not path.exists():
|
||
raise HTTPException(status_code=404, detail="manifest missing")
|
||
return FileResponse(
|
||
path,
|
||
media_type="application/manifest+json",
|
||
headers={"Cache-Control": "no-cache"},
|
||
)
|
||
|
||
|
||
@app.get("/sw.js")
|
||
async def service_worker():
|
||
path = _dist_file("sw.js")
|
||
if not path.exists():
|
||
raise HTTPException(status_code=404, detail="service worker missing")
|
||
return FileResponse(
|
||
path,
|
||
media_type="application/javascript",
|
||
headers={
|
||
"Cache-Control": "no-cache",
|
||
"Service-Worker-Allowed": "/",
|
||
},
|
||
)
|
||
|
||
|
||
@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)
|