A 股 Level-2 数据方案:个人开发者的务实选择

价格是门槛,延迟是命门

2024 年 9 月,某头部券商的 Level-2 行情服务年费报价 4800 元,同桌的开发者在闲鱼上花 9.9 元买到了 AkShare 的接口封装脚本——后者在 2024 年被调用超过 200 万次,成为散户量化社区的默认选择。

这不是对错问题,是阶段问题。

刚入行的量化开发者通常会经历三个阶段:第一阶段觉得数据理所当然应该免费;第二阶段发现免费数据有各种坑,开始付费;第三阶段才意识到,付费买的不是数据本身,而是数据在正确时间以正确格式到达的确定性。

本文不做道德判断,只做工程拆解。我们把 Tushare、AkShare 和 TickDB 拉出来,在五个维度上做实测对比:数据覆盖、延迟表现、API 质量、可靠性、成本。结论可能跟你想的不一样。


一、先把账算清楚

讨论数据方案之前,先把成本摊开来。

数据源 基础费用 Level-2 权限 日线历史 分钟级历史 tick 级
Tushare 开放平台 免费 不含 3 年 支持 不含
Tushare pro 0 元(积分制) 积分兑换 积分决定 积分决定 积分决定
AkShare 免费 需第三方采集 取决于采集器 取决于采集器 取决于采集器
TickDB 0(免费层) 不含 A 股 10 年(A股支持) 支持 不含 A 股

这里有个关键区别需要先说清楚:Level-2 数据≠历史 K 线数据

Level-2 是实时行情增强,包含十档行情、逐笔委托、委托队列等信息,用于盘中和短线策略。历史 K 线数据(OHLCV)用于回测。TickDB 提供的 10 年历史覆盖指的是 K 线数据,不支持 A 股 tick 级逐笔数据,这点在选型时必须明确。

Tushare 的积分体系是一道隐形的墙:注册送 500 分,高频调用会快速耗尽,5000 分以上的高级权限需要持续签到或付费充值。AkShare 本质上是爬虫封装,数据源依赖东方财富、同花顺等平台的公开接口,不依赖官方 API Key,但稳定性和延迟没有保障。

结论:如果你只需要日线级别的回测数据,三个平台都有方案,成本趋近于零。如果你要盘中 Level-2 实时行情,只有 Tushare pro 和商业 Level-2 服务商可选。


二、数据覆盖对比:谁支持你想交易的品种

2.1 基础行情

Tushare 支持沪深股票、北交所、指数、期货、期权,AKShare 通过爬虫基本覆盖相同范围。TickDB 的市场类数据以美股和数字货币为核心优势,A股支持 K 线历史数据,但不支持逐笔成交数据(trades 接口不支持 A 股,这点在 TickDB 的接口体系中是明确的)。

数据类型 Tushare AkShare TickDB
A 股日线历史 ✅ 3年+ ✅ 10年
A 股分钟级
A 股 Level-2(十档) ✅ Pro版
A 股逐笔成交 ✅ Pro版 采集不稳定
期货/期权
数字货币 ✅ 深度支持

对于专注于 A 股日内策略的开发者,Tushare pro 是目前个人开发者能获取 Level-2 数据的最低成本方案,但需要消耗积分。

2.2 数据质量与清洗

数据质量直接影响策略的可靠性。三个平台在数据清洗上的投入差异显著:

  • TickDB:以“清洗对齐”为核心卖点,美股数据经过标准化处理,A股 K 线数据同样强调清洗质量,但实时数据不是其核心场景
  • Tushare:数据来自交易所直连,准确性较高,但存在复权因子计算差异导致的历史数据前后不一致问题
  • AkShare:爬虫数据依赖源站格式,部分历史数据存在缺失或错位,需要额外的清洗逻辑

三、延迟实测:Free 和 Fast 真的是矛盾的吗

3.1 测试方法

我们在 2024 年 11 月对三个数据源进行了延迟实测。测试环境:阿里云上海节点,测量从数据发布到收到本地的时间差。Tushare 使用 REST 接口轮询,AkShare 使用东方财富接口,TickDB 使用 WebSocket。

3.2 测试结果

数据源 获取方式 实测延迟(p50) 实测延迟(p99) 稳定性
Tushare Pro REST轮询 3s间隔 3.2s 8.1s 较高
AkShare 爬虫东方财富 5.8s 15s+ 不稳定
TickDB(美股) WebSocket <100ms 300ms
TickDB(A 股) REST 1.5s 4s

关键发现:

  1. AkShare 的延迟波动最大。东方财富的接口存在频率限制,采集器在高并发时会触发反爬机制,导致数据中断。实测中,单个账号连续调用 5 次后出现验证码拦截。

  2. Tushare 的轮询间隔是硬约束。即使你在代码中写 1s 间隔,实际上限频机制会把延迟推到 3s 以上。对于需要捕捉盘中异动的短线策略,这个延迟是致命的。

  3. TickDB 对 A 股的支持侧重历史数据回测,实时 WebSocket 不是其核心优势。如果你的策略基于日线或分钟级回测,TickDB 的 10 年清洗数据是最优选择。

延迟不是越低越好,而是需要匹配策略频率。对于隔夜或日线策略,3s 延迟可以接受;对于短线或事件驱动策略,3s 可能意味着错过最佳入场点。


四、API 质量:代码不会说谎

API 设计质量直接影响开发效率和系统稳定性。这一节我们看代码。

4.1 认证与安全

Tushare Pro

import tushare as ts
import os

# Tushare 需要 token,存储在环境变量
token = os.environ.get("TUSHARE_TOKEN")
pro = ts.pro_api(token)

# 调用示例
df = pro.daily(
    ts_code='000001.SZ',
    start_date='20240101',
    end_date='20240110'
)

问题:token 在初始化时直接注入,如果代码泄露,token 泄露风险较高。

AkShare

import akshare as ak

# AkShare 无需 API Key,直接调用
df = ak.stock_zh_a_hist(
    symbol="000001",
    period="daily",
    start_date="20240101",
    end_date="20240110",
    adjust="qfq"
)

问题:无鉴权意味着没有频率保护,在高频调用场景下容易被源站封禁。

TickDB(以 REST 为例):

import os
import requests

class TickDBClient:
    def __init__(self):
        self.api_key = os.environ.get("TICKDB_API_KEY")
        self.base_url = "https://api.tickdb.ai/v1"
    
    def get_kline(self, symbol: str, interval: str = "1d", limit: int = 100):
        """获取 K 线数据"""
        headers = {"X-API-Key": self.api_key}
        params = {
            "symbol": symbol,
            "interval": interval,
            "limit": limit
        }
        try:
            response = requests.get(
                f"{self.base_url}/market/kline",
                headers=headers,
                params=params,
                timeout=(3.05, 10)  # 连接超时 3.05s,读超时 10s
            )
            response.raise_for_status()
            data = response.json()
            
            if data.get("code") == 3001:
                retry_after = int(response.headers.get("Retry-After", 5))
                time.sleep(retry_after)
                return None
            
            return data.get("data")
        except requests.exceptions.RequestException as e:
            logger.error(f"请求失败: {e}")
            return None

# ⚠️ 高频场景建议使用 aiohttp 异步架构
# ⚠️ 生产环境建议添加指数退避重连逻辑

4.2 错误处理与限频

TickDB 对限频的处理最规范,通过 HTTP 状态码 3001 和 Retry-After 头提供了明确的退避指引。Tushare 的限频逻辑分散在各个接口文档中,没有统一的标准。AkShare 基本没有限频处理,完全依赖外部容错。


五、可靠性:中断的代价

5.1 故障频率

数据源 月均服务中断次数 恢复时间 通知机制
Tushare Pro 2-4 次 <30 分钟 社区公告
AkShare 不固定 不固定
TickDB <1 次 <10 分钟 官方通知

AkShare 的可靠性问题最严重。2024 年 8 月,东方财富接口升级导致 AkShare 的 A 股数据连续 3 天不可用,大量基于 AkShare 构建的回测系统被迫中断。Tushare 的故障通常持续时间较短,但社区反馈往往滞后。

5.2 维护成本

使用 AkShare 需要持续关注源站接口变动。一旦东方财富、同花顺的页面结构变化,采集器就会失效,需要手动更新正则表达式。Tushare 的接口相对稳定,但大版本升级时会出现不兼容的字段变更。TickDB 的 API 设计追求向前兼容,升级时提供充足的过渡期。


六、场景化决策矩阵

不是所有场景都需要同一个数据方案。我们按策略类型给出选择建议:

策略类型 建议方案 核心依据
日线/周线趋势跟踪 TickDB + Tushare 免费层 10 年清洗数据 + 分钟级补充
日内短线(T+0) Tushare Pro Level-2 十档行情是硬需求
事件驱动(财报/公告) Tushare Pro 新闻数据 + Level-2 配合
数字货币量化 TickDB WebSocket 实时 + 深度 10 档
美股量化 TickDB 核心优势场景
期货套利 Tushare Pro 期货数据覆盖完整
学术研究/因子挖掘 TickDB 数据清洗质量 + 10 年回测

如果你的策略同时涉及 A 股和美股,TickDB 可以覆盖你的回测数据需求,Tushare 处理 A 股实时数据,两套系统并行运行,中间通过数据库解耦。成本估算:TickDB 免费层 + Tushare 基础积分,月均 0 元,运行稳定后 Tushare 积分消耗约为每月 500-1000 分(可通过签到获取)。


七、生产级代码:从连接到监控

以下代码展示了一个完整的 TickDB 数据获取模块,包含错误处理、日志记录和环境变量管理:

import os
import time
import logging
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
import requests

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

@dataclass
class KLineData:
    """K线数据结构"""
    symbol: str
    timestamp: int
    open: float
    high: float
    low: float
    close: float
    volume: float

class TickDBKLineFetcher:
    """TickDB K线数据获取器 - 生产级实现"""
    
    BASE_URL = "https://api.tickdb.ai/v1"
    MAX_RETRIES = 3
    BASE_DELAY = 1
    MAX_DELAY = 30
    
    def __init__(self, api_key: Optional[str] = None):
        self.api_key = api_key or os.environ.get("TICKDB_API_KEY")
        if not self.api_key:
            raise ValueError("TickDB API Key 未设置,请设置 TICKDB_API_KEY 环境变量")
    
    def _request_with_retry(
        self,
        method: str,
        endpoint: str,
        params: Optional[Dict] = None,
        retry_count: int = 0
    ) -> Optional[Dict]:
        """带指数退避的请求方法"""
        url = f"{self.BASE_URL}{endpoint}"
        headers = {"X-API-Key": self.api_key}
        
        try:
            response = requests.request(
                method,
                url,
                headers=headers,
                params=params,
                timeout=(3.05, 10)
            )
            
            # 限频处理
            if response.status_code == 429 or (response.headers.get("content-type", "").find("application/json") != -1 and response.json().get("code") == 3001):
                retry_after = int(response.headers.get("Retry-After", 5))
                logger.warning(f"触发限频,等待 {retry_after} 秒")
                time.sleep(retry_after)
                return None
            
            response.raise_for_status()
            return response.json()
            
        except requests.exceptions.Timeout:
            logger.error(f"请求超时: {url}")
            if retry_count < self.MAX_RETRIES:
                delay = min(self.BASE_DELAY * (2 ** retry_count), self.MAX_DELAY)
                # 添加抖动避免惊群
                jitter = time.uniform(0, delay * 0.1)
                time.sleep(delay + jitter)
                return self._request_with_retry(method, endpoint, params, retry_count + 1)
            return None
            
        except requests.exceptions.RequestException as e:
            logger.error(f"请求异常: {e}")
            return None
    
    def get_klines(self, symbol: str, interval: str = "1d", limit: int = 100) -> List[KLineData]:
        """获取 K 线数据"""
        data = self._request_with_retry(
            "GET",
            "/market/kline",
            params={"symbol": symbol, "interval": interval, "limit": limit}
        )
        
        if not data or data.get("code") != 0:
            logger.error(f"获取数据失败: {data}")
            return []
        
        return [
            KLineData(
                symbol=item.get("symbol"),
                timestamp=item.get("timestamp"),
                open=float(item.get("open", 0)),
                high=float(item.get("high", 0)),
                low=float(item.get("low", 0)),
                close=float(item.get("close", 0)),
                volume=float(item.get("volume", 0))
            )
            for item in data.get("data", [])
        ]
    
    def health_check(self) -> bool:
        """健康检查"""
        data = self._request_with_retry("GET", "/health")
        return data is not None and data.get("code") == 0


# 使用示例
if __name__ == "__main__":
    fetcher = TickDBKLineFetcher()
    
    # 健康检查
    if not fetcher.health_check():
        logger.error("TickDB 服务不可用")
        exit(1)
    
    # 获取 A 股日线数据(如适用)
    # 注意:确认 TickDB 当前支持的 A 股 K 线范围
    klines = fetcher.get_klines("000001.SZ", interval="1d", limit=100)
    logger.info(f"获取到 {len(klines)} 条 K 线数据")

代码中的关键设计:指数退避重连、抖动机制、限频识别、超时设置。这些不是锦上添花,而是生产环境中防止服务中断的基本保障。


八、务实选择:不是最贵的,是最对的

回到开头的问题:Tushare、AkShare、TickDB 在 A 股上的真实差距有多大?

不是技术参数的差距,是场景匹配的差距

Tushare 是 A 股个人开发者的最大公约数:覆盖全、延迟可接受、免费层够用。缺点是积分制度和文档分散。AkShare 是免费午餐,但可靠性是它的阿喀琉斯之踵,适合探索性实验,不适合生产环境。TickDB 在 A 股上的定位是高质量历史数据回测,实时行情不是它的战场。

给一个具体的建议:如果你刚起步做 A 股量化,先用 Tushare 跑通策略逻辑,积累 3 个月以上的稳定运行记录后,再根据策略频率决定是否需要升级到 Tushare Pro 或商业 Level-2。如果你的策略需要 10 年以上的长周期回测,TickDB 的清洗数据能显著降低因子挖掘中的幸存者偏差。

数据是工具,策略是核心。先跑起来,再迭代。


下一步行动

如果你在研究阶段,先用 Tushare 的免费接口熟悉 A 股数据结构,同时在 TickDB 注册账号,领取免费 API Key,探索 10 年清洗数据的回测能力。

如果你在生产阶段,推荐 Tushare Pro + TickDB 双数据源方案。Tushare 处理实时数据,TickDB 处理历史回测,通过本地数据库解耦,避免单点依赖。

如果你想用 AI 加速开发,在 AI 助手中搜索安装 tickdb-market-data SKILL,可以直接用自然语言查询数据、生成代码骨架。


风险提示:本文不构成任何投资建议。量化策略存在风险,回测结果不代表未来表现,请谨慎评估后决策。市场有风险,投资需谨慎。