国内量化交易免费数据源全景指南:从pytdx到DuckDB的实战数据架构
为什么免费数据源是量化交易的第一道门槛
很多量化新手第一件事就是去找回测框架——聚宽、米筐、优矿,注册后发现:免费账号只能跑分钟级回测、标的数量受限、API调用有配额。等你策略写好了,想搬到本地跑实盘,突然发现——数据从哪来?
商业数据源(Wind、iFinD、Choice)年费2~5万起步,对个人投资者来说成本太高。好在A股生态中有一批高质量免费数据源,配合合理的架构设计,完全能支撑从回测到实盘的全流程。
本文先横向对比国内主流免费数据源,然后以我实际运行的量化系统为例——5607只标的、1628万行K线数据、1094MB DuckDB本地仓库——拆解一套可复用的数据管道。
国内免费数据源横向对比
一、K线历史行情(核心需求)
| 数据源 | 协议 | 覆盖范围 | 速度 | 稳定性 | 成本 |
|---|---|---|---|---|---|
| pytdx(通达信) | TCP直连 | 股票/ETF/可转债/指数 | ~0.16s/只 | ⭐⭐⭐⭐ | 免费 |
| akshare | HTTP抓取 | 最全(含期货/港美股) | 中等 | ⭐⭐ | 免费 |
| 新浪行情 | HTTP | 实时行情为主 | 快 | ⭐⭐⭐ | 免费 |
| 腾讯行情 | HTTP | 实时+含市值/PE/PB | 快 | ⭐⭐⭐ | 免费 |
| 东财行情 | HTTP | 实时+历史 | 中 | ⭐(IP封) | 免费 |
| BaoStock | TCP | 股票/指数 | 慢 | ⭐⭐ | 免费 |
各数据源深度评测
1. pytdx(通达信行情服务器直连)— 个人量化首选
pytdx 是通达信PC客户端背后的行情协议的Python封装,直连通达信公共行情服务器,不经过网页接口,不会被封IP。这是个人量化交易者最可靠的数据源。
from pytdx.hq import TdxHq_API
api = TdxHq_API()
# 连接公共行情服务器(9个节点自动选优)
api.connect('180.153.18.170', 7709)
# 获取贵州茅台日K线(market=1上证, 每次最多800条)
bars = api.get_bars(4, 1, '600519', 0, 800)
# 返回: date, open, high, low, close, vol, amount, datetime
api.disconnect()
优势:
- 直连行情服务器,不走网页接口,IP不会被封
- 支持股票、ETF、可转债、指数,全品种覆盖
- 单次请求~0.16秒,全市场4500+只股票增量更新约15分钟
- 数据质量好,除权除息信息完整(
get_finance_info/get_xdxr_info)
局限:
- 每次最多返回800条K线,长历史需循环翻页
- 不提供前复权/后复权,需要自行处理(下文详解)
- 在WSL2环境中TCP长连接偶尔超时,需定期重连(每500只重连一次)
2. akshare — 数据最全但稳定性存疑
akshare 封装了东财、新浪、雪球等几十个网页接口,覆盖范围最广(期货、港美股、宏观、财务报表等),但稳定性是最大问题:
- 东财API对服务器IP长期封禁(非临时限流),
fund_etf_hist_em等接口彻底不可用 - 分红汇总接口HTML解析经常失败
- 接口签名随时可能变化,代码维护跟不上
经验法则: akshare作为备用数据源和宏观/基本面数据源,不做K线主源。
3. 新浪/腾讯/东财HTTP行情 — 四路并行实时报价
实时行情场景下,单一数据源容易超时或被封。实际工程中采用多源并行架构:
import requests
import concurrent.futures
# 新浪行情
def sina_quote(code):
prefix = 'sh' if code.startswith('6') else 'sz'
r = requests.get(f"http://hq.sinajs.cn/list={prefix}{code}")
# 返回字段: 名称/今开/昨收/最新价/最高/最低/买一价/卖一价/成交量...
# 腾讯行情(额外含市值/PE/PB)
def tencent_quote(codes):
# f45=总市值(万), f39=PE, f46=PB
url = f"http://qt.gtimg.cn/q={','.join(codes)}"
r = requests.get(url)
# 四路并行,50只<200ms
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(sina_quote, c) for c in codes]
二、基本面与宏观数据
| 数据类型 | 推荐数据源 | 接口示例 |
|---|---|---|
| 财务报表 | akshare | ak.stock_yjbb_em(date="20241231") |
| PE/PB估值 | akshare(百度) | ak.stock_zh_valuation_baidu(symbol, indicator) |
| 申万行业 | akshare | ak.sw_index_second_info() + ak.index_component_sw() |
| 宏观数据 | akshare | ak.macro_china_shibor_all() |
| 可转债数据 | 集思录 | ak.bond_cb_jsl()(免费仅30条) |
| 智能选股 | 东财妙想 | 自然语言选股API(免费50次/天) |
关键提醒: 申万行业代码接口返回带.SI后缀,但成分股查询需要纯数字,必须.replace('.SI','')处理。这类细节是免费数据源使用的家常便饭。
实战:我的量化数据管道
下面以我实际运行的系统为例,展示从数据采集到入库的完整流程。
系统全貌
┌─────────────────────────────────────────────────────────────┐
│ 每日01:00自动执行 │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ pytdx │───▶│ CSV本地存储 │───▶│ DuckDB同步(ETL) │ │
│ │ 增量下载 │ │ /data/stocks│ │ UPSERT防重复 │ │
│ └──────────┘ │ /data/bonds │ └───────┬───────────┘ │
│ │ │ /data/etf │ │ │
│ ▼ └──────────────┘ ▼ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ 指数+宏观 │ │ 前复权计算 │ │ quant.duckdb │ │
│ │ 辅助更新 │ │ 分红复权因子 │ │ 1094MB / 20张表 │ │
│ └──────────┘ └──────────────┘ └───────────────────┘ │
│ │
│ 全流程自动完成,无需人工干预 │
└─────────────────────────────────────────────────────────────┘
数据规模(截至2026年6月)
| 表名 | 标的数 | 行数 | 说明 |
|---|---|---|---|
stock_daily | 4,777只 | 15,425,394行 | A股全市场日K(排除北交所/科创板) |
bond_daily | 988只 | 745,411行 | 可转债日K |
etf_daily | 59只 | 118,105行 | 主流ETF日K |
index_daily | 7个 | 41,505行 | 上证/深证/沪深300等 |
stock_fundamentals | — | — | 财务指标(EPS/营收/ROE等) |
macro_daily | — | — | SHIBOR/PMI/M2等宏观数据 |
| 合计 | 5607只 | ~1628万行 | DuckDB 1094.5MB |
第一层:pytdx增量下载
核心思路是:每个标的维护一个CSV文件,每次只下载最新数据追加到文件末尾。
class PytdxDataSource:
"""通达信行情服务器直连数据源"""
# 9个公共行情服务器节点
TDX_SERVERS = [
('180.153.18.170', 7709),
('xxx.xxx.xxx.xxx', 7709),
('112.74.214.43', 7709),
# ... 共9个节点
]
def get_daily_bars(self, code, market, start=0, count=800):
"""获取日K线,每次最多800条"""
bars = self.api.get_bars(4, market, code, start, count)
# 返回: datetime, open, high, low, close, vol, amount
return pd.DataFrame(bars)
# 增量更新流程
downloader = StockDownloader()
downloader.run_incremental()
# 输出: 统计: 成功=4591 失败=0 跳过=0
工程细节:
- 每500只主动重连pytdx,防止TCP长连接datetime反序列化错误
- 重试机制:失败自动重试5次,间隔15秒
- 请求间隔2秒(pytdx直连不受此限制,仅akshare备用源需要限流)
第二层:ETL同步到DuckDB
CSV是中间层,DuckDB是分析层。每天把更新的CSV增量同步到DuckDB。
# etl_incremental.py 核心逻辑
con = duckdb.connect("quant.duckdb")
con.execute("PRAGMA threads=8")
con.execute("PRAGMA memory_limit='4GB'")
# 检查点机制:记录每个CSV的mtime+size,跳过未变更文件
for csv_file in csv_files:
fp = get_file_fingerprint(csv_file)
if not need_reimport(csv_file, state):
skipped += 1
continue
df = pd.read_csv(csv_file)
# UPSERT: 先删除重复键再插入(防重复积累)
con.register('tmp_df', df)
con.execute("""
DELETE FROM stock_daily
WHERE (code, date) IN (SELECT code, date FROM tmp_df)
""")
con.execute("INSERT INTO stock_daily SELECT * FROM tmp_df")
save_checkpoint(con, csv_file, ...)
为什么用DuckDB而不是MySQL/SQLite?
- 列式存储,读取OHLCV快10倍
- SQL兼容,Python无缝集成
- 单文件部署,无需安装服务端
- 1628万行聚合查询<1秒
第三层:前复权处理
这是免费数据源最大的坑。pytdx默认返回不复权数据,直接用于回测会导致收益率严重失真。
问题根源: pytdx的”不复权”数据中,送转/配股导致的跳空已经被消除,但纯现金分红的跳空仍然存在。一只每年稳定分红5%的股票,不复权数据看起来每年都有5%的跳空下跌,策略会误判为”大跌”而卖出。
def apply_dividend_qfq(raw_df, datasource, code):
"""对纯分红事件做前复权
pytdx不复权数据中:
- 送转/配股跳空已消除(不需要处理)
- 纯分红(fenhong>0)跳空仍在(必须处理)
修正方式: ratio = (close_before - fenhong/10) / close_before
乘到除权日之前的所有价格上
"""
events = datasource._compute_qfq_factor(code)
if events is None or len(events) == 0:
return raw_df # 无分红,无需复权
df = raw_df.copy()
for _, event in events.sort_values('date', ascending=False).iterrows():
# 除权日前所有价格乘以复权因子
mask = pd.to_datetime(df['date']) <= event['date']
ratio = (event['close_before'] - event['fenhong']/10) / event['close_before']
df.loc[mask, ['open','high','low','close']] *= ratio
return df
实测影响: 修正后策略年化收益提升1.5~4.3个百分点,与聚宽/Ptrade回测结果对齐。这是不处理前复权根本发现不了的隐性偏差。
第四层:自动化调度
整套管道通过cron job每日凌晨1点自动执行:
#!/bin/bash
# cron_incremental_download.sh — 每日01:00执行
# 被 cron job 调用,三步流水线
# 第1步: pytdx增量下载CSV
python3.10 multi_downloader.py incremental
# 第2步: CSV → DuckDB同步(检查点+UPSERT)
python3.10 etl_incremental.py
# 第3步: 指数+宏观数据更新
python3.10 update_index_aux.py
# 输出: ✅ 增量数据下载+同步完成 (12分34秒)
# 📥 下载: 成功4591只, 失败0只
# 📦 DuckDB同步: 2,384行 (1.2s)
# 📈 指数更新: 2026-06-17 | 宏观: USD_CNY更新
工程保障:
flock文件锁防止并发执行- 失败标记文件记录上次成功时间
- 详细日志按日期归档(
/data/logs/incremental_YYYYMMDD.log) - 成功/失败都只输出一行汇总,适合飞书/钉钉通知
避坑指南:我踩过的6个坑
坑1:东财API IP长期封禁
现象: ak.fund_etf_hist_em() 持续报 RemoteDisconnected
根因: 东财对服务器IP做了永久封禁,不是临时限流
解决: ETF历史数据改用 pytdx get_daily_bars + apply_qfq 替代
坑2:DuckDB数据重复率75%
现象: 一次SELECT发现4500万行中有3400万行重复
根因: 早期ETL用INSERT而非UPSERT,每天追加导致全量重复
解决: DELETE重复键→INSERT (UPSERT模式),重复率从75%降至0%
影响: 数据库从4.2GB瘦身到1.1GB
坑3:前复权遗漏纯分红
现象: 回测年化收益比聚宽低3-4个百分点
根因: pytdx不复权数据含纯分红跳空,未做前复权
解决: rebuild_stock_qfq.py 全市场4439只重建前复权
教训: 回测结果必须与第三方平台交叉验证
坑4:集思录免费接口仅返回30条
现象: ak.bond_cb_jsl() 只拿到30只可转债数据
根因: 集思录免费接口限制返回条数,全市场需付费cookie
解决: 用 bond_zh_hs_cov_spot() (337只) 拿行情,自行算双低值
坑5:WSL2下pytdx TCP超时
现象: pytdx在WSL2中运行30秒后卡死
根因: WSL2网络栈对TCP长连接处理有bug
解决: 每500只主动断开重连,避免长连接累积
坑6:宏观数据日期格式混乱
现象: PMI/M2等宏观日期为"2024年03月份"中文格式
根因: akshare直接返回网页爬取文本,未做格式标准化
解决: parse_cn_date() 统一解析为 YYYY-MM-DD
免费数据源选型建议
根据我的实战经验,给个人量化交易者的选型建议:
K线历史行情: pytdx > akshare > BaoStock。pytdx直连行情服务器最稳定,全市场增量15分钟搞定。
实时行情: 四路并行(新浪+腾讯+东财+pytdx),50只报价<200ms。单源必超时。
基本面数据: akshare财务报表接口尚可,估值用百度数据源。申万行业注意代码后缀处理。
可转债数据: 集思录免费仅30条,全样本用东货行情接口+自行计算双低值。
宏观数据: akshare的SHIBOR/PMI/CPI接口基本可用,但日期格式需自行标准化。
一句话总结: pytdx做K线主源,akshare做基本面/宏观补充,多源HTTP行情做实时报价,DuckDB做本地仓库。零成本、全品种、自动化的数据管道,完全可行。
本文所有数据均为作者实际系统运行数据(截至2026年6月)。完整源代码和数据管道脚本已在实际系统中验证通过。如果你也在搭建量化数据系统,欢迎在评论区交流经验。
延伸阅读
- DuckDB搭建A股量化数据库教程 — 本地量化数据库实战
- 量化因子挖掘引擎 — 从Alpha158到CogAlpha
- 自建量化回测引擎V2 — 事件驱动回测框架
- Barra因子模型多因子选股实战 — 10因子ICIR合成
- Ptrade 量化实盘部署 — 从策略到自动交易
- Python 量化入门 — 从零到第一个回测脚本