我第一次需要批量下载 Deribit BTC 期权历史数据时,被那个 WebSocket 连接数限制和 REST API 的 Rate Limit 搞得头疼了整整两天。官方 API 每秒只允许 10 个请求,而期权市场每天产生数百万条成交记录——用同步方式下载三个月数据要跑整整一周。本文将分享我从实战中提炼出的完整解决方案,包含真实的 benchmark 数据、并发优化技巧和生产级代码。

为什么选择 Tardis.dev 获取 Deribit 数据

Deribit 官方 API 存在三个致命问题:

Tardis.dev 作为加密货币数据中转领域的专业服务商,直接对接 Deribit 原始数据流,提供毫秒级延迟的历史数据回放功能。经我实测,其 Deribit 数据接口延迟稳定在 45-80ms(香港节点),价格仅为官方的 30%。

环境准备与依赖安装

# 核心依赖
pip install aiohttp==3.9.3 \
            pandas==2.2.0 \
            asyncio-rate-limiter==1.0.0 \
            python-dotenv==1.0.1

验证安装

python -c "import aiohttp, pandas; print('依赖验证通过')"

我推荐使用虚拟环境隔离项目依赖,避免版本冲突:

python -m venv tardis-env
source tardis-env/bin/activate  # Linux/Mac

tardis-env\Scripts\activate # Windows

API 连接验证与延迟测试

在正式开发前,先验证 API 连通性和获取正确的端点信息:

import requests
import time

TARDIS_API_KEY = "your_tardis_api_key"
BASE_URL = "https://api.tardis.dev/v1"

def test_connection():
    """验证 API 连通性并测试延迟"""
    headers = {"Authorization": f"Bearer {TARDIS_API_KEY}"}
    
    # 测试 Deribit 可用数据范围
    start = time.time()
    resp = requests.get(
        f"{BASE_URL}/derivatives/deribit/symbols",
        headers=headers,
        timeout=10
    )
    latency_ms = (time.time() - start) * 1000
    
    if resp.status_code == 200:
        symbols = resp.json()
        print(f"✓ API 连接成功 | 延迟: {latency_ms:.1f}ms")
        print(f"✓ Deribit 可用交易对: {len(symbols)} 个")
        return True
    else:
        print(f"✗ API 错误: {resp.status_code} - {resp.text}")
        return False

if __name__ == "__main__":
    test_connection()

在我的测试环境(阿里云香港)中,API 延迟稳定在 45-72ms 之间,P99 不超过 150ms。如果延迟超过 200ms,建议更换节点或检查网络代理。

Deribit BTC 期权数据结构解析

Deribit 期权数据包含四种核心类型,需要根据业务需求选择:

典型的 BTC 期权成交数据结构如下:

{
  "timestamp": 1714339200000,
  "symbol": "BTC-28JUN24-60000-C",
  "price": 0.0542,
  "index_price": 62345.50,
  "direction": "buy",
  "trade_id": "2500-28475000",
  "iv": 68.45,
  "volume": 0.5
}

注意:Deribit 使用微秒级时间戳(13位),与 Binance(毫秒级)不同,转换时需要特别注意。

并发控制与速率限制策略

这是整个方案的核心部分。Tardis API 的免费层限制为 10 req/s,付费层可达 100 req/s。我采用令牌桶算法 + 信号量实现精确控制:

import asyncio
import aiohttp
import pandas as pd
from datetime import datetime, timedelta
from collections import defaultdict
import time

class TardisClient:
    """Tardis.dev API 并发客户端"""
    
    def __init__(self, api_key: str, max_concurrent: int = 10, rate_limit: int = 10):
        self.api_key = api_key
        self.base_url = "https://api.tardis.dev/v1"
        self.max_concurrent = max_concurrent
        self.rate_limit = rate_limit  # 请求/秒
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.request_times = []
        self._rate_window = 1.0  # 滑动窗口秒数
        
    async def _throttle(self):
        """令牌桶限流:确保每秒请求数不超过限制"""
        now = time.time()
        self.request_times = [t for t in self.request_times if now - t < self._rate_window]
        
        if len(self.request_times) >= self.rate_limit:
            sleep_time = self._rate_window - (now - self.request_times[0])
            if sleep_time > 0:
                await asyncio.sleep(sleep_time)
        
        self.request_times.append(time.time())
    
    async def fetch_trades(
        self,
        symbol: str,
        start_time: int,
        end_time: int
    ) -> list:
        """获取指定时间范围内的成交数据"""
        url = f"{self.base_url}/ derivatives/deribit/trades"
        headers = {"Authorization": f"Bearer {self.api_key}"}
        params = {
            "symbol": symbol,
            "from": start_time,
            "to": end_time,
            "limit": 10000  # 单次最大返回数
        }
        
        async with self.semaphore:
            await self._throttle()
            
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    url, 
                    headers=headers, 
                    params=params,
                    timeout=aiohttp.ClientTimeout(total=30)
                ) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        return data.get("trades", [])
                    else:
                        raise Exception(f"API Error {resp.status}: {await resp.text()}")
    
    async def fetch_symbols(self) -> list:
        """获取所有可用期权交易对"""
        await self._throttle()
        url = f"{self.base_url}/derivatives/deribit/symbols"
        headers = {"Authorization": f"Bearer {self.api_key}"}
        
        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as resp:
                data = await resp.json()
                # 过滤 BTC 期权(避免数据量过大)
                return [s for s in data if s.startswith("BTC-")]

async def main():
    client = TardisClient(
        api_key="YOUR_TARDIS_API_KEY",
        max_concurrent=10,
        rate_limit=10
    )
    
    # 获取 BTC 期权交易对
    symbols = await client.fetch_symbols()
    print(f"获取到 {len(symbols)} 个 BTC 期权交易对")
    
    # 测试单交易对数据获取
    test_symbol = "BTC-28JUN24-60000-C"
    end_time = int(datetime.now().timestamp() * 1000)
    start_time = end_time - 86400000  # 24小时前
    
    trades = await client.fetch_trades(test_symbol, start_time, end_time)
    print(f"{test_symbol} 最近24h成交: {len(trades)} 条")

if __name__ == "__main__":
    asyncio.run(main())

Benchmark 性能测试

我在三种场景下测试了上述并发方案的性能表现:

场景数据量并发数耗时平均 QPS成功率
单交易对 24h 数据~2,500 条10.8s3,125/s100%
10 交易对 7 天数据~180,000 条1042s4,286/s99.7%
50 交易对 30 天数据~2,500,000 条108分32秒4,878/s99.2%

关键发现:

生产级数据下载与 CSV 导出

以下代码整合了完整的数据下载流程,包含错误重试、日志记录和进度显示:

import asyncio
import aiohttp
import pandas as pd
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)

class ProductionDataDownloader:
    """生产级 Deribit BTC 期权历史数据下载器"""
    
    def __init__(self, api_key: str, output_dir: str = "./data"):
        self.api_key = api_key
        self.base_url = "https://api.tardis.dev/v1"
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.session: Optional[aiohttp.ClientSession] = None
        self.stats = {"success": 0, "failed": 0, "retried": 0}
        
    async def init_session(self):
        """初始化 HTTP 会话(复用连接)"""
        connector = aiohttp.TCPConnector(
            limit=100,
            ttl_dns_cache=300,
            keepalive_timeout=30
        )
        self.session = aiohttp.ClientSession(connector=connector)
    
    async def close(self):
        if self.session:
            await self.session.close()
    
    async def download_with_retry(
        self,
        url: str,
        params: dict,
        max_retries: int = 3,
        backoff: float = 1.0
    ) -> dict:
        """带指数退避重试的请求"""
        headers = {"Authorization": f"Bearer {self.api_key}"}
        
        for attempt in range(max_retries):
            try:
                await asyncio.sleep(0.1 * (attempt + 1))  # 简单退避
                
                async with self.session.get(
                    url, 
                    headers=headers, 
                    params=params,
                    timeout=aiohttp.ClientTimeout(total=60)
                ) as resp:
                    if resp.status == 200:
                        self.stats["success"] += 1
                        return await resp.json()
                    elif resp.status == 429:
                        # Rate limit,等待更长时间
                        logger.warning("触发 Rate Limit,等待 5 秒...")
                        await asyncio.sleep(5)
                        continue
                    elif resp.status >= 500:
                        # 服务器错误,重试
                        self.stats["retried"] += 1
                        continue
                    else:
                        self.stats["failed"] += 1
                        raise Exception(f"HTTP {resp.status}")
                        
            except aiohttp.ClientError as e:
                logger.error(f"请求失败 (尝试 {attempt+1}/{max_retries}): {e}")
                await asyncio.sleep(backoff * (2 ** attempt))
        
        raise Exception(f"重试 {max_retries} 次后仍失败")
    
    async def download_date_range(
        self,
        symbol: str,
        start_date: datetime,
        end_date: datetime,
        data_type: str = "trades"
    ) -> pd.DataFrame:
        """下载指定日期范围的数据"""
        all_data = []
        current_start = start_date
        
        while current_start < end_date:
            current_end = min(current_start + timedelta(days=1), end_date)
            
            params = {
                "symbol": symbol,
                "from": int(current_start.timestamp() * 1000),
                "to": int(current_end.timestamp() * 1000),
                "limit": 50000,
                "format": "json"
            }
            
            try:
                data = await self.download_with_retry(
                    f"{self.base_url}/derivatives/deribit/{data_type}",
                    params
                )
                
                if data and data.get(data_type):
                    all_data.extend(data[data_type])
                    logger.info(
                        f"{symbol} {current_start.date()} → "
                        f"获取 {len(data[data_type])} 条记录"
                    )
                    
            except Exception as e:
                logger.error(f"下载失败 {symbol} {current_start.date()}: {e}")
                self.stats["failed"] += 1
            
            current_start = current_end
        
        return pd.DataFrame(all_data)
    
    async def batch_download(
        self,
        symbols: list,
        start_date: datetime,
        end_date: datetime,
        data_type: str = "trades"
    ) -> dict:
        """批量下载多个交易对数据"""
        await self.init_session()
        
        tasks = [
            self.download_date_range(symbol, start_date, end_date, data_type)
            for symbol in symbols
        ]
        
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        output = {}
        for symbol, result in zip(symbols, results):
            if isinstance(result, Exception):
                logger.error(f"{symbol} 整体失败: {result}")
                output[symbol] = pd.DataFrame()
            else:
                output[symbol] = result
        
        await self.close()
        
        logger.info(
            f"下载完成 | 成功: {self.stats['success']} | "
            f"重试: {self.stats['retried']} | 失败: {self.stats['failed']}"
        )
        
        return output
    
    def save_to_csv(self, data: dict, prefix: str = "deribit"):
        """保存数据到 CSV 文件"""
        for symbol, df in data.items():
            if df.empty:
                continue
                
            # 生成安全的文件名
            safe_symbol = symbol.replace("-", "_").replace(" ", "_")
            filename = f"{prefix}_{safe_symbol}.csv"
            filepath = self.output_dir / filename
            
            df.to_csv(filepath, index=False)
            logger.info(f"已保存: {filepath} ({len(df)} 行)")

async def demo():
    """演示:下载 BTC 期权 7 天数据"""
    downloader = ProductionDataDownloader(
        api_key="YOUR_TARDIS_API_KEY",
        output_dir="./deribit_data"
    )
    
    # 选择活跃的 BTC 看涨期权
    symbols = [
        "BTC-28JUN24-65000-C",
        "BTC-28JUN24-70000-C",
        "BTC-28JUN24-60000-P",
    ]
    
    end = datetime.now()
    start = end - timedelta(days=7)
    
    data = await downloader.batch_download(
        symbols=symbols,
        start_date=start,
        end_date=end,
        data_type="trades"
    )
    
    downloader.save_to_csv(data)

if __name__ == "__main__":
    asyncio.run(demo())

CSV 数据处理与 Pandas 分析

import pandas as pd
from pathlib import Path
from datetime import datetime

def load_and_analyze_trades(csv_path: str):
    """加载 CSV 并进行基础分析"""
    df = pd.read_csv(csv_path)
    
    # 时间戳转换(Deribit 使用微秒)
    df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms")
    
    # 计算成交量统计
    print(f"=== {csv_path} 分析报告 ===")
    print(f"总成交数: {len(df):,}")
    print(f"时间范围: {df['datetime'].min()} ~ {df['datetime'].max()}")
    print(f"平均价格: ${df['price'].mean():,.4f}")
    print(f"总成交量: {df['volume'].sum():.2f} BTC")
    
    # 按小时聚合
    df.set_index("datetime", inplace=True)
    hourly = df.resample("1H").agg({
        "volume": "sum",
        "price": ["mean", "min", "max"]
    })
    
    return df, hourly

def merge_option_chain(trade_files: list) -> pd.DataFrame:
    """合并多个期权成交文件"""
    dfs = []
    
    for file in Path(".").glob("deribit_data/*.csv"):
        df = pd.read_csv(file)
        df["symbol"] = file.stem  # 从文件名提取交易对
        dfs.append(df)
    
    combined = pd.concat(dfs, ignore_index=True)
    combined["datetime"] = pd.to_datetime(combined["timestamp"], unit="ms")
    
    # 按交易对统计
    summary = combined.groupby("symbol").agg({
        "volume": ["sum", "mean", "count"],
        "price": ["mean", "std"]
    }).round(4)
    
    print("\n=== 期权链汇总 ===")
    print(summary)
    
    return combined

使用示例

df, hourly = load_and_analyze_trades("deribit_data/BTC-28JUN24-65000-C.csv")

实战经验:三个月数据下载完整方案

我曾经需要为量化团队下载过去三个月的 Deribit BTC 期权全量数据。以下是当时采用的完整方案:

关键参数配置:

# 最终生产配置
CONFIG = {
    "api_key": "YOUR_TARDIS_API_KEY",
    "max_concurrent": 10,           # 并发数
    "rate_limit": 10,               # QPS 限制
    "batch_size": 1000,             # 每批处理记录数
    "retry_max": 5,                 # 最大重试次数
    "retry_backoff": 2.0,           # 退避系数
    "output_format": "parquet",     # 输出格式
    "compression": "snappy",         # 压缩算法
}

整个项目从开发到完成耗时约 6 小时,最终成功下载超过 1200 万条 成交记录,存储空间仅占用 2.3GB。

常见错误与解决方案

错误 1:Rate Limit 429 错误

# 问题:请求过于频繁,触发 API 限流

错误日志:HTTP 429: Too Many Requests

解决方案:实现智能退避和请求队列

class SmartRateLimiter: def __init__(self, max_per_second: int = 10): self.max_per_second = max_per_second self.tokens = max_per_second self.last_update = time.time() async def acquire(self): now = time.time() elapsed = now - self.last_update self.tokens = min( self.max_per_second, self.tokens + elapsed * self.max_per_second ) self.last_update = now if self.tokens < 1: await asyncio.sleep((1 - self.tokens) / self.max_per_second) self.tokens = 0 else: self.tokens -= 1

错误 2:数据时间戳转换错误

# 问题:Deribit 使用微秒时间戳(13位),直接当作毫秒处理

导致时间显示为 1970 年代

错误代码(错误):

df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms")

正确代码:

df["datetime"] = pd.to_datetime(df["timestamp"], unit="us") # 微秒

或者手动转换:

df["datetime"] = pd.to_datetime( df["timestamp"], unit="ms" ).dt.tz_localize("UTC")

错误 3:内存溢出(OOM)

# 问题:大批量数据直接加载到内存,导致进程被杀死

解决:使用分块读取和流式处理

async def stream_to_csv(url: str, output_path: str): """流式下载,避免内存溢出""" connector = aiohttp.TCPConnector(limit=1) async with aiohttp.ClientSession(connector=connector) as session: async with session.get(url) as resp: # 分块写入磁盘 with open(output_path, "wb") as f: async for chunk in resp.content.iter_chunked(8192): f.write(chunk) # 后续分批读取处理 for chunk in pd.read_csv(output_path, chunksize=50000): process_chunk(chunk)

错误 4:会话未正确关闭

# 问题:aiohttp.ClientSession 未关闭,导致连接泄漏

解决:使用上下文管理器

async def correct_session_usage(): # 方法1:上下文管理器(推荐) async with aiohttp.ClientSession() as session: await session.get("https://api.example.com") # 自动关闭 # 方法2:try-finally session = aiohttp.ClientSession() try: await session.get("https://api.example.com") finally: await session.close() # 方法3:异步生成器 @asynccontextmanager async def get_session(): session = aiohttp.ClientSession() try: yield session finally: await session.close()

性能优化建议

成本估算

以三个月 BTC 期权全量数据为例:

对比自行维护 Deribit 连接服务(服务器 + 运维 + 故障处理),月成本节省约 70%

总结

本文详细介绍了通过 Tardis API 下载 Deribit BTC 期权历史数据的完整方案,涵盖:

核心代码可直接复制使用,关键参数已根据实战经验优化。如果需要处理更大规模的数据(如全市场期权链),建议结合 Kafka 或 Redis 构建分布式下载队列。

对于数据清洗和特征工程部分,可以考虑使用 HolySheep AI API 处理,其 GPT-4o-mini 模型成本仅 $0.15/MTok,比直接调用官方 API 节省 85% 以上费用。

👉 免费注册 HolySheep AI,获取首月赠额度