Files
crypto_monitor/manual_trading_hub/amp_stats_routes.py
T
dekun 789ab43dbe Add long-straddle premium overlay to hub amp stats.
Configurable bilateral premium with exceed counts/ratios and settlement PnL for buying volatility.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 02:33:23 +08:00

175 lines
6.2 KiB
Python

"""中控振幅统计 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,
rows_page,
straddle_long_stats,
summarize_rows,
)
class ComputeBody(BaseModel):
symbol: str = "eth"
start_hour: int = 16
period: str = "2m"
custom_days: Optional[int] = None
straddle_premium: Optional[float] = None
page: int = 1
page_size: int = 20
class SaveBody(BaseModel):
result: dict[str, Any] = Field(default_factory=dict)
class StraddleBody(BaseModel):
"""已有日表上按权利金重算买跨对照(不拉 K 线)."""
rows: list[dict[str, Any]] = Field(default_factory=list)
straddle_premium: Optional[float] = None
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": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
"straddle_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,
straddle_premium=body.straddle_premium,
)
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("/straddle")
def api_straddle(body: StraddleBody):
try:
prem = normalize_straddle_premium(body.straddle_premium)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if prem is None:
return {"ok": True, "straddle": None}
try:
st = straddle_long_stats(body.rows or [], prem)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "straddle": st}
@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),
straddle_premium: Optional[float] = Query(default=None),
):
if (history_id or "").strip():
item = get_history(history_id.strip())
if not item:
raise HTTPException(status_code=404, detail="历史不存在")
payload = dict(item)
try:
prem = normalize_straddle_premium(straddle_premium)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if prem is not None:
summary = summarize_rows(payload.get("rows") or [], straddle_premium=prem)
payload["summary"] = summary
payload["straddle_premium"] = prem
else:
try:
payload = compute_amp_stats(
symbol=symbol,
start_hour=start_hour,
period=period,
custom_days=custom_days,
straddle_premium=straddle_premium,
)
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