"""v2 数据接口路由:同花顺官方金融数据 API(/api/v2) 保留现有 /api/* 为 v1(腾讯/东财/新浪等抓取源),本模块提供独立 v2。 数据源:https://fuyao.aicubes.cn(同花顺官方),密钥仅后端持有。 接口返回统一使用现有 v1 的 data 信封风格:{"data": ..., "count": ...}, 上游错误转 HTTPException,密钥永不出现在响应中。 """ 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("/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)