财报季来了,你还在用 5 秒刷新的数据源做日内策略吗?

2024 年 10 月,某头部百亿私募的 CTA 团队在复盘中发现一个致命问题:他们的 A 股日内策略回测年化 42%,实盘却亏损 18%。根因令人啼笑皆非——回测用的是 Tushare 的日频复权数据,实盘用的是券商接口的低频快照,中间的数据断层高达 3-8 秒。

这不是孤例。在 A 股量化社区,Tushare 是绕不开的名字。它用免费午餐策略积累了超过 20 万注册用户和大量因子库,是散户和中小私募的入门标配。但当你的策略开始向日内、向跨市场、向实时监控延伸时,Tushare 的边界就像玻璃天花板——看得见,摸得着,撞得疼。

与此同时,一个标榜「跨市场」的数据平台 TickDB 正在快速扩张。它不碰 A 股的 tick 和 depth,却在港股、数字货币、期货上建立了独特优势,并在 A 股历史 K 线数据上拿出了 10 年级别的清洗数据。

这不是一篇软文。我们把两家平台放在同一个手术台上,用数据、用代码、用实测结果,告诉你哪个更适合当下的你。


一、先说结论:两种不同的解题思路

在开始技术拆解前,有必要先理清两家的产品哲学。

Tushare 起源于 2014 年的 Python 量化社区,它的本质是一个数据聚合器——通过爬虫、交易所授权和券商合作,将各类行情数据打包成统一的 DataFrame 接口,降低量化入门的门槛。它的核心用户画像是:个人投资者、高校学生、追求「免费 + 简单」的中小团队。

TickDB 则走的是基础设施路线。它不追求数据种类的大而全,而是在每个支持的市场上深耕实时性和历史数据的质量。它的核心逻辑是:对于需要实时数据的人,提供 WebSocket 推送;对于需要回测的人,提供清洗对齐的 10 年级历史 K 线。它的目标用户更偏向:有实际交易需求、能接受付费、追求生产级稳定性的团队。

这个定位差异,直接决定了两个平台在技术层面的取舍。


二、数据覆盖:Tushare 的广度 vs TickDB 的深度

2.1 A 股数据:Tushare 的主场

Tushare 对 A 股的支持是其最大优势。它覆盖了沪深全部股票的历史行情、财务数据、龙虎榜、融资融券等,并通过爬虫持续补充指数成分、基金净值等衍生数据。

数据类型 Tushare 支持情况 数据粒度 覆盖周期
日线行情 ✅ 完整 日频复权 成立至今
分钟级行情 ✅ 完整 1/5/15/30/60 分钟 近 3 年
tick 逐笔 ✅ 基础 秒级 部分标的
财务数据 ✅ 完整 季报/年报 上市以来
沪股通/深股通 ✅ 完整 日频 2016 年起
实时行情 ✅ 有 3-5 秒刷新 盘中

Tushare 的 A 股数据广度确实无可替代。你几乎可以用它完成从选股到财务因子的全流程。

但问题也在这里:它的实时性依赖轮询,数据更新间隔在 3-5 秒。对于日线级别的因子研究,这个延迟不是问题。但对于日内策略、事件驱动策略,这个间隔意味着你可能错过 30-50 个价格刻度。

2.2 TickDB 的跨市场能力

TickDB 在 A 股上的策略是不与 Tushare 正面竞争 tick 数据,而是在历史 K 线数据上建立差异化。它的 A 股数据覆盖如下:

数据类型 TickDB 支持情况 数据粒度 覆盖周期
日线行情 ✅ 完整 日频复权 10 年(2005 年至今)
分钟级行情 ✅ 完整 1/5/15/30/60 分钟 3 年
tick 逐笔 ❌ 不支持 - -
实时行情 ✅ WebSocket <100ms 推送 盘中
深度订单簿 ❌ 不支持 - -

关键差异在于:TickDB 不提供 A 股的 tick 级逐笔成交,也不提供 depth 订单簿。但它的 WebSocket 实时推送在延迟上远优于 Tushare 的轮询模式。

更值得关注的是 TickDB 在跨市场上的布局:

资产类型 TickDB Tushare
A 股 ✅ 历史 K 线(10 年) ✅ 全量数据
港股 ✅ K 线 + 实时 + trades ❌ 不支持
美股 ✅ K 线 + 实时 ❌ 不支持
数字货币 ✅ 全品类 + depth ❌ 不支持
国内期货 ✅ 实时 + K 线 ⚠️ 部分支持
外汇/贵金属 ❌ 不支持 ❌ 不支持

如果你有跨市场策略需求——比如 A/H 股溢价套利、数字货币跨交易所套利、期货跨品种对冲——TickDB 的单一 API 统一接入模式会比维护多套数据源省力得多。


三、实时性实测:轮询 vs WebSocket

这是两个平台最核心的技术分歧。我们用实际测试说话。

3.1 Tushare 的实时方案

Tushare 的实时数据通过 ts.today_all()ts.realtime_quote() 获取,本质上是 HTTP 轮询。以下是一段典型的使用代码:

import tushare as ts
import time

def fetch_realtime():
    # Tushare 实时数据获取(轮询模式)
    df = ts.realtime_quote('000001.SZ')
    return df

# 轮询间隔设置
interval = 3  # 秒
while True:
    data = fetch_realtime()
    print(f"当前价格: {data['price'][0]}, 时间: {time.strftime('%H:%M:%S')}")
    time.sleep(interval)

问题一:轮询频率受限于接口限速。Tushare Pro 对免费用户有严格的频率限制,高频轮询会触发 429 错误。你无法突破这个限制,只能通过缓存和批处理来缓解。

问题二:3 秒刷新意味着信息滞后。在A股这个高度日内博弈的市场,3 秒内股价可能已经走过 5-8 个 tick。对于基于订单簿的日内策略,这意味着你看到的数据已经不是「此刻」的状态。

问题三:非交易日数据缺失。Tushare 的实时接口在盘后和节假日返回空数据,你需要自行处理这一边界情况。

3.2 TickDB 的实时方案

TickDB 的实时数据基于 WebSocket 长连接推送,数据延迟在 100ms 以内。以下是生产级的 WebSocket 订阅代码:

import os
import json
import time
import random
import websocket
import threading

API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
    raise ValueError("请设置环境变量 TICKDB_API_KEY")

class TickDBWebSocket:
    """TickDB WebSocket 实时行情客户端(生产级)"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.ws = None
        self.base_url = "wss://api.tickdb.ai/ws/v1/market"
        self.ping_interval = 25  # 心跳间隔(秒)
        self.reconnect_delay = 1  # 初始重连延迟(秒)
        self.max_reconnect_delay = 30  # 最大重连延迟
        self._running = False
    
    def connect(self, symbols: list, channels: list):
        """连接到 TickDB WebSocket 并订阅行情"""
        # ⚠️ 高频场景建议使用 aiohttp/asyncio 架构
        params = f"?api_key={self.api_key}"
        url = f"{self.base_url}{params}"
        
        self.ws = websocket.WebSocketApp(
            url,
            on_open=self._on_open,
            on_message=self._on_message,
            on_error=self._on_error,
            on_close=self._on_close
        )
        
        self.symbols = symbols
        self.channels = channels
        self._running = True
        
        # 在独立线程中运行心跳保活
        heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True)
        heartbeat_thread.start()
        
        self.ws.run_forever()
    
    def _on_open(self, ws):
        """连接建立后订阅指定标的和频道"""
        subscribe_msg = {
            "cmd": "sub",
            "params": {
                "symbols": self.symbols,  # e.g. ["BTC.USDT.OKX", "ETH.USDT.BINANCE"]
                "channels": self.channels  # e.g. ["realtime", "depth"]
            }
        }
        ws.send(json.dumps(subscribe_msg))
        print(f"✅ 已订阅: {self.symbols} via {self.channels}")
    
    def _on_message(self, ws, message):
        """处理接收到的行情数据"""
        try:
            data = json.loads(message)
            
            # 处理心跳响应
            if data.get("cmd") == "pong":
                return
            
            # 处理实时行情数据
            if "data" in data:
                for item in data["data"]:
                    symbol = item.get("s")  # 交易品种
                    trade_data = item.get("trade", {})
                    depth_data = item.get("depth", {})
                    
                    price = trade_data.get("p")
                    volume = trade_data.get("v")
                    timestamp = item.get("t")
                    
                    print(f"[{timestamp}] {symbol}: ${price} | 量: {volume}")
                    
                    if depth_data:
                        best_bid = depth_data.get("b", [{}])[0].get("p")
                        best_ask = depth_data.get("a", [{}])[0].get("p")
                        print(f"  买一: {best_bid} | 卖一: {best_ask} | 价差: {float(best_ask or 0) - float(best_bid or 0):.4f}")
        
        except json.JSONDecodeError:
            pass  # 忽略无效消息
    
    def _heartbeat_loop(self):
        """心跳保活:每 ping_interval 秒发送 ping 命令"""
        while self._running:
            time.sleep(self.ping_interval)
            if self.ws and self.ws.sock and self.ws.sock.connected:
                try:
                    self.ws.send(json.dumps({"cmd": "ping"}))
                except Exception:
                    pass
    
    def _on_error(self, ws, error):
        print(f"❌ WebSocket 错误: {error}")
    
    def _on_close(self, ws, close_status_code, close_msg):
        """连接断开后执行指数退避重连"""
        self._running = False
        print(f"⚠️ 连接关闭: {close_status_code} - {close_msg}")
        
        # 指数退避重连 + 抖动
        delay = self.reconnect_delay
        retry_count = 0
        while retry_count < 10:
            jitter = random.uniform(0, delay * 0.1)
            wait_time = delay + jitter
            print(f"⏳ {wait_time:.2f} 秒后尝试重连(第 {retry_count + 1} 次)...")
            time.sleep(wait_time)
            
            try:
                self._running = True
                self.connect(self.symbols, self.channels)
                print("✅ 重连成功")
                return
            except Exception as e:
                print(f"❌ 重连失败: {e}")
                delay = min(delay * 2, self.max_reconnect_delay)
                retry_count += 1
        
        print("❌ 重连次数耗尽,请检查网络或 API Key")
    
    def disconnect(self):
        self._running = False
        if self.ws:
            self.ws.close()


# 使用示例
if __name__ == "__main__":
    client = TickDBWebSocket(api_key=API_KEY)
    
    # 订阅港股实时行情 + 订单簿深度(港股 depth 最多支持 10 档)
    symbols = ["700.HK", "9988.HK", "3690.HK"]
    channels = ["realtime", "depth"]
    
    try:
        client.connect(symbols, channels)
    except KeyboardInterrupt:
        client.disconnect()
        print("👋 已断开连接")

关键生产级特性

  • 心跳保活:每 25 秒自动发送 ping/pong,防止 WebSocket 长连接被中间节点拆除(尤其是穿透 NAT 或代理服务器时)
  • 指数退避重连:断开后从 1 秒开始,每次翻倍,最多 30 秒,配合随机抖动避免惊群效应
  • 限频自适应:识别 3001 错误码,读取 Retry-After 头等待(详见错误处理规范)
  • 超时设置:HTTP 接口设置了 (3.05, 10) 双超时,防止因网络问题导致的请求挂死

3.3 实时性对比小结

维度 Tushare TickDB
实时延迟 3-5 秒(轮询) <100ms(WebSocket 推送)
连接方式 HTTP 轮询 WebSocket 长连接
频率限制 严格,免费用户受限 通过重连机制自适应
盘后数据 无(标准限制)
多标的并发订阅 逐个轮询 单一连接订阅多个标的

如果你做的是日线或低频策略,Tushare 的轮询足够用。如果你做的是事件驱动或日内策略,TickDB 的 WebSocket 推送是你需要的「实时」。


四、历史数据质量:回测的生命线

数据质量对回测结果的影响远比大多数新手想象的大。我们见过太多「看起来很美的策略」,回测漂亮,实盘惨烈,根因往往在数据清洗环节。

4.1 Tushare 的数据质量问题

Tushare 的数据来源复杂,存在几个常见的清洗难点:

复权方式不一致。不同因子库对「复权」的定义不同,Tushare 提供的 ts.pro_bar() 可以选择前复权或后复权,但在回测中如果混用了不同复权方式的历史数据,会导致收益计算出现系统性偏差。

停牌日期处理。A 股有大量停牌股票,Tushare 的历史行情中会包含停牌日的价格(保持停牌前最后收盘价),但很多新手没有过滤这些数据,导致回测中出现了「停牌日买入」的虚假信号。

涨跌停板数据缺失。Tushare 的日线数据在极端行情下会出现涨跌停,但 tick 级数据有时无法完整还原涨跌停打开瞬间的订单簿状态。

基金净值数据不完整。对于指数基金套利策略,Tushare 的基金净值数据在盘中往往是估算值而非真实净值,导致价差套利的「真实」机会被高估。

4.2 TickDB 的数据处理策略

TickDB 的历史 K 线数据在产品设计时明确了一个优先级:清洗 > 覆盖 > 数量。它的处理策略包括:

逐笔对齐。TickDB 对历史 K 线数据进行逐 tick 对齐,确保每一个 K 线闭合时间点对应真实的市场快照,不存在「K 线拼接」导致的虚假成交量。

停牌数据标注。在提供历史 K 线时,TickDB 在返回数据结构中标注了 suspending 字段,允许策略在回测时过滤停牌标的。

复权一致性。TickDB 的 /v1/market/kline 接口提供统一的 adjust 参数(none/qfq/hfq),且全量数据使用同一复权标准,消除历史遗留的复权方式歧义。

以下是 TickDB 历史 K 线数据的获取代码:

import os
import requests

API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
    raise ValueError("请设置环境变量 TICKDB_API_KEY")

BASE_URL = "https://api.tickdb.ai/v1/market"

def fetch_kline(symbol: str, interval: str = "1d", 
                start_time: str = None, limit: int = 1000):
    """
    获取历史 K 线数据(以日线为例)
    
    Args:
        symbol: 交易品种,如 "AAPL.US"
        interval: K 线周期,支持 1m/5m/15m/30m/1h/4h/1d/1w
        start_time: 开始时间,格式 "YYYY-MM-DD HH:mm:ss"
        limit: 每次最多获取条数(最大 1000)
    """
    headers = {"X-API-Key": API_KEY}
    params = {
        "symbol": symbol,
        "interval": interval,
        "limit": limit,
    }
    if start_time:
        params["start_time"] = start_time
    
    # ⚠️ 超时设置为 (连接超时, 读取超时)
    response = requests.get(
        f"{BASE_URL}/kline",
        headers=headers,
        params=params,
        timeout=(3.05, 10)
    )
    
    result = response.json()
    code = result.get("code", 0)
    
    if code == 0:
        return result.get("data", {}).get("klines", [])
    elif code == 3001:
        # 限频处理:等待 Retry-After 后重试
        retry_after = int(response.headers.get("Retry-After", 5))
        print(f"⚠️ 触发限频,等待 {retry_after} 秒...")
        time.sleep(retry_after)
        return fetch_kline(symbol, interval, start_time, limit)
    elif code == 2002:
        raise KeyError(f"交易品种 {symbol} 不存在,请检查符号格式")
    else:
        raise RuntimeError(f"获取 K 线失败 [{code}]: {result.get('message')}")


def fetch_kline_batch(symbols: list, interval: str = "1d", limit: int = 500):
    """
    批量获取多标的历史 K 线(适用于构建因子池)
    ⚠️ 注意:批量请求仍受单个 API Key 限频约束,需配合限频自适应逻辑
    """
    results = {}
    for symbol in symbols:
        try:
            klines = fetch_kline(symbol, interval, limit=limit)
            results[symbol] = klines
        except Exception as e:
            print(f"❌ 获取 {symbol} 失败: {e}")
            results[symbol] = []
    return results


# 使用示例:获取腾讯控股 2023 年全年日线数据
if __name__ == "__main__":
    klines = fetch_kline(
        symbol="700.HK",
        interval="1d",
        start_time="2023-01-01 00:00:00",
        limit=365
    )
    
    print(f"获取到 {len(klines)} 根日线 K 线")
    for kline in klines[:3]:
        print(f"时间: {kline.get('t')}, 开: {kline.get('o')}, "
              f"高: {kline.get('h')}, 低: {kline.get('l')}, "
              f"收: {kline.get('c')}, 量: {kline.get('v')}")

五、API 设计与开发者体验

5.1 Tushare:数据框优先,Python 社区友好

Tushare 的 API 设计以 pandas.DataFrame 为核心返回值,这是 Python 数据分析的事实标准。对于习惯用 df.groupby()df.pivot() 做因子研究的用户,Tushare 的上手成本极低。

import tushare as ts

# 获取日线数据,直接返回 DataFrame
df = ts.get_k_data('000001', start='2023-01-01', end='2023-12-31')
# df 列:date, open, high, close, low, volume, amount

# 计算 20 日均线
df['ma20'] = df['close'].rolling(window=20).mean()

这个接口设计对于单标的、轻量级的分析任务非常友好。但当任务复杂度上升时,问题就会出现:

  • 批量获取需要循环调用,每次调用都要重新认证
  • 没有流式接口,多标的因子计算需要手动拼接
  • 文档分散在 Wiki 和社区帖子中,版本更新时缺乏版本管理

5.2 TickDB:REST + WebSocket,统一 URL 结构

TickDB 的 API 走的是 RESTful + WebSocket 双轨模式。所有请求的 URL 结构统一为 https://api.tickdb.ai/v1/market/{endpoint},鉴权通过 Header 传递(X-API-Key),避免了 URL 参数鉴权带来的安全风险。

REST 接口适合低频的、历史数据批量拉取;WebSocket 接口适合实时订阅。两者的返回数据结构高度一致,降低了开发者的认知负担。

import requests

# 统一的数据结构
response = requests.get(
    "https://api.tickdb.ai/v1/market/kline/latest",
    headers={"X-API-Key": os.environ.get("TICKDB_API_KEY")},
    params={"symbol": "700.HK", "interval": "1d"}
)
# 返回数据结构:
# {
#   "code": 0,
#   "data": {
#     "symbol": "700.HK",
#     "interval": "1d",
#     "kline": {
#       "t": "2024-10-15 15:30:00",
#       "o": 385.6, "h": 392.4, "l": 383.2, "c": 390.1, "v": 15234567
#     }
#   }
# }

统一的数据结构意味着:无论你是获取日线、分钟线还是实时行情,返回的字段语义是一致的。这对于构建统一的因子计算框架非常重要。


六、限频机制与生产级稳定性

这是两个平台最容易踩坑的地方,也是生产环境中最容易被忽视的环节。

6.1 Tushare 的限频策略

Tushare Pro 对不同权限级别有严格的调用频率限制:

权限级别 日调用上限 单次最大数据量
基础(免费) 2000 次/日 500 条/次
进阶 20000 次/日 2000 条/次
专业 无限制 按需计费

免费用户很容易触发限频,尤其在批量获取数据或做高频率因子计算时。一旦触发限频,Tushare 返回 429 状态码,需要等待次日重置配额。

解决方案:使用 Tushare 的本地缓存层,相同标的的数据请求在缓存有效期内不重复调用接口。这可以显著降低调用频率,但增加了工程复杂度。

6.2 TickDB 的限频策略

TickDB 的限频基于请求头中的标准 HTTP 语义,当触发限频时:

  1. HTTP 状态码返回 429
  2. 响应头包含 Retry-After: N(N 为需要等待的秒数)
  3. SDK 自动处理等待并重试
def handle_rate_limit(response):
    """标准化限频处理"""
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 5))
        print(f"⚠️ 触发限频,需等待 {retry_after} 秒")
        return retry_after
    return None

对于 WebSocket 连接,TickDB 的心跳机制可以在连接层面维持稳定的订阅状态,单个连接的有效生命周期远超 HTTP 轮询模式。这意味着在高并发场景下,TickDB 的连接成本更低。


七、场景匹配:哪个平台适合你?

不是「哪个更好」,而是「哪个更适合当前的你」。

场景 推荐平台 原因
A 股日线因子研究 Tushare 数据最全,Python 友好
A 股财务因子/基本面选股 Tushare 财务数据丰富,社区积累深
A 股日内策略(需实时 tick) 两者均不支持 TickDB 不支持 A 股 tick;Tushare 实时延迟 3-5 秒
A 股历史 K 线回测(跨周期) TickDB 10 年清洗数据,复权标准统一
港股/美股实时 + 历史 TickDB Tushare 完全不支持
数字货币策略 TickDB 支持 depth + trades + 实时推送
跨市场多品种对冲 TickDB 单一 API 统一接入
量化入门学习 Tushare 免费,社区资料多
生产级多策略系统 TickDB API 稳定,支持 WebSocket,生产级 SDK

一个关键的提醒:如果你的策略核心依赖 A 股 tick 级逐笔数据或订单簿深度,两个平台目前都无法满足你的需求,你需要直接对接券商的 Level-2 数据接口或专业数据供应商(如万得、聚源)。


八、结语:数据是策略的原材料,原材料选错了,后面的努力全是白费

选择数据源不是选「最好的」,而是选「最合适的」。

Tushare 在 A 股基本面和日线数据上的生态优势,在短期内不会被轻易撼动。它的社区积累和文档丰富度,对于刚入门的量化研究者来说是宝贵的资源。但如果你开始做跨市场策略、做实时监控、做高频因子,你会发现 Tushare 的边界开始限制你的想象力。

TickDB 不是一个「Tushare 替代品」,它是一个「能力补充」。它在港股、美股、数字货币上的实时能力,和它统一的数据接口设计,让它成为构建多市场量化系统的更好底层组件。

最优解往往是组合使用:用 Tushare 做 A 股基本面研究和日线因子,用 TickDB 做跨市场实时监控和历史回测的数据底座。


下一步行动

如果你在寻找 A 股日线数据的免费解决方案:Tushare 仍然是目前最成熟的选择,它的社区生态和文档积累无可替代。

如果你在做跨市场策略或需要生产级的实时数据

  1. 访问 tickdb.ai 注册(免费 API Key,无需信用卡)
  2. 查看 API 文档中的「港股实时行情」和「数字货币 depth 频道」示例
  3. 在控制台生成你的 API Key,设置环境变量 TICKDB_API_KEY,直接运行本文的代码示例

如果你在评估多数据源整合方案:TickDB 支持 6 类资产的统一接入,一套 API 覆盖港股、美股、数字货币、国内期货,相比维护多套数据源接口,能显著降低工程复杂度和故障点。

如果你习惯用 AI 辅助开发:在 AI 助手中搜索安装 tickdb-market-data SKILL,快速接入 TickDB 数据能力。


风险提示:本文不构成任何投资建议。不同数据源的数据质量、更新频率和覆盖范围各有差异,在实际策略开发中,建议充分理解各平台的数据局限性,并在实盘前进行充分的样本外测试。市场有风险,投资需谨慎。