"""同花顺金融数据 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 {}