3e492eb46c
Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""日/周/月统计 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.expiry_filter import filter_rows_min_hours_to_expiry
|
||
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
|
||
|
||
|
||
def _validate_min_hours(min_hours: float) -> float:
|
||
# 8–48,步进 2(与前端一致)
|
||
h = float(min_hours)
|
||
if h < 8 or h > 48 or abs(h - round(h)) > 1e-9 or int(round(h)) % 2 != 0:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="min_hours must be even integer in [8, 48]",
|
||
)
|
||
return float(int(round(h)))
|
||
|
||
|
||
@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),
|
||
min_hours: float = Query(
|
||
default=12,
|
||
ge=8,
|
||
le=48,
|
||
description="采样时距离到期 ≥ 该小时数才计入",
|
||
),
|
||
) -> dict:
|
||
s = get_settings()
|
||
mh = _validate_min_hours(min_hours)
|
||
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,
|
||
)
|
||
rows = filter_rows_min_hours_to_expiry(rows, min_hours=mh)
|
||
payload = leverage_stats_payload(
|
||
rows,
|
||
range_info=info,
|
||
bucket_minutes=bucket_minutes,
|
||
min_leverage=float(s.min_option_leverage),
|
||
side=side,
|
||
)
|
||
payload["min_hours"] = mh
|
||
return payload
|
||
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),
|
||
min_hours: float = Query(
|
||
default=12,
|
||
ge=8,
|
||
le=48,
|
||
description="采样时距离到期 ≥ 该小时数才计入",
|
||
),
|
||
) -> dict:
|
||
s = get_settings()
|
||
mh = _validate_min_hours(min_hours)
|
||
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,
|
||
)
|
||
rows = filter_rows_min_hours_to_expiry(rows, min_hours=mh)
|
||
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)
|
||
payload = move_points_stats_payload(
|
||
rows,
|
||
settlements,
|
||
range_info=info,
|
||
bucket_minutes=bucket_minutes,
|
||
side=side,
|
||
)
|
||
payload["min_hours"] = mh
|
||
return payload
|
||
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),
|
||
min_hours: float = Query(default=12, ge=8, le=48),
|
||
) -> dict:
|
||
mh = _validate_min_hours(min_hours)
|
||
lev = leverage_stats(
|
||
range=range, date=date, side=side, bucket_minutes=bucket_minutes, min_hours=mh
|
||
)
|
||
mov = move_points_stats(
|
||
range=range, date=date, side=side, bucket_minutes=bucket_minutes, min_hours=mh
|
||
)
|
||
return {
|
||
"range": range,
|
||
"date": lev.get("date"),
|
||
"side": side,
|
||
"bucket_minutes": bucket_minutes,
|
||
"min_hours": mh,
|
||
"leverage": lev,
|
||
"move_points": mov,
|
||
}
|