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:
@@ -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)
|
||||
Reference in New Issue
Block a user