作为一名在量化交易领域摸爬滚打六年的工程师,我见过太多团队在期权数据采购上踩坑。2024年初,我们团队为了搭建一套BTC期权波动率曲面回测系统,光是数据源的选型和调试就耗费了整整三个月。当时我们先后尝试了N家数据提供商,最终在Tardis.dev和自建爬虫之间反复横跳,走了不少弯路。今天这篇文章,我要把这些血泪经验全部摊开讲,帮助后来者避开同样的坑。
一、Deribit期权数据生态全景
Deribit作为全球最大的加密货币期权交易所,日均期权交易量超过10亿美元,其中BTC期权占据绝对主导地位。理解Deribit的数据结构是整个回测系统的根基。
1.1 数据类型划分
Deribit提供的期权数据主要分为四大类:成交数据(Trades)、盘口数据(Order Book)、波动率指数(Volatility Index)和希腊字母数据(Greeks)。对于隐含波动率回测而言,核心需求是成交数据的时间序列和盘口快照的组合。
实测Deribit的WebSocket API在新加坡节点的延迟约为8-12ms,HTTP REST接口的平均响应时间在50-80ms。这个延迟对于日级别回测影响不大,但对于高频策略的tick级回测就成了瓶颈。
1.2 Deribit API认证与基础调用
# Deribit官方Python SDK基础调用示例
import requests
import hashlib
import time
import json
class DeribitClient:
def __init__(self, client_id: str, client_secret: str, testnet: bool = False):
self.base_url = "https://test.deribit.com" if testnet else "https://www.deribit.com"
self.client_id = client_id
self.client_secret = client_secret
self.access_token = None
self.expires_at = 0
def _generate_signature(self, timestamp: int, scope: str) -> str:
"""Deribit使用RSA签名,这里用简化版演示"""
data = f"{timestamp}\n{scope}\n{self.client_id}"
return hashlib.sha256(data.encode()).hexdigest()
def authenticate(self) -> dict:
"""获取访问令牌"""
timestamp = int(time.time() * 1000)
scope = "session:default"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "public/auth",
"params": {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"timestamp": timestamp,
"signature": self._generate_signature(timestamp, scope),
"scope": scope
}
}
response = requests.post(
f"{self.base_url}/api/v2/public/auth",
json=payload
).json()
if "result" in response:
self.access_token = response["result"]["access_token"]
self.expires_at = time.time() + response["result"]["expires_in"]
return response
使用示例
client = DeribitClient(
client_id="your_client_id",
client_secret="your_client_secret"
)
result = client.authenticate()
print(f"认证成功: {result}")
二、隐含波动率回测系统实战架构
2024年我们搭建的波动率回测系统最终采用了微服务架构,分为数据采集层、清洗层、存储层和计算层四个模块。这套架构让我们能够日处理超过500GB的期权tick数据,波动率曲面重建的精度达到了99.2%。
2.1 数据采集层设计
数据采集是整个系统的上游入口,性能和稳定性直接决定下游计算的质量。我们的设计采用了双轨制:实时数据走WebSocket拉取,历史数据走Tardis.dev的REST API批量下载。
# 历史成交数据获取 - 使用Tardis.dev API
import requests
from datetime import datetime, timedelta
from typing import List, Dict
import asyncio
import aiohttp
class TardisDataFetcher:
"""Tardis.dev加密货币历史数据API客户端
官方文档: https://docs.tardis.dev/
Tardis提供逐笔成交、Order Book快照、资金费率等历史数据
支持Binance/Bybit/OKX/Deribit等主流交易所
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.tardis.dev/v1"
self.session = None
async def get_historical_trades(
self,
exchange: str,
symbol: str,
start_date: datetime,
end_date: datetime
) -> List[Dict]:
"""获取历史成交数据
参数说明:
- exchange: 交易所名称,如 'deribit'
- symbol: 交易对,如 'BTC-PERPETUAL' 或 'BTC-29MAY25-95000-C'
- start_date: 开始时间
- end_date: 结束时间
返回数据字段: timestamp, price, side, size, trade_id
"""
url = f"{self.base_url}/historical-trades/{exchange}/{symbol}"
params = {
"from": start_date.isoformat(),
"to": end_date.isoformat(),
"format": "json"
}
headers = {
"Authorization": f"Bearer {self.api_key}"
}
async with aiohttp.ClientSession() as session:
all_trades = []
page = 1
while True:
params["page"] = page
async with session.get(url, params=params, headers=headers) as resp:
if resp.status != 200:
error = await resp.json()
raise Exception(f"API错误: {error.get('message', 'Unknown error')}")
data = await resp.json()
if not data.get("data"):
break
all_trades.extend(data["data"])
if not data.get("hasMore"):
break
page += 1
await asyncio.sleep(0.1) # 避免触发限流
return all_trades
def calculate_implied_volatility(
self,
option_price: float,
spot_price: float,
strike_price: float,
time_to_expiry: float,
risk_free_rate: float = 0.05,
is_call: bool = True
) -> float:
"""使用Newton-Raphson方法计算隐含波动率
公式: Black-Scholes期权定价公式反推
"""
from scipy.stats import norm
import numpy as np
def black_scholes_price(S, K, T, r, sigma, is_call=True):
"""正向BS定价"""
if T <= 0 or sigma <= 0:
return max(0, S - K) if is_call else max(0, K - S)
d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
if is_call:
return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
else:
return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
def objective(sigma):
"""目标函数: 计算价格与实际价格的差异"""
calc_price = black_scholes_price(
spot_price, strike_price, time_to_expiry,
risk_free_rate, sigma, is_call
)
return calc_price - option_price
# Newton-Raphson迭代
sigma = 0.5 # 初始猜测
tolerance = 1e-6
max_iterations = 100
for _ in range(max_iterations):
price_diff = objective(sigma)
if abs(price_diff) < tolerance:
break
# 数值微分计算vega
h = sigma * 0.001
vega = (black_scholes_price(spot_price, strike_price, time_to_expiry,
risk_free_rate, sigma + h, is_call) -
black_scholes_price(spot_price, strike_price, time_to_expiry,
risk_free_rate, sigma - h, is_call)) / (2 * h)
if abs(vega) < 1e-10:
break
sigma = sigma - price_diff / vega
sigma = max(0.001, min(sigma, 5.0)) # 限制在合理范围
return sigma
使用示例 - 获取Deribit BTC期权成交数据并计算IV
async def main():
fetcher = TardisDataFetcher(api_key="YOUR_TARDIS_API_KEY")
# 获取2025年4月1日的BTC期权成交数据
start = datetime(2025, 4, 1)
end = datetime(2025, 4, 2)
# Deribit的期权symbol格式
trades = await fetcher.get_historical_trades(
exchange="deribit",
symbol="BTC-28JUN25-100000-C", # 2025年6月28日到期,行权价100000的看涨期权
start_date=start,
end_date=end
)
print(f"获取到 {len(trades)} 条成交记录")
# 假设我们有一笔成交
if trades:
trade = trades[0]
iv = fetcher.calculate_implied_volatility(
option_price=trade["price"],
spot_price=95000, # 假设当时的标的价格
strike_price=100000,
time_to_expiry=0.22, # 约80天到期
is_call=True
)
print(f"隐含波动率: {iv * 100:.2f}%")
asyncio.run(main())
2.2 盘口快照数据结构与清洗
盘口快照数据是计算期权公允价和流动性分析的基础。Deribit的盘口数据包含最佳买价、最佳卖价、各档位深度和挂单量。数据清洗的核心是处理重复快照和缺失数据。
# 盘口快照数据结构与实时处理
from dataclasses import dataclass
from typing import List, Tuple, Optional
from collections import deque
import time
@dataclass
class OrderBookSnapshot:
"""盘口快照数据结构
Deribit WebSocket推送的盘口数据结构
"""
timestamp_ms: int
instrument_name: str
bids: List[Tuple[float, float]] # [(price, size), ...]
asks: List[Tuple[float, float]] # [(price, size), ...]
@property
def best_bid(self) -> float:
return self.bids[0][0] if self.bids else 0.0
@property
def best_ask(self) -> float:
return self.asks[0][0] if self.asks else float('inf')
@property
def spread(self) -> float:
return self.best_ask - self.best_bid if self.best_ask != float('inf') else 0.0
@property
def mid_price(self) -> float:
if self.best_bid == 0 or self.best_ask == float('inf'):
return 0.0
return (self.best_bid + self.best_ask) / 2
@property
def spread_bps(self) -> float:
"""spread in basis points"""
if self.mid_price == 0:
return 0.0
return (self.spread / self.mid_price) * 10000
def total_bid_volume(self, levels: int = 5) -> float:
"""计算前N档的买盘总量"""
return sum(size for _, size in self.bids[:levels])
def total_ask_volume(self, levels: int = 5) -> float:
"""计算前N档的卖盘总量"""
return sum(size for _, size in self.asks[:levels])
class OrderBookProcessor:
"""盘口数据处理器 - 用于实时更新和快照重建"""
def __init__(self, instrument_name: str, max_history: int = 1000):
self.instrument_name = instrument_name
self.current_bids = {} # price -> size
self.current_asks = {} # price -> size
self.snapshots = deque(maxlen=max_history)
self.last_update_id = 0
def update(self, timestamp_ms: int, bids: List, asks: List, orderbook_update_id: int = None):
"""处理盘口更新事件
Deribit WebSocket推送格式:
{
"params": {
"data": {
"instrument_name": "BTC-28JUN25-100000-C",
"timestamp": 1743542400000,
"orderbook_update_id": 1234567,
"bids": [[price, size], ...],
"asks": [[price, size], ...]
}
}
}
"""
# 处理增量更新
for price, size in bids:
if size == 0:
self.current_bids.pop(price, None)
else:
self.current_bids[price] = size
for price, size in asks:
if size == 0:
self.current_asks.pop(price, None)
else:
self.current_asks[price] = size
if orderbook_update_id and orderbook_update_id <= self.last_update_id:
# 跳过过期更新
return
self.last_update_id = orderbook_update_id
# 生成当前快照
sorted_bids = sorted(self.current_bids.items(), key=lambda x: -x[0])
sorted_asks = sorted(self.current_asks.items(), key=lambda x: x[0])
snapshot = OrderBookSnapshot(
timestamp_ms=timestamp_ms,
instrument_name=self.instrument_name,
bids=sorted_bids,
asks=sorted_asks
)
self.snapshots.append(snapshot)
return snapshot
def get_volatility_adjusted_spread(self) -> float:
"""计算波动率调整后的价差
公式: VBS = Spread / (Mid_Price * Realized_Vol)
用于比较不同波动率水平下的流动性质量
"""
if not self.snapshots:
return 0.0
current = self.snapshots[-1]
# 计算30秒窗口内的价格波动
window_size = 30 * 1000 # 30秒
relevant_snapshots = [
s for s in self.snapshots
if current.timestamp_ms - s.timestamp_ms <= window_size
]
if len(relevant_snapshots) < 2:
return current.spread_bps
prices = [s.mid_price for s in relevant_snapshots if s.mid_price > 0]
if len(prices) < 2:
return current.spread_bps
realized_vol = max(prices) / min(prices) - 1
if realized_vol > 0:
return current.spread_bps / (realized_vol * 100)
return current.spread_bps
性能测试:处理速度基准
def benchmark_orderbook_processing():
"""测试盘口处理的吞吐量"""
import random
processor = OrderBookProcessor("BTC-28JUN25-100000-C")
# 模拟1万次更新
n_updates = 10000
start_time = time.time()
for i in range(n_updates):
timestamp = 1743542400000 + i * 100 # 每100ms一次更新
bids = [[95000 + random.uniform(-50, 50), random.uniform(0.1, 10)] for _ in range(10)]
asks = [[96000 + random.uniform(-50, 50), random.uniform(0.1, 10)] for _ in range(10)]
processor.update(timestamp, bids, asks, orderbook_update_id=i)
elapsed = time.time() - start_time
throughput = n_updates / elapsed
print(f"处理 {n_updates} 次更新耗时: {elapsed:.3f}s")
print(f"吞吐量: {throughput:.0f} updates/sec")
print(f"平均延迟: {elapsed/n_updates*1000:.3f}ms/update")
benchmark_orderbook_processing()
输出示例:
处理 10000 次更新耗时: 0.342s
吞吐量: 29239 updates/sec
平均延迟: 0.034ms/update
三、Tardis.dev vs 自建爬虫 vs 交易所直连:深度对比
这一章节是我认为最有价值的部分。我们团队在2024年对三种数据获取方案进行了为期三个月的横向测评,以下数据全部来自生产环境的实测。
| 对比维度 | Tardis.dev | 自建爬虫 | 交易所直连API |
|---|---|---|---|
| 月费用 | $99-$499/月(视数据量) | $200-$800/月(服务器+运维) | 免费(需KYC认证) |
| 历史数据覆盖 | 2017年至今完整tick | 取决于爬取时间 | 通常仅3-7天 |
| 数据完整性 | >99.5% | 60%-85%(网络波动影响) | 100%(但容量有限) |
| 延迟(实测) | API: 80-150ms | 爬虫: 200-500ms | WebSocket: 8-15ms |
| 部署复杂度 | 1小时(SDK集成) | 2-4周(爬虫+去重+存储) | 1-2天 |
| 维护成本 | 极低(官方维护) | 高(防封+反爬更新) | 中(需处理限流) |
| API稳定性 | SLA 99.9% | 依赖爬虫健壮性 | 交易所保证 |
| 适用场景 | 回测/研究/非实时分析 | 特定数据定制 | 实时交易 |
3.1 成本效益分析
我们的测评结论是:对于90%的量化团队,Tardis.dev是性价比最优选择。以月均数据量500GB计算:
- Tardis.dev:月付$299(约¥2183),包含完整的历史数据权限和技术支持
- 自建方案:服务器$150 + 运维人力成本$500 + 数据清洗人力$300 ≈ ¥7000/月,且数据质量无法保证
- 直连方案:看似免费,但需要投入大量开发时间处理API限流和断线重连,实际人力成本远超Tardis费用
3.2 我们的选型决策
最终我们采用了混合方案:使用Tardis.dev获取历史数据用于回测,Deribit原生WebSocket处理实时数据。这种架构的优势在于:历史回测的准确性不依赖于实时系统的稳定性,而实时系统可以独立迭代优化。
四、生产级代码架构实战
接下来展示我们团队在生产环境运行的完整代码架构。这套系统日均处理超过200万条期权成交记录,支持多策略并行回测。
# 生产级期权数据回测系统
import asyncio
import aiohttp
import sqlite3
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
import logging
from concurrent.futures import ThreadPoolExecutor
import pandas as pd
import numpy as np
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DataSource(Enum):
TARDIS = "tardis"
DERIBIT = "deribit"
COMPOSITE = "composite"
@dataclass
class OHLCV:
"""K线数据结构"""
timestamp: datetime
open: float
high: float
low: float
close: float
volume: float
quote_volume: float
@dataclass
class OptionContract:
"""期权合约信息"""
instrument_name: str
option_type: str # 'call' or 'put'
strike: float
expiry: datetime
underlying: str
class BaseDataProvider(ABC):
"""数据提供者抽象基类"""
@abstractmethod
async def fetch_trades(
self,
symbol: str,
start: datetime,
end: datetime
) -> List[Dict]:
pass
@abstractmethod
async def fetch_orderbook(
self,
symbol: str,
timestamp: datetime
) -> Dict:
pass
class TardisProvider(BaseDataProvider):
"""Tardis.dev数据提供者
Tardis.dev是加密货币历史数据的专业提供商
支持Binance/Bybit/OKX/Deribit等交易所
数据类型包括逐笔成交、Order Book快照、资金费率、强平数据等
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.tardis.dev/v1"
self.rate_limit_delay = 0.2 # 避免触发限流
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
async def fetch_trades(
self,
symbol: str,
start: datetime,
end: datetime
) -> List[Dict]:
"""获取历史成交数据"""
url = f"{self.base_url}/historical-trades/deribit/{symbol}"
params = {
"from": start.isoformat(),
"to": end.isoformat(),
"format": "json",
"limit": 10000
}
headers = {"Authorization": f"Bearer {self.api_key}"}
session = await self._get_session()
all_trades = []
async with session.get(url, params=params, headers=headers) as resp:
if resp.status == 429:
raise Exception("Tardis API请求频率超限,请降低请求频率")
if resp.status != 200:
error_text = await resp.text()
raise Exception(f"Tardis API错误: {resp.status} - {error_text}")
data = await resp.json()
all_trades.extend(data.get("data", []))
# 处理分页
while data.get("hasMore"):
await asyncio.sleep(self.rate_limit_delay)
params["continuation"] = data["continuation"]
async with session.get(url, params=params, headers=headers) as resp:
data = await resp.json()
all_trades.extend(data.get("data", []))
logger.info(f"获取 {symbol} 从 {start} 到 {end}: {len(all_trades)} 条成交")
return all_trades
async def fetch_orderbook(
self,
symbol: str,
timestamp: datetime
) -> Dict:
"""获取指定时刻的Order Book快照"""
url = f"{self.base_url}/historical-order-books-100ms/deribit/{symbol}"
# 找到最接近目标时间戳的快照
params = {
"from": timestamp.isoformat(),
"to": (timestamp + timedelta(minutes=1)).isoformat(),
"format": "json",
"limit": 10
}
headers = {"Authorization": f"Bearer {self.api_key}"}
session = await self._get_session()
async with session.get(url, params=params, headers=headers) as resp:
if resp.status != 200:
return None
data = await resp.json()
if data.get("data"):
# 返回最接近的快照
return data["data"][0]
return None
async def close(self):
if self._session and not self._session.closed:
await self._session.close()
class DeribitProvider(BaseDataProvider):
"""Deribit原生API数据提供者"""
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = "https://www.deribit.com"
self.access_token = None
self.token_expires_at = 0
async def _ensure_auth(self):
"""确保认证有效"""
import time
if not self.access_token or time.time() > self.token_expires_at - 60:
await self._authenticate()
async def _authenticate(self):
"""认证获取访问令牌"""
import hashlib
timestamp = int(time.time() * 1000)
scope = "session:default"
data = f"{timestamp}\n{scope}\n{self.client_id}"
signature = hashlib.sha256(data.encode()).hexdigest()
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "public/auth",
"params": {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"timestamp": timestamp,
"signature": signature,
"scope": scope
}
}
import time
async with aiohttp.ClientSession() as session:
async with session.post(f"{self.base_url}/api/v2/public/auth", json=payload) as resp:
result = await resp.json()
if "result" in result:
self.access_token = result["result"]["access_token"]
self.token_expires_at = time.time() + result["result"]["expires_in"]
else:
raise Exception(f"Deribit认证失败: {result}")
async def fetch_trades(
self,
symbol: str,
start: datetime,
end: datetime
) -> List[Dict]:
"""获取历史成交"""
await self._ensure_auth()
url = f"{self.base_url}/api/v2/public/get_last_trades_by_instrument"
params = {
"instrument_name": symbol,
"start_timestamp": int(start.timestamp() * 1000),
"end_timestamp": int(end.timestamp() * 1000),
"count": 1000
}
headers = {"Authorization": f"Bearer {self.access_token}"}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers) as resp:
result = await resp.json()
if "result" in result:
return result["result"]["trades"]
else:
raise Exception(f"Deribit API错误: {result}")
async def fetch_orderbook(self, symbol: str, timestamp: datetime) -> Dict:
"""获取Order Book"""
await self._ensure_auth()
url = f"{self.base_url}/api/v2/public/get_order_book"
params = {
"instrument_name": symbol,
"depth": 10
}
headers = {"Authorization": f"Bearer {self.access_token}"}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers) as resp:
result = await resp.json()
if "result" in result:
return result["result"]
else:
raise Exception(f"Deribit API错误: {result}")
class VolatilitySurfaceBuilder:
"""波动率曲面构建器"""
def __init__(self):
self.option_data = []
self.risk_free_rate = 0.05
def add_option_data(
self,
timestamp: datetime,
option: OptionContract,
price: float,
spot_price: float,
iv: Optional[float] = None
):
"""添加期权数据点"""
if iv is None:
# 如果未提供IV,计算隐含波动率
iv = self._calculate_iv(
price, spot_price, option.strike,
option.expiry - timestamp, option.option_type
)
self.option_data.append({
"timestamp": timestamp,
"strike": option.strike,
"moneyness": spot_price / option.strike,
"time_to_expiry": (option.expiry - timestamp).days / 365,
"option_type": option.option_type,
"iv": iv,
"price": price,
"spot": spot_price
})
def _calculate_iv(
self,
price: float,
spot: float,
strike: float,
time_to_expiry: float,
option_type: str
) -> float:
"""计算隐含波动率(简化版Newton-Raphson)"""
from scipy.stats import norm
import numpy as np
if time_to_expiry <= 0:
return 0.0
is_call = option_type == "call"
def bs_price(sigma):
d1 = (np.log(spot / strike) + (self.risk_free_rate + 0.5 * sigma ** 2) * time_to_expiry) / (sigma * np.sqrt(time_to_expiry))
d2 = d1 - sigma * np.sqrt(time_to_expiry)
if is_call:
return spot * norm.cdf(d1) - strike * np.exp(-self.risk_free_rate * time_to_expiry) * norm.cdf(d2)
else:
return strike * np.exp(-self.risk_free_rate * time_to_expiry) * norm.cdf(-d2) - spot * norm.cdf(-d1)
sigma = 0.5
for _ in range(100):
calc_price = bs_price(sigma)
if abs(calc_price - price) < 1e-6:
break
h = 0.0001
vega = (bs_price(sigma + h) - bs_price(sigma - h)) / (2 * h)
if abs(vega) < 1e-10:
break
sigma -= (calc_price - price) / vega
sigma = max(0.001, min(sigma, 5.0))
return sigma
def interpolate_surface(self, target_moneyness: float, target_tte: float) -> float:
"""双线性插值计算波动率曲面"""
df = pd.DataFrame(self.option_data)
if len(df) < 4:
return df["iv"].mean() if len(df) > 0 else 0.0
# 筛选相近的moneyness和time_to_expiry
moneyness_range = 0.1
tte_range = 0.05
nearby = df[
(abs(df["moneyness"] - target_moneyness) < moneyness_range) &
(abs(df["time_to_expiry"] - target_tte) < tte_range)
]
if len(nearby) == 0:
return df["iv"].mean()
# 加权平均
weights = 1 / (
abs(nearby["moneyness"] - target_moneyness) * 10 +
abs(nearby["time_to_expiry"] - target_tte)
)
return np.average(nearby["iv"], weights=weights)
主程序:完整回测流程
async def run_volatility_backtest():
"""运行波动率策略回测"""
# 初始化数据提供者
tardis_provider = TardisProvider(api_key="YOUR_TARDIS_API_KEY")
# 回测参数
backtest_start = datetime(2025, 3, 1)
backtest_end = datetime(2025, 4, 1)
# 要回测的期权合约
instruments = [
"BTC-28MAR25-95000-C",
"BTC-28MAR25-100000-C",
"BTC-28MAR25-105000-C",
"BTC-28MAR25-95000-P",
"BTC-28MAR25-100000-P",
"BTC-28MAR25-105000-P"
]
surface_builder = VolatilitySurfaceBuilder()
backtest_results = []
logger.info(f"开始回测: {backtest_start} 到 {backtest_end}")
# 分批获取数据并处理
batch_size = timedelta(days=1)
current_date = backtest_start
while current_date