Files
auv/backend/services/fuyao_client.py
T

312 lines
12 KiB
Python
Raw 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.
"""同花顺金融数据 API 客户端(v2 数据源,接口前缀 /api/v2)
本模块是基于官方 Python SDK(vendor 在 backend/sdk/)的薄封装,
对外暴露与 /api/v2 路由匹配的 async 函数,并统一返回 list/dict。
- SDK 提供:历史K线 >10 年自动切片、重试、参数校验、标的缓存。
- SDK 内部用同步 requests,这里用 asyncio.to_thread 桥接,避免阻塞事件循环。
- API Key 由 SDK 的 credentials resolver 读取(兼容 .env 的 fuyao_apikey),
**绝不写入代码、日志、错误信息或 git。**
官方 SDK 来源:https://github.com/HiThink-Tech/Financial-API (MIT)
"""
import asyncio
import time
from typing import Any, Iterable, Optional
from sdk import fuyao_client as _sdk
from sdk.fuyao_client import FuyaoApiError # 复用官方错误类型
# 兼容旧路由引用名(routes/fuyao.py 用 FuyaoError)
FuyaoError = FuyaoApiError
# ---------------------------------------------------------------
# 拼音首字母检索(同花顺接口不支持拼音,本地补齐)
# ---------------------------------------------------------------
# 全市场 A 股标的列表缓存(用于拼音首字母匹配)
_ticker_cache: dict = {"data": None, "at": 0.0}
_TICKER_CACHE_TTL = 12 * 3600 # 12 小时
async def _ensure_a_share_tickers() -> list[dict]:
"""拉取全市场 A 股标的列表并缓存(幂等,TTL 内复用)"""
now = time.time()
if _ticker_cache["data"] is not None and now - _ticker_cache["at"] < _TICKER_CACHE_TTL:
return _ticker_cache["data"]
items = await _run(_sdk.tickers_list, asset_type="a-share", limit=10000, offset=0)
data = _item_list(items)
_ticker_cache["data"] = data
_ticker_cache["at"] = now
return data
def _pinyin_initials(name: str) -> str:
"""中文名 → 拼音首字母(如 深科技→skj,TCL科技→tkj)"""
try:
from pypinyin import lazy_pinyin
except ImportError:
return ""
return "".join(
w[0] for w in lazy_pinyin(name)
if w and w[0].isalpha()
).lower()
def _match_by_pinyin(query: str, tickers: list[dict], limit: int) -> list[dict]:
"""按拼音首字母匹配:精确=前缀优先,包含匹配次之"""
q = query.lower()
exact: list[dict] = []
prefix: list[dict] = []
contains: list[dict] = []
for t in tickers:
name = t.get("name") or ""
if not name:
continue
initials = _pinyin_initials(name)
if not initials:
continue
if initials == q:
exact.append(t)
elif initials.startswith(q):
prefix.append(t)
elif q in initials:
contains.append(t)
return (exact + prefix + contains)[:limit]
def _run(fn, *args, **kwargs):
"""同步 SDK 调用桥接到 async"""
return asyncio.to_thread(fn, *args, **kwargs)
def _item_list(data, key="item"):
"""把 SDK 返回的 dict 信封或 list 统一成 item list"""
if isinstance(data, dict):
return data.get(key, []) or []
if isinstance(data, list):
return data or []
return []
# ---------------------------------------------------------------
# 基础数据:标的检索 / 标的列表
# ---------------------------------------------------------------
async def ticker_search(q: str, exchange: Optional[str] = None,
asset_type: Optional[str] = None, limit: int = 10) -> list[dict]:
items = await _run(_sdk.tickers_search, q, exchange=exchange,
asset_type=asset_type, limit=limit)
result = _item_list(items)
# 拼音首字母兜底:同花顺无结果 且 输入是纯字母(如 skj)时,本地按拼音首字母匹配
if not result and q and q.strip().isalpha():
try:
tickers = await _ensure_a_share_tickers()
result = await asyncio.to_thread(_match_by_pinyin, q, tickers, limit)
except Exception:
pass # 拼音匹配失败不影响主流程
return result
async def ticker_list(asset_type: Optional[str] = None, limit: int = 100,
offset: int = 0) -> list[dict]:
items = await _run(_sdk.tickers_list, asset_type=asset_type or "a-share",
limit=limit, offset=offset)
return _item_list(items)
# ---------------------------------------------------------------
# A股行情 / 日历 / 竞价
# ---------------------------------------------------------------
async def prices_snapshot(thscodes: str) -> list[dict]:
"""行情快照(单只/多只,逗号分隔)"""
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
items = await _run(_sdk.prices_snapshot, codes)
return _item_list(items)
async def prices_historical(thscode: str, start_ms: int, end_ms: int,
adjust: str = "forward", offset: int = 0) -> list[dict]:
"""历史日K(毫秒时间戳)。SDK 自动处理 >10 年窗口切片与去重排序。"""
return await _run(_sdk.prices_historical, thscode, start_ms, end_ms,
interval="1d", adjust=adjust)
async def corp_actions_adjustment_factors(thscode: str, from_date: str = None,
to_date: str = None) -> dict:
data = await _run(_sdk.corp_actions_adjustment_factors, thscode,
**({"from": from_date} if from_date else {}),
**({"to": to_date} if to_date else {}))
return data or {}
async def calendar_trading_days() -> list[dict]:
return await _run(_sdk.calendar_trading_days)
async def auction_snapshot(thscodes: str, stage: str = "final") -> dict:
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
data = await _run(_sdk.a_share_auction_snapshot, codes, stage=stage)
return data or {}
async def auction_short_term_benchmark(date: Optional[str] = None) -> dict:
data = await _run(_sdk.a_share_auction_short_term_benchmark, date=date)
return data or {}
# ---------------------------------------------------------------
# A股财务 / 估值
# ---------------------------------------------------------------
async def financials(statement: str, thscode: str, period: str = "annual",
limit: int = 6) -> list[dict]:
fn_map = {
"income-statements": _sdk.financials_income_statements,
"balance-sheets": _sdk.financials_balance_sheets,
"cash-flow-statements": _sdk.financials_cash_flow_statements,
}
fn = fn_map[statement]
items = await _run(fn, thscode, period=period, limit=limit)
return _item_list(items)
async def financial_indicators(thscode: str, report: str) -> list[dict]:
data = await _run(_sdk.financials_indicators, thscode, report)
return _item_list(data)
async def valuations_snapshot(thscodes: str) -> list[dict]:
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
data = await _run(_sdk.a_share_valuations_snapshot, codes)
return _item_list(data)
# ---------------------------------------------------------------
# 指数 / 板块
# ---------------------------------------------------------------
async def index_catalog(tag: str = "industry") -> list[dict]:
items = await _run(_sdk.index_catalog_ths_index_list, tag=tag)
return _item_list(items)
async def index_constituents(thscode: str) -> list[dict]:
items = await _run(_sdk.index_constituents_ths_stock_list, thscode)
return _item_list(items)
async def index_prices_snapshot(thscodes: str) -> list[dict]:
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
items = await _run(_sdk.index_prices_snapshot, codes)
return _item_list(items)
async def index_prices_historical(thscode: str, start_ms: int, end_ms: int) -> list[dict]:
items = await _run(_sdk.index_prices_historical, thscode, start_ms, end_ms)
return _item_list(items)
# ---------------------------------------------------------------
# 特殊数据(涨停/跌停池)
# ---------------------------------------------------------------
async def limit_up_pool(date_ms=None, page=1, size=50,
sort_field="seal_money", sort_dir="desc") -> dict:
data = await _run(_sdk.special_data_limit_up_pool, date_ms=date_ms,
page=page, size=size, sort_field=sort_field, sort_dir=sort_dir)
return data or {}
async def limit_down_pool(date_ms=None, page=1, size=50,
sort_field="last_limit_time", sort_dir="desc") -> dict:
data = await _run(_sdk.special_data_limit_down_pool, date_ms=date_ms,
page=page, size=size, sort_field=sort_field, sort_dir=sort_dir)
return data or {}
# ---------------------------------------------------------------
# 全市场行情快照
# ---------------------------------------------------------------
async def prices_snapshot_all(limit: int = 5000) -> list[dict]:
"""拉取全市场 A 股行情快照(自动分页)"""
items = await _run(_sdk.prices_snapshot, None, fetch_all_market=True, limit=limit)
return _item_list(items)
# ---------------------------------------------------------------
# 特殊数据(热门股/龙虎榜/异动/连板/飙升)
# ---------------------------------------------------------------
async def hot_stock_list(period: str = "day") -> dict:
data = await _run(_sdk.special_data_hot_stock_list, period=period)
return data or {}
async def dragon_tiger_list(board_type: str = "all", date: str = None) -> dict:
data = await _run(_sdk.special_data_dragon_tiger_list, board_type=board_type, date=date)
return data or {}
async def anomaly_analysis_list(tag_codes=None) -> dict:
data = await _run(_sdk.special_data_anomaly_analysis_list, tag_codes=tag_codes)
return data or {}
async def skyrocket_list(period: str = "day") -> dict:
data = await _run(_sdk.special_data_skyrocket_list, period=period)
return data or {}
async def limit_up_ladder() -> dict:
data = await _run(_sdk.special_data_limit_up_ladder)
return data or {}
# ---------------------------------------------------------------
# 集合竞价 / 估值 / 炸板 / 概念板块
# ---------------------------------------------------------------
async def auction_snapshot(thscodes: str, stage: str = "final") -> dict:
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
data = await _run(_sdk.a_share_auction_snapshot, codes, stage=stage)
return data or {}
async def auction_short_term_benchmark(date: str = None) -> dict:
data = await _run(_sdk.a_share_auction_short_term_benchmark, date=date)
return data or {}
async def valuations_snapshot(thscodes: str) -> dict:
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
data = await _run(_sdk.a_share_valuations_snapshot, codes)
return data or {}
async def limit_break_pool(date_ms=None, page=1, size=50,
sort_field="price_change_ratio_pct", sort_dir="desc") -> dict:
data = await _run(_sdk.special_data_limit_break_pool, date_ms=date_ms,
page=page, size=size, sort_field=sort_field, sort_dir=sort_dir)
return data or {}
async def hot_stock_list_history(date: str) -> dict:
data = await _run(_sdk.special_data_hot_stock_list_history, date)
return data or {}
async def hot_stock_rank_trend(thscode: str, start_date: str, end_date: str) -> dict:
data = await _run(_sdk.special_data_hot_stock_rank_trend, thscode, start_date, end_date)
return data or {}
async def anomaly_analysis_stock(thscodes: str) -> dict:
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
data = await _run(_sdk.special_data_anomaly_analysis_stock, codes)
return data or {}