Files
PolyWeather/web/services/scan_api.py
T
2569718930@qq.com 2ee00f8016 MiMo AI 能力扩展:TAF解读、概率分布解读、异常检测、市场概览
AI 解读字段扩展:
    - 新增 taf_read_zh/en:解读机场预报中影响今日峰值窗口的变化
    - 新增 probability_read_zh/en:描述概率分布形态(最高桶、偏左/偏右)
    - stream max_tokens 900→1200 容纳新输出字段
    - 缓存 key 简化为 METAR原文+观测时间,大幅提升命中率
    - 兜底函数补全 TAF 和概率字段的确定性生成

    异常检测:
    - 纯数学计算,零 AI 延迟:实测温度 vs 全部模型预测上下限
    - 三级告警:breakout_above / breakout_below / deviation

    市场概览:
    - 新增 POST /api/scan/terminal/overview(MiMo 批量解读,缓存10分钟)
    - 前端 MarketOverviewBanner 可折叠横幅(顶栏与标签栏之间)
    - 移动端适配 640px/768px 断点,暗色/亮色双主题

    Scope-risk: MEDIUM — 170 测试通过,TypeScript 零错误,ruff 零告警
    Tested: python -m pytest -q (170 passed), npx tsc --noEmit (0 errors), ruff check .
2026-05-14 22:41:31 +08:00

131 lines
4.1 KiB
Python

"""Market scan API service functions."""
from __future__ import annotations
from typing import Any, Dict
from fastapi import HTTPException, Request
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import StreamingResponse
from web.services.market_overview_api import build_market_overview_payload
import web.routes as legacy_routes
def _boolish(value: Any) -> bool:
return str(value or "false").lower() in {"1", "true", "yes", "on"}
async def _json_body_or_empty(request: Request) -> Dict[str, Any]:
try:
body = await request.json()
except Exception:
body = {}
if not isinstance(body, dict):
raise HTTPException(status_code=400, detail="Invalid JSON body")
return body
def _extract_required_city(body: Dict[str, Any]) -> str:
city = str(body.get("city") or "").strip()
if not city:
raise HTTPException(status_code=400, detail="city is required")
return city
async def get_scan_terminal_payload(
request: Request,
*,
scan_mode: str = "tradable",
min_price: float = 0.05,
max_price: float = 0.95,
min_edge_pct: float = 2.0,
min_liquidity: float = 500.0,
high_liquidity_only: bool = False,
market_type: str = "maxtemp",
time_range: str = "today",
limit: int = 25,
force_refresh: bool = False,
) -> Dict[str, Any]:
legacy_routes._assert_entitlement(request)
filters = {
"scan_mode": scan_mode,
"min_price": min_price,
"max_price": max_price,
"min_edge_pct": min_edge_pct,
"min_liquidity": min_liquidity,
"high_liquidity_only": high_liquidity_only,
"market_type": market_type,
"time_range": time_range,
"limit": limit,
}
return await run_in_threadpool(
legacy_routes.build_scan_terminal_payload,
filters,
force_refresh=force_refresh,
)
async def get_scan_terminal_ai_payload(request: Request) -> Dict[str, Any]:
legacy_routes._assert_entitlement(request)
body = await _json_body_or_empty(request)
filters = body.get("filters") if isinstance(body.get("filters"), dict) else {}
snapshot_id = str(body.get("snapshot_id") or "").strip() or None
return await run_in_threadpool(
legacy_routes.build_scan_terminal_ai_payload,
filters,
snapshot_id=snapshot_id,
)
async def get_scan_city_ai_forecast_payload(request: Request) -> Dict[str, Any]:
legacy_routes._assert_entitlement(request)
body = await _json_body_or_empty(request)
city = _extract_required_city(body)
force_refresh = _boolish(body.get("force_refresh"))
locale = str(body.get("locale") or "zh-CN").strip()
return await run_in_threadpool(
legacy_routes.build_scan_city_ai_forecast_payload,
city,
force_refresh=force_refresh,
locale=locale,
)
async def get_scan_city_ai_stream_response(request: Request) -> StreamingResponse:
legacy_routes._assert_entitlement(request)
body = await _json_body_or_empty(request)
city = _extract_required_city(body)
force_refresh = _boolish(body.get("force_refresh"))
locale = str(body.get("locale") or "zh-CN").strip()
return StreamingResponse(
legacy_routes.stream_scan_city_ai_forecast_payload(
city,
force_refresh=force_refresh,
locale=locale,
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-store",
"X-Accel-Buffering": "no",
},
)
async def get_scan_terminal_overview_payload(request: Request) -> Dict[str, Any]:
try:
body = await request.json()
except Exception:
body = {}
if not isinstance(body, dict):
raise HTTPException(status_code=400, detail="Invalid JSON body")
rows = body.get("rows") if isinstance(body.get("rows"), list) else []
locale = str(body.get("locale") or "zh-CN").strip()
force_refresh = str(body.get("force_refresh") or "false").strip().lower() in {"1", "true", "yes"}
return await run_in_threadpool(
build_market_overview_payload,
rows,
locale=locale,
force_refresh=force_refresh,
)