feat: 接入同花顺官方SDK,新增v2数据接口,股票详情K线改用v2(前复权日K)

- vendor 同花顺官方 SDK 到 backend/sdk(含K线>10年自动切片、重试、拼音首字母检索兜底)
- 新增 /api/v2 路由:行情/估值/财务/日历/指数/K线/标的检索
- 股票详情页K线改用 v2 同花顺接口(前复权日K+总手+按昨收涨跌幅)
- 密钥仅后端持有,响应/日志无泄露
- 新增 run_local.sh 本地直接拉起(不再依赖 docker)
This commit is contained in:
Sakurasan
2026-08-28 02:09:49 +08:00
parent 82006f7cf9
commit d5516b72d7
13 changed files with 2465 additions and 5 deletions
+165
View File
@@ -0,0 +1,165 @@
"""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)