"""中控振幅统计 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, normalize_straddle_premium, normalize_take_profit, normalize_weekend_filter, reframe_amp_stats, rows_page, ) class PerpHedgeBody(BaseModel): spot: Optional[float] = None target_profit_u: Optional[float] = None perp_leverage: Optional[float] = None option_leverage: Optional[float] = None ratio_perp: float = 1.0 ratio_opt: float = 2.0 ct_mult: float = 0.01 class ComputeBody(BaseModel): symbol: str = "eth" start_hour: int = 16 period: str = "2m" custom_days: Optional[int] = None straddle_premium: Optional[float] = None take_profit: Optional[float] = None weekend_filter: str = "all" perp_hedge: Optional[PerpHedgeBody] = None page: int = 1 page_size: int = 20 class SaveBody(BaseModel): result: dict[str, Any] = Field(default_factory=dict) class ReframeBody(BaseModel): """已有日表上改周末/权利金/止盈/永期参数(不拉 K 线).""" rows_all: list[dict[str, Any]] = Field(default_factory=list) symbol: str = "eth" start_hour: int = 16 period: str = "2m" sample_days: int = 60 straddle_premium: Optional[float] = None take_profit: Optional[float] = None weekend_filter: str = "all" perp_hedge: Optional[PerpHedgeBody] = None price_source: str = "" inst_id: str = "" page: int = 1 page_size: int = 20 def _hedge_dict(body_hedge: Optional[PerpHedgeBody]) -> Optional[dict[str, Any]]: if body_hedge is None: return None return body_hedge.model_dump() 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": "自定义"}, ], "weekend_filters": [ {"key": "all", "label": "全部"}, {"key": "exclude", "label": "排除周末"}, {"key": "only", "label": "仅周末"}, ], "default_period": "2m", "default_weekend_filter": "all", "timeframe": "1H", "metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)", "straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金", "perp_hedge_note": "永期对冲:永续多1币+买期权;比例默认1:2;达标与组合盈亏见文档", } @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, straddle_premium=body.straddle_premium, take_profit=body.take_profit, weekend_filter=body.weekend_filter, perp_hedge=_hedge_dict(body.perp_hedge), ) 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.post("/reframe") def api_reframe(body: ReframeBody): rows_all = body.rows_all or [] if not rows_all: raise HTTPException(status_code=400, detail="无日表可重算") try: result = reframe_amp_stats( rows_all=rows_all, symbol=body.symbol, start_hour=body.start_hour, period=body.period, sample_days=body.sample_days, straddle_premium=body.straddle_premium, take_profit=body.take_profit, weekend_filter=body.weekend_filter, perp_hedge=_hedge_dict(body.perp_hedge), price_source=body.price_source, inst_id=body.inst_id, ) except ValueError as exc: raise HTTPException(status_code=400, 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("rows_all") 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), straddle_premium: Optional[float] = Query(default=None), take_profit: Optional[float] = Query(default=None), weekend_filter: str = Query(default="all"), hedge_spot: Optional[float] = Query(default=None), hedge_target: Optional[float] = Query(default=None), hedge_perp_lev: Optional[float] = Query(default=None), hedge_opt_lev: Optional[float] = Query(default=None), hedge_ratio_perp: float = Query(default=1.0), hedge_ratio_opt: float = Query(default=2.0), hedge_ct_mult: float = Query(default=0.01), ): hedge_q = { "spot": hedge_spot, "target_profit_u": hedge_target, "perp_leverage": hedge_perp_lev, "option_leverage": hedge_opt_lev, "ratio_perp": hedge_ratio_perp, "ratio_opt": hedge_ratio_opt, "ct_mult": hedge_ct_mult, } if (history_id or "").strip(): item = get_history(history_id.strip()) if not item: raise HTTPException(status_code=404, detail="历史不存在") rows_all = item.get("rows_all") or item.get("rows") or [] item_hedge = item.get("perp_hedge") if isinstance(item.get("perp_hedge"), dict) else None use_hedge = hedge_q if hedge_spot is not None else item_hedge try: payload = reframe_amp_stats( rows_all=rows_all, symbol=item.get("symbol") or symbol, start_hour=int(item.get("start_hour") if item.get("start_hour") is not None else start_hour), period=str(item.get("period") or period), sample_days=int(item.get("sample_days_requested") or 60), straddle_premium=straddle_premium if straddle_premium is not None else item.get("straddle_premium"), take_profit=take_profit if take_profit is not None else item.get("take_profit"), weekend_filter=weekend_filter or item.get("weekend_filter") or "all", perp_hedge=use_hedge, price_source=str(item.get("price_source") or ""), inst_id=str(item.get("inst_id") or ""), missing=item.get("missing_days") or [], ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc else: try: # validate enums early normalize_weekend_filter(weekend_filter) normalize_straddle_premium(straddle_premium) normalize_take_profit(take_profit) payload = compute_amp_stats( symbol=symbol, start_hour=start_hour, period=period, custom_days=custom_days, straddle_premium=straddle_premium, take_profit=take_profit, weekend_filter=weekend_filter, perp_hedge=hedge_q, ) 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