"""中控振幅统计 API.""" from __future__ import annotations from typing import Any, Optional from fastapi import APIRouter, HTTPException, Query from fastapi.responses import Response from pydantic import BaseModel, Field from amp_stats_store import delete_history, get_history, list_history, save_history from lib.hub.amp_stats_lib import ( build_export_csv, compute_amp_stats, export_filename, rows_page, ) class ComputeBody(BaseModel): symbol: str = "eth" start_hour: int = 16 period: str = "2m" custom_days: Optional[int] = None page: int = 1 page_size: int = 20 class SaveBody(BaseModel): result: dict[str, Any] = Field(default_factory=dict) def create_amp_stats_router() -> APIRouter: router = APIRouter(prefix="/api/amp-stats", tags=["amp-stats"]) @router.get("/meta") def api_meta(): return { "ok": True, "exchange": "okx", "symbols": [ {"key": "eth", "label": "ETH"}, {"key": "btc", "label": "BTC"}, ], "end_hour": 16, "start_hours": list(range(24)), "periods": [ {"key": "1m", "label": "1个月"}, {"key": "2m", "label": "2个月"}, {"key": "3m", "label": "3个月"}, {"key": "6m", "label": "半年"}, {"key": "1y", "label": "1年"}, {"key": "custom", "label": "自定义"}, ], "default_period": "2m", "timeframe": "1H", "metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)", } @router.post("/compute") def api_compute(body: ComputeBody): try: result = compute_amp_stats( symbol=body.symbol, start_hour=body.start_hour, period=body.period, custom_days=body.custom_days, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc page = rows_page(result.get("rows") or [], page=body.page, page_size=body.page_size) return { "ok": True, "result": result, "page": page, } @router.get("/history") def api_history(symbol: str = "", limit: int = 50): return {"ok": True, "items": list_history(symbol=symbol, limit=limit)} @router.post("/history") def api_history_save(body: SaveBody): payload = body.result if isinstance(body.result, dict) else {} if not payload.get("rows") and not payload.get("summary"): raise HTTPException(status_code=400, detail="无可保存的结果") item = save_history(payload) return {"ok": True, "item": item} @router.get("/history/{item_id}") def api_history_detail(item_id: str): item = get_history(item_id) if not item: raise HTTPException(status_code=404, detail="历史不存在") return {"ok": True, "item": item} @router.delete("/history/{item_id}") def api_history_delete(item_id: str): if not delete_history(item_id): raise HTTPException(status_code=404, detail="历史不存在") return {"ok": True} @router.get("/export") def api_export( history_id: str = Query(default=""), symbol: str = Query(default="eth"), start_hour: int = Query(default=16), period: str = Query(default="2m"), custom_days: Optional[int] = Query(default=None), ): if (history_id or "").strip(): item = get_history(history_id.strip()) if not item: raise HTTPException(status_code=404, detail="历史不存在") payload = item else: try: payload = compute_amp_stats( symbol=symbol, start_hour=start_hour, period=period, custom_days=custom_days, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc csv_text = build_export_csv(payload) name = export_filename(payload) return Response( content=csv_text.encode("utf-8"), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="{name}"'}, ) return router