Files

283 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)