Files
market_intel/apps/api/routes/stats.py
T
2026-08-02 10:42:55 +08:00

153 lines
5.1 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.
"""日/周/月统计 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:
# 848,步进 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,
}