Initial eth_hedge_sim: P0 market, auth UI, one-click deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-24 16:33:25 +08:00
commit e51f357b48
49 changed files with 4783 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
from .auth_routes import router as auth_router
from .market import router as market_router
router = APIRouter()
router.include_router(auth_router)
router.include_router(market_router)
+85
View File
@@ -0,0 +1,85 @@
"""简单 HMAC Token 鉴权(无 JWT 依赖)。"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import time
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, Field
from ..config import Settings, get_settings
_bearer = HTTPBearer(auto_error=False)
class LoginRequest(BaseModel):
username: str = Field(min_length=1)
password: str = Field(min_length=1)
class LoginResponse(BaseModel):
token: str
username: str
expires_in: int
env_name: str
mode: str
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def _b64url_decode(s: str) -> bytes:
pad = "=" * (-len(s) % 4)
return base64.urlsafe_b64decode(s + pad)
def issue_token(username: str, settings: Settings) -> tuple[str, int]:
exp = int(time.time()) + int(settings.auth_token_ttl_sec)
payload = {"u": username, "exp": exp}
raw = _b64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
sig = hmac.new(
settings.auth_secret.encode("utf-8"),
raw.encode("ascii"),
hashlib.sha256,
).hexdigest()
return f"{raw}.{sig}", settings.auth_token_ttl_sec
def verify_token(token: str, settings: Settings) -> str:
try:
raw, sig = token.rsplit(".", 1)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token") from e
expect = hmac.new(
settings.auth_secret.encode("utf-8"),
raw.encode("ascii"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expect, sig):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
try:
payload = json.loads(_b64url_decode(raw))
except Exception as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token") from e
if int(payload.get("exp") or 0) < int(time.time()):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="token expired")
username = str(payload.get("u") or "")
if not username:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
return username
def require_user(
creds: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
settings: Annotated[Settings, Depends(get_settings)],
) -> str:
if creds is None or not creds.credentials:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="login required")
return verify_token(creds.credentials, settings)
+36
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from ..config import Settings, get_settings
from .auth import LoginRequest, LoginResponse, issue_token, require_user
router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.post("/login", response_model=LoginResponse)
async def login(body: LoginRequest, settings: Annotated[Settings, Depends(get_settings)]) -> LoginResponse:
user_ok = body.username == settings.auth_username
pass_ok = body.password == settings.auth_password
if not (user_ok and pass_ok):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
token, ttl = issue_token(body.username, settings)
return LoginResponse(
token=token,
username=body.username,
expires_in=ttl,
env_name=settings.env_name,
mode=settings.mode,
)
@router.get("/me")
async def me(username: Annotated[str, Depends(require_user)], settings: Annotated[Settings, Depends(get_settings)]) -> dict:
return {
"username": username,
"env_name": settings.env_name,
"mode": settings.mode,
"sim": settings.is_sim,
}
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from ..market import get_gateway
from .auth import require_user
router = APIRouter(prefix="/api/market", tags=["market"])
@router.get("/snapshot")
async def market_snapshot(_user: Annotated[str, Depends(require_user)]) -> dict:
gw = get_gateway()
snap = gw.snapshot_dict()
if snap.get("pair") is None:
raise HTTPException(status_code=503, detail="market not aligned yet")
return snap
@router.post("/realign")
async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict:
"""手动重对齐次日到期 ATM 合约(运维/调试用)。"""
gw = get_gateway()
try:
pair = await gw.realign_async()
except Exception as e:
raise HTTPException(status_code=502, detail=str(e)) from e
return {"ok": True, "pair": pair.to_dict() if pair else None, "snapshot": gw.snapshot_dict()}