Files
eth_hedge_sim/backend/app/main.py
T
dekun 4585dba3c3 Add 永期半自动: human arm, machine open/close, then stop.
Settings toggle, Plan panel, Fleet monitor, exit locks and armed TOCTOU gates; docs and dual audits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 11:20:11 +08:00

236 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
app_settings = get_settings()
if not app_settings.is_sim:
secret = (app_settings.auth_secret or "").strip()
if not secret or secret == "change-me-eth-hedge-sim-secret":
raise RuntimeError(
"LIVE 拒绝启动:请在 .env 设置非默认 AUTH_SECRET(勿用 change-me-eth-hedge-sim-secret"
)
settings = load_runtime_settings()
engine = StrategyEngine()
set_engine(engine)
# 半自动:无仓时清陈旧授权,须首页重新「授权开下一单」
try:
from .strategy.semi_auto import clear_armed_if_flat
clear_armed_if_flat(
db, has_open_position=engine.matcher.has_open_position()
)
except Exception:
logger.exception("clear stale semi_armed on startup failed")
# 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")
try:
from .live import get_executor
from .live.reconcile import log_exchange_db_mismatch
log_exchange_db_mismatch(get_executor(db))
except Exception:
logger.exception("LIVE startup reconcile log failed")
engine.ensure_loop()
from .backup import start_auto_backup_task
start_auto_backup_task()
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
from .backup import stop_auto_backup_task
await stop_auto_backup_task()
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")
@app.get("/fleet-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)