"""日/周/月统计 API。""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from apps.api.auth import require_auth from apps.worker.settle import ensure_settlements_for_ymds from packages.config import get_settings from packages.db import Repository from packages.domain.aggregate import leverage_stats_payload, move_points_stats_payload from packages.domain.range import resolve_range router = APIRouter(prefix="/stats", tags=["stats"], dependencies=[Depends(require_auth)]) def _range_info(range_name: str, date: str | None) -> dict: s = get_settings() try: return resolve_range( range_name, date, month_mode=s.month_range_mode, ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) from e @router.get("/leverage") def leverage_stats( range: str = Query(default="day", pattern="^(day|week|month)$"), date: str | None = Query(default=None, description="锚点日 YYYY-MM-DD(上海)"), side: str = Query(default="both", pattern="^(C|P|both)$"), bucket_minutes: int = Query(default=60, ge=15, le=120), ) -> dict: s = get_settings() info = _range_info(range, date) repo = Repository(s.db_path) try: rows = repo.fetch_option_quotes( start_ms=info["start_ms"], end_ms=info["end_ms"], side=side, underlying=s.underlying, ) return leverage_stats_payload( rows, range_info=info, bucket_minutes=bucket_minutes, min_leverage=float(s.min_option_leverage), side=side, ) finally: repo.close() @router.get("/move_points") def move_points_stats( range: str = Query(default="day", pattern="^(day|week|month)$"), date: str | None = Query(default=None, description="锚点日 YYYY-MM-DD"), side: str = Query(default="both", pattern="^(C|P|both)$"), bucket_minutes: int = Query(default=60, ge=15, le=120), ) -> dict: s = get_settings() info = _range_info(range, date) repo = Repository(s.db_path) try: rows = repo.fetch_option_quotes( start_ms=info["start_ms"], end_ms=info["end_ms"], side=side, underlying=s.underlying, ) ymds = sorted({str(r.get("expiry_ymd")) for r in rows if r.get("expiry_ymd")}) # 懒回填:已到期但缺锚点时尽量补齐(本地指数优先,失败则跳过) try: ensure_settlements_for_ymds( repo, ymds, underlying=s.underlying, index_inst_id=s.index_inst_id, ) except Exception: # noqa: BLE001 — 回填失败不阻断统计 pass settlements = repo.list_settlements(ymds) return move_points_stats_payload( rows, settlements, range_info=info, bucket_minutes=bucket_minutes, side=side, ) finally: repo.close() @router.get("/ops-map") def ops_map( range: str = Query(default="day", pattern="^(day|week|month)$"), date: str | None = Query(default=None), side: str = Query(default="both", pattern="^(C|P|both)$"), bucket_minutes: int = Query(default=60, ge=15, le=120), ) -> dict: lev = leverage_stats(range=range, date=date, side=side, bucket_minutes=bucket_minutes) mov = move_points_stats(range=range, date=date, side=side, bucket_minutes=bucket_minutes) return { "range": range, "date": lev.get("date"), "side": side, "bucket_minutes": bucket_minutes, "leverage": lev, "move_points": mov, }