数据源总览
多源降级架构
本项目采用按数据类型分级的多源降级策略(权威声明见
backend/app/services/data_sources/fallback_registry.py,35 个数据类型 × 主源/备胎/兜底),
参考 a-stock-data V3.4.0 十层架构 + AkShare 分页模式设计:
注意:降级方向因数据类型而异,上图仅为整体优先级示意。例如行业排名主源是 腾讯
getRank、资金流向主源是 TDX;东财只在独有数据(龙虎榜/解禁/两融/大宗/ 研报/新闻/财务指标/涨停池)上作主源,K线/实时行情/市值/PE/PB 禁止东财作主源。
数据源目录(可信度页的单一真源)
上面「数据源对比」表只列了参与主备竞争的源,但本仓实际接入的外部源远不止这些。
用户视角的「有哪些源」由 backend/app/services/data_sources/source_catalog.py
声明式目录统一定义,23 个源分8 组:
| 分组 | 源 | 定位 |
|---|---|---|
| 行情 | 腾讯财经 / 通达信 TDX / 新浪 / 东方财富 / 东财涨停池 / 雪球 | 主链路 |
| 交易所 | 上交所 / 深交所 | 官方口径(市场概况/龙虎榜/股票列表) |
| 披露 | 巨潮资讯 / 同花顺 F10 | 法定披露与 F10 档案 |
| 固收指数 | 申万宏源研究 / 中债估值 / 中证指数官网 | 行业分类 / 国债曲线 / 指数估值 |
| 资讯 | 同花顺快讯 / 财联社电报 / 金融界 JRJ / 央视财经 | 舆情与资讯 |
| 宏观 | 金十数据 / FRED 圣路易斯联储 | 国内外宏观 |
| 海外 | yfinance | 港美股兜底(腾讯优先 / Yahoo 兜底) |
| 备用 | 集思录 / 乐咕乐股 / 雪球私募 | 主链路拿不到或需交叉校验 |
三条派生用途:
SOURCE_CATALOG ─┬─ probe_specs() → /health/inspect(主动探测)
├─ catalog_as_dicts() → /health/sources(目录,前端标签真源)
└─ audit_catalog_vs_fallback() → 交叉审计
交叉审计断言「降级链注册表里的每个源都在目录中出现」。若不成立,意味着
存在「业务在用它、但用户在可信度页看不到」的真盲区—— 2026-10-06 首次运行该
审计即抓出 ths 漏登记(降级链键是 ths,目录键是 ths_f10,靠 aliases 对上)。
目录与降级链注册表是互补不是包含关系:
fallback_registry.SOURCE_CAPABILITIES只登记参与主备竞争的 9 个源(降级关系真源),目录覆盖全部 23 个(展示真源)。 两者都需要,缺一不可。
探针纪律三条(违反即产生假信号):
- 必须走业务真实代码路径(调本仓客户端函数),不走裸 URL —— 否则会出现 「探针绿灯、业务因缺 Cookie/指纹而失败」的假健康信号
- 不产生副作用:只读接口,不落库、不发通知
probe=None表示不支持主动探测(如需登录态的雪球私募),前端显示「未探测」 而非「异常」—— 把"没测"说成"坏了"同样是误导
页面入口 /data-trust(侧栏「发现」→ 数据可信度)。
数据源对比
| 数据源 | 协议 | 封IP风险 | 数据覆盖 | 延迟 | 适用场景 |
|---|---|---|---|---|---|
| 腾讯财经 | HTTP | 无 | K线/指数/估值/行业排名/全市场行情 | ~100ms | K线、指数、估值、行业排名主源 |
| TDX 自实现 | TCP 7709 | 无 | 盘口/资金流/财务30字段/板块/市场统计/除权除息 | ~50ms | 实时盘口/资金流主源,K线第一备胎 |
| 东方财富 | HTTP | 高 | 独有数据最全(龙虎榜/两融/大宗/涨停池…) | ~200ms | 独有数据主源,需限流 |
| 新浪 | HTTP | 低 | 财报三表/复权因子/北交所K线/资金流兜底 | ~150ms | 东财被封时备用 |
| 巨潮 | HTTP | 低 | 公告/定期报告/F10 | ~200ms | 公告主源 |
| 申万宏源研究 | HTTP | 低 | 行业分类/成分股/行业指数日线 | ~300ms | 申万口径行业唯一源 |
上表只列参与主备竞争的源;全量 23 个源见上方「数据源目录」。 历史遗留的
baostock_client.py/baostock_provider.py是自建兼容壳 (原 baostock 依赖已完全移除,K线转发东财 push2his),保留仅为兼容旧引用。
限速与节流参数
各 HTTP 数据源客户端内置进程级最小间隔节流(配置均可环境变量覆盖,见 core/config.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
EM_MIN_INTERVAL | 0.3s | 东财两次请求最小间隔(附 0.1~0.5s 随机抖动;批量同步实测安全值,可用环境变量调回更保守值) |
TENCENT_MIN_INTERVAL | 0.2s | 腾讯最小间隔(不封 IP,宽松) |
XQ_MIN_INTERVAL | 1.0s | 雪球最小间隔(风控较严,保守) |
HOST_FAILURE_THRESHOLD | 2 | 东财镜像主机连续失败 N 次才进入 30s 冷却 |
STOCK_CHANGES_CONCURRENCY | 3 | push2ex 异动监控并发上限(Semaphore) |
TDX 为 TCP 协议自带连接管理(心跳 15s + 健康评分 + 共享冷却熔断),不做 HTTP 式节流。
数据源级成功/失败计数可通过 GET /datamgr/probe-all 响应的 counters/breakers 字段查看。
数据源抽象层
data_source.py 定义统一接口,通过工厂模式切换实现:
class DataSource(ABC):
def search(self, keyword: str) -> list[dict]: ...
def get_stock(self, code: str) -> dict | None: ...
def get_kline(self, code: str, days: int = 120) -> list[dict]: ...
def get_finance(self, code: str) -> list[dict]: ...
def get_industry_compare(self, code: str) -> list[dict]: ...
def get_data_source(source: str = "mock") -> DataSource:
# "real" → RealDataSource (腾讯+TDX自实现+东财+新浪,腾讯为K线/估值主源)
# "akshare" → AkshareDataSource (直连东财/新浪API)
# "tushare" → TushareDataSource (占位,需 TUSHARE_TOKEN)
# "mock" → MockDataSource (开发测试)
K线数据三级降级
RealDataSource.get_kline() 的降级链:
北交所(920xxx/8xxxxx/4xxxxx)为例外链路:腾讯实测仅返回最新 1 根、TDX 不覆盖,
故走 新浪 → 东财 push2his。分钟线为独立链路(minute_providers,1/5/15/30/60m)。
财务数据二级降级
涨跌分布/资金概况三级降级
首页涨跌分布和资金概况使用 TDX TCP 实时统计,不依赖已移除的 stock_base_info 静态快照:
⚠️ 东财 push2
filter参数已被服务端停用(2026-09-22 实测):带括号 / 无括号 /filters/ URL 编码等任何形式都被忽略(同一fs传两种取值total不同,可证只认fs),所有分段请求都返回全量总数 →up == down == total、13 段全等。em_market_breadth()因此改为先发 2 路探针(全量 + 上涨)验证 filter 是否生效, 失效即整体判不可用:既不白打 17 路请求(东财有风控,无效请求同样计入主机冷却, 会牵连龙虎榜/两融等东财独有数据),也不把"静默错值"交给下游。filter 恢复后无需改码, 探针通过即自动回到链上。
🔴 分布载荷自洽门禁:
builders.distribution_counts_consistent()要求up + down + flat ≈ total ≈ Σbins.count(±10%,容忍停牌无涨跌幅行)且 13 段不全等, 作为所有返回路径(实时 / 快照 / 缓存)与东财源的统一闸门——不自洽的载荷 无法落快照、也无法被展示,存量脏快照行会被is_valid_data跳过并重新获取。 交易时段禁用宽表/DB 旧分布兜底(盘中展示昨日分布会误导短线决策),此时宁可 不给直方图、由前端退化为 5 分类展示,家数仍保持实时源。
db_market_breadth()等 DB 聚合函数自 2026-08-22 起恒返回 None(stock_base_info表已移除),资金概况的「上一日成交额」改走fox_market_daily.total_amount。
数据标准化
所有数据源的返回值经过统一标准化处理:
- 数值转换:
safe_float()处理"-"、""、None等异常值 - 代码校验:
_validate_stock_code()正则验证6位数字格式 - 字段映射:东财
f2/f3/f4...→ 语义化price/change_percent/...
from .data_sources.em_paginated import safe_float
# 东财返回的 "-" 不会导致 ValueError
price = safe_float(item.get("f2")) # "-" → 0.0