283 lines
13 KiB
Python
283 lines
13 KiB
Python
"""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) |