"""v2 数据接口路由:同花顺官方金融数据 API(/api/v2) 保留现有 /api/* 为 v1(腾讯/东财/新浪等抓取源),本模块提供独立 v2。 数据源:https://fuyao.aicubes.cn(同花顺官方),密钥仅后端持有。 接口返回统一使用现有 v1 的 data 信封风格:{"data": ..., "count": ...}, 上游错误转 HTTPException,密钥永不出现在响应中。 """ from datetime import datetime from fastapi import APIRouter, Query, HTTPException from fastapi.responses import JSONResponse from services import fuyao_client router = APIRouter() # 上游反代/CDN 可能按 path 缓存,显式禁止缓存 _NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"} async def _guard(awaitable): """调用 fuyao_client,把上游/校验错误统一转 HTTPException""" try: return await awaitable except fuyao_client.FuyaoError as e: # 上游业务错误,message 来自上游,不含密钥 raise HTTPException(status_code=502, detail=f"同花顺API: {e}") except ValueError as e: # SDK 参数校验错误(如 thscode 格式) raise HTTPException(status_code=400, detail=f"参数错误: {e}") # --------------------------------------------------------------- # 基础数据 # --------------------------------------------------------------- @router.get("/meta/tickers/search", summary="v2 标的检索") async def v2_ticker_search( q: str = Query(..., description="代码/名称/拼音关键词"), exchange: str = Query(None, description="交易所 SH/SZ/BJ"), asset_type: str = Query(None, description="资产类型 a-share 等"), limit: int = Query(10, ge=1, le=50), ): items = await _guard(fuyao_client.ticker_search(q, exchange, asset_type, limit)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/meta/tickers/list", summary="v2 标的列表(分页)") async def v2_ticker_list( asset_type: str = Query(None, description="资产类型"), limit: int = Query(100, ge=1, le=10000), offset: int = Query(0, ge=0), ): items = await _guard(fuyao_client.ticker_list(asset_type, limit, offset)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) # --------------------------------------------------------------- # A股行情 / 日历 / 竞价 # --------------------------------------------------------------- @router.get("/prices/snapshot", summary="v2 行情快照(单只/多只)") async def v2_prices_snapshot(thscodes: str = Query(..., description="逗号分隔的 thscode,如 600519.SH,000001.SZ")): items = await _guard(fuyao_client.prices_snapshot(thscodes)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/prices/historical", summary="v2 历史日K(窗口≤10年)") async def v2_prices_historical( thscode: str = Query(..., description="标的 thscode,单只"), start: int = Query(..., description="起始时间,毫秒 Unix 时间戳"), end: int = Query(..., description="结束时间,毫秒 Unix 时间戳"), adjust: str = Query("forward", description="复权 none/forward/backward"), ): try: items = await fuyao_client.prices_historical(thscode, start, end, adjust) except fuyao_client.FuyaoError as e: raise HTTPException(status_code=502, detail=f"同花顺API: {e}") except ValueError as e: raise HTTPException(status_code=400, detail=f"参数错误: {e}") return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/corporate-actions/adjustment-factors", summary="v2 复权因子事件流") async def v2_adjustment_factors( thscode: str = Query(..., description="标的 thscode,单只"), from_date: str = Query(None, description="起始日 yyyy-MM-dd"), to_date: str = Query(None, description="截止日 yyyy-MM-dd"), ): data = await _guard(fuyao_client.corp_actions_adjustment_factors(thscode, from_date, to_date)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/calendar/trading-days", summary="v2 近一年交易日序列") async def v2_calendar(): items = await _guard(fuyao_client.calendar_trading_days()) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/auction/snapshot", summary="v2 集合竞价快照") async def v2_auction_snapshot( thscodes: str = Query(..., description="逗号分隔 thscode"), stage: str = Query("final", description="live 实时 / final 终态"), ): data = await _guard(fuyao_client.auction_snapshot(thscodes, stage)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/auction/short-term-benchmark", summary="v2 短线风向标竞价基准") async def v2_auction_benchmark(date: str = Query(None, description="日期 yyyy-MM-dd")): data = await _guard(fuyao_client.auction_short_term_benchmark(date)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) # --------------------------------------------------------------- # A股财务 / 估值 # --------------------------------------------------------------- @router.get("/financials/{statement}", summary="v2 三大财务报表") async def v2_financials( statement: str, thscode: str = Query(..., description="标的 thscode"), period: str = Query("annual", description="annual 年报 / quarterly 季报"), limit: int = Query(6, ge=1, le=20), ): if statement not in ("income-statements", "balance-sheets", "cash-flow-statements"): raise HTTPException(status_code=400, detail="statement 仅支持 income-statements/balance-sheets/cash-flow-statements") items = await _guard(fuyao_client.financials(statement, thscode, period, limit)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/financials/indicators", summary="v2 五类财务指标") async def v2_financial_indicators( thscode: str = Query(..., description="标的 thscode"), report: str = Query(..., description="报告期 yyyy-1 ~ yyyy-4"), ): items = await _guard(fuyao_client.financial_indicators(thscode, report)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/valuations/snapshot", summary="v2 估值快照") async def v2_valuations_snapshot(thscodes: str = Query(..., description="逗号分隔 thscode")): items = await _guard(fuyao_client.valuations_snapshot(thscodes)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) # --------------------------------------------------------------- # 指数 / 板块 # --------------------------------------------------------------- @router.get("/index/catalog", summary="v2 同花顺指数清单") async def v2_index_catalog(tag: str = Query("industry", description="cn_concept/region/tszs/industry")): items = await _guard(fuyao_client.index_catalog(tag)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/index/constituents", summary="v2 指数成分股") async def v2_index_constituents(thscode: str = Query(..., description="指数 thscode")): items = await _guard(fuyao_client.index_constituents(thscode)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/index/prices/snapshot", summary="v2 指数行情快照") async def v2_index_prices_snapshot(thscodes: str = Query(..., description="逗号分隔指数 thscode")): items = await _guard(fuyao_client.index_prices_snapshot(thscodes)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) @router.get("/index/prices/historical", summary="v2 指数历史K线") async def v2_index_prices_historical( thscode: str = Query(..., description="指数 thscode"), start: int = Query(..., description="起始时间,毫秒 Unix 时间戳"), end: int = Query(..., description="结束时间,毫秒 Unix 时间戳"), ): items = await _guard(fuyao_client.index_prices_historical(thscode, start, end)) return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS) # --------------------------------------------------------------- # 特殊数据(涨停/跌停/炸板/连板/热股/龙虎/异动/飙升) # --------------------------------------------------------------- @router.get("/special/limit-up-pool", summary="v2 涨停池") async def v2_limit_up_pool( date: str = Query(None, description="日期 yyyy-MM-dd"), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), sort_field: str = Query("seal_money", description="排序字段"), sort_dir: str = Query("desc", description="asc/desc"), ): date_ms = int(datetime.strptime(date, "%Y-%m-%d").timestamp() * 1000) if date else None data = await _guard(fuyao_client.limit_up_pool(date_ms, page, size, sort_field, sort_dir)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/limit-down-pool", summary="v2 跌停池") async def v2_limit_down_pool( date: str = Query(None, description="日期 yyyy-MM-dd"), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), sort_field: str = Query("last_limit_time"), sort_dir: str = Query("desc"), ): date_ms = int(datetime.strptime(date, "%Y-%m-%d").timestamp() * 1000) if date else None data = await _guard(fuyao_client.limit_down_pool(date_ms, page, size, sort_field, sort_dir)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/limit-break-pool", summary="v2 炸板池") async def v2_limit_break_pool( date: str = Query(None, description="日期 yyyy-MM-dd"), page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), sort_field: str = Query("price_change_ratio_pct"), sort_dir: str = Query("desc"), ): date_ms = int(datetime.strptime(date, "%Y-%m-%d").timestamp() * 1000) if date else None data = await _guard(fuyao_client.limit_break_pool(date_ms, page, size, sort_field, sort_dir)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/limit-up-ladder", summary="v2 连板梯队") async def v2_limit_up_ladder(): data = await _guard(fuyao_client.limit_up_ladder()) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/hot-stock-list", summary="v2 热股榜") async def v2_hot_stock_list( period: str = Query("day", description="day/week/month"), ): data = await _guard(fuyao_client.hot_stock_list(period)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/hot-stock-history", summary="v2 热股榜历史") async def v2_hot_stock_history(date: str = Query(..., description="日期 yyyy-MM-dd")): data = await _guard(fuyao_client.hot_stock_list_history(date)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/hot-stock-rank-trend", summary="v2 热股排名趋势") async def v2_hot_stock_rank_trend( thscode: str = Query(..., description="标的 thscode"), start_date: str = Query(..., description="起始日期 yyyy-MM-dd"), end_date: str = Query(..., description="结束日期 yyyy-MM-dd"), ): data = await _guard(fuyao_client.hot_stock_rank_trend(thscode, start_date, end_date)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/dragon-tiger-list", summary="v2 龙虎榜") async def v2_dragon_tiger_list( board_type: str = Query("all", description="all/sh/sz"), date: str = Query(None, description="日期 yyyy-MM-dd"), ): data = await _guard(fuyao_client.dragon_tiger_list(board_type, date)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/anomaly-list", summary="v2 异动分析列表") async def v2_anomaly_list( tag_codes: str = Query(None, description="逗号分隔异动类型编码"), ): codes = [c.strip() for c in tag_codes.split(",") if c.strip()] if tag_codes else None data = await _guard(fuyao_client.anomaly_analysis_list(codes)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/anomaly-stock", summary="v2 个股异动明细") async def v2_anomaly_stock( thscodes: str = Query(..., description="逗号分隔 thscode,最多50只"), ): data = await _guard(fuyao_client.anomaly_analysis_stock(thscodes)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS) @router.get("/special/skyrocket-list", summary="v2 飙升榜") async def v2_skyrocket_list( period: str = Query("day", description="day/week/month"), ): data = await _guard(fuyao_client.skyrocket_list(period)) return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS)