mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
fix(tdxquant): 区分除权空结果与服务端失败
在 SDK 丢失错误前检查原生除权响应,保留脱敏错误与重试语义;正常无事件返回空结果。事件或配价失败时中止同步并保留旧因子文件,避免被报告为成功的零行结果。补充 SDK 离线回放、数值与落盘回归测试。 Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -282,29 +283,41 @@ class TdxQuantClient:
|
||||
"""单标的除权事件, 每行 {Date 'YYYYMMDD', Type, Bonus, ShareBonus,
|
||||
AllotPrice, Allotment} — 金额/比例均为**每 10 股**口径(Bonus=10派X元)。
|
||||
"""
|
||||
start = f"{start_d:%Y-%m-%d}" if start_d else ""
|
||||
end = f"{end_d:%Y-%m-%d}" if end_d else ""
|
||||
try:
|
||||
out = self._tqs.get_divid_factors(code, start, end)
|
||||
except Exception as exc: # SDK 内部异常类型不可枚举
|
||||
raise TdxQuantError(f"get_divid_factors({code}): {exc}") from exc
|
||||
if isinstance(out, list):
|
||||
return out
|
||||
# 环境装有 pandas 时 SDK 返回 DataFrame, 归一为 list[dict]。
|
||||
# 注意 Date 会成为 DataFrame 索引(同 get_market_data 的日期索引惯例),
|
||||
# reset_index 后若列名漂移, 把首列恢复为 'Date'。
|
||||
to_dict = getattr(out, "to_dict", None)
|
||||
reset_index = getattr(out, "reset_index", None)
|
||||
if callable(to_dict) and callable(reset_index):
|
||||
start = f"{start_d:%Y-%m-%d} 00:00:00" if start_d else ""
|
||||
end = f"{end_d:%Y-%m-%d} 00:00:00" if end_d else ""
|
||||
# SDK get_divid_factors 会把 _clean 拒绝的服务端错误和正常无事件
|
||||
# 都变成空 DataFrame。与日K共用原生重试/错误检查,避免丢失原因;
|
||||
# 使用同一 get_cw_data 端点及 SDK 1.2.2 的四列顺序,单位不变。
|
||||
out = self._raw("get_cw_data", str(code).strip(), start, end)
|
||||
if not isinstance(out, dict):
|
||||
raise TdxQuantError(f"get_cw_data({code}) 返回异常结构: {type(out)!r}")
|
||||
if not out:
|
||||
return []
|
||||
dates, values, types = out.get("Date"), out.get("Value"), out.get("Type", [])
|
||||
if (not all(isinstance(v, list) for v in (dates, values, types))
|
||||
or len(dates) != len(values)):
|
||||
raise TdxQuantError(f"get_cw_data({code}) Date/Value/Type 列结构异常")
|
||||
records = []
|
||||
fields = ("Bonus", "AllotPrice", "ShareBonus", "Allotment")
|
||||
for i, item in enumerate(values):
|
||||
date_key = "".join(c for c in str(dates[i]).strip() if c.isdigit())[:8]
|
||||
try:
|
||||
records = reset_index().to_dict("records")
|
||||
if isinstance(records, list) and records:
|
||||
if "Date" not in records[0]:
|
||||
first_key = next(iter(records[0]))
|
||||
if first_key not in ("Type", "Bonus", "AllotPrice", "ShareBonus", "Allotment"):
|
||||
for row in records:
|
||||
row["Date"] = row.pop(first_key)
|
||||
return records
|
||||
except Exception: # noqa: BLE001 — 转换失败按结构异常处理
|
||||
pass
|
||||
raise TdxQuantError(f"get_divid_factors({code}) 返回异常结构: {type(out)!r}")
|
||||
event_date = datetime.strptime(date_key, "%Y%m%d").date() if len(date_key) == 8 else None
|
||||
except ValueError:
|
||||
event_date = None
|
||||
if event_date is None or (start_d and event_date < start_d) or (end_d and event_date > end_d):
|
||||
continue # SDK 同样过滤无效日期及服务端未裁剪的窗口外事件。
|
||||
if item is None:
|
||||
item = []
|
||||
if not isinstance(item, list):
|
||||
raise TdxQuantError(f"get_cw_data({code}) Value[{i}] 结构异常")
|
||||
padded = item + [0] * max(0, 4 - len(item))
|
||||
try:
|
||||
amounts = [float(v or 0) for v in padded[:4]]
|
||||
except (TypeError, ValueError, OverflowError) as exc:
|
||||
raise TdxQuantError(f"get_cw_data({code}) Value[{i}] 含无效数值") from exc
|
||||
if not all(math.isfinite(v) for v in amounts):
|
||||
raise TdxQuantError(f"get_cw_data({code}) Value[{i}] 含非有限数值")
|
||||
records.append({"Date": date_key, "Type": types[i] if i < len(types) else "",
|
||||
**dict(zip(fields, amounts, strict=True))})
|
||||
return records
|
||||
|
||||
@@ -586,7 +586,9 @@ class TdxQuantProvider:
|
||||
events = client.divid_factors(symbol, start_d, end_d + timedelta(days=_ADJ_FUTURE_DAYS))
|
||||
except TdxQuantError as e:
|
||||
logger.warning("TdxQuant 除权事件 %s 拉取失败: %s", symbol, e)
|
||||
events = []
|
||||
# 失败不能当作「该股无除权」;由任务层标记失败,整轮不写入
|
||||
# 不完整因子,也避免权限/额度异常后继续请求其余数千只。
|
||||
raise
|
||||
events = [ev for ev in events if _compact_date(ev.get("Date")) is not None]
|
||||
if events:
|
||||
events.sort(key=lambda ev: str(ev.get("Date")))
|
||||
@@ -610,7 +612,7 @@ class TdxQuantProvider:
|
||||
raw = client.his_dats(symbol, end_d, _bars_for_window((end_d - start_d).days))
|
||||
except TdxQuantError as e:
|
||||
logger.warning("TdxQuant 除权配价 %s 日K失败: %s", symbol, e)
|
||||
return {}
|
||||
raise
|
||||
out: dict[date, float] = {}
|
||||
dates = raw.get("Date") or []
|
||||
closes = raw.get("Close") or []
|
||||
|
||||
Reference in New Issue
Block a user