QuantStats回测起始日期异常问题的修复方案咨询
解决QuantStats回测起始日期偏移至2月的问题
问题根源
核心问题是策略函数的循环逻辑跳过了2013年1月的收益计算,加上重复的空值删除操作,导致策略收益列的1月数据全为NaN,后续的ffill填充只能从2月的第一个有效交易日开始,最终QuantStats默认从第一个非空数据点启动分析。
具体细节:
- 原策略通过
daily_returns.resample('M').last().index获取月末日期,第一个月末是2013-01-31,随后以下月初(2013-02-01)作为起始点,完全跳过1月所有交易日。 - 两次调用
dropna()直接删除了2013年1月的所有收益数据,进一步加剧了起始日期偏移。
解决方法与修改代码
重构策略逻辑,从数据覆盖的第一个月开始计算收益,同时调整数据处理步骤保留起始日期的有效数据:
import numpy as np import pandas as pd import yfinance as yf import quantstats as qs qs.extend_pandas() import matplotlib.pyplot as plt equity_etfs = ['VTI','IEF'] stock_daily_frames = [] for tick in equity_etfs: # 提前一天获取数据,确保pct_change能生成2013-01-02的有效收益 data = yf.download(tick, start="2012-12-31", end="2023-12-31", group_by='ticker', interval='1d') data = data[['Adj Close']].rename(columns={'Adj Close': tick}) stock_daily_frames.append(data) equity_daily_returns = pd.concat(stock_daily_frames, axis=1) # 仅做一次pct_change,保留第一个有效日(2013-01-02) equity_daily_returns = equity_daily_returns.pct_change() def calculate_60_40_strategy(daily_returns, fees=0, name='60/40 Strategy'): daily_returns.index = pd.to_datetime(daily_returns.index) weights = pd.Series({'VTI': 0.6, 'IEF': 0.4}) daily_returns[name] = np.nan # 获取数据覆盖的所有月份,从第一个月开始遍历 first_month = daily_returns.index.min().to_period('M') last_month = daily_returns.index.max().to_period('M') monthly_periods = pd.period_range(first_month, last_month) # 处理第一个月:无调仓手续费 first_period = monthly_periods[0] first_mask = (daily_returns.index >= first_period.start_time) & (daily_returns.index <= first_period.end_time) daily_returns.loc[first_mask, name] = daily_returns.loc[first_mask, weights.index].dot(weights) # 处理后续月份:扣除调仓手续费 for period in monthly_periods[1:]: mask = (daily_returns.index >= period.start_time) & (daily_returns.index <= period.end_time) daily_returns.loc[mask, name] = daily_returns.loc[mask, weights.index].dot(weights) * (1 - fees) # 仅填充当月内的非交易日空值,避免跨月填充逻辑错误 daily_returns[name] = daily_returns[name].fillna(method='ffill') return daily_returns fee_levels = [0, 0.01, 0.03, 0.05] for fee in fee_levels: strategy_name = f"60/40 fees={fee}" calculate_60_40_strategy(equity_daily_returns, fees=fee, name=strategy_name) # 仅删除策略列全空的行(如果有) equity_daily_returns = equity_daily_returns.dropna(subset=[col for col in equity_daily_returns.columns if '60/40' in col]) cumulative_returns = (1 + equity_daily_returns.filter(like="60/40")).cumprod() plt.figure(figsize=(12, 6)) for column in cumulative_returns.columns: plt.plot(cumulative_returns.index, cumulative_returns[column], label=column) plt.title("Cumulative Returns of 60/40 Strategy with Different Fees") plt.xlabel("Date") plt.ylabel("Cumulative Returns") plt.legend() plt.show() # 现在回测报告将从2013-01-02开始 qs.reports.html(equity_daily_returns['60/40 fees=0'], equity_daily_returns['VTI'], output="60_40_2013_2023 Strategy.html")
关键修改说明
- 提前数据起始日:将
yf.download的start设为2012-12-31,确保pct_change()能生成2013-01-02的有效收益(基于前一日收盘价计算)。 - 重构策略循环:直接遍历数据覆盖的所有月份,从第一个月(2013年1月)开始计算收益,不再跳过起始月。
- 优化空值处理:去掉重复的
dropna()调用,仅在最后过滤策略列全空的行;限制ffill填充范围,避免跨月填充导致的逻辑错误。 - 简化手续费逻辑:明确区分第一个月(无调仓手续费)和后续月份(扣除调仓手续费),逻辑更清晰。
运行修改后的代码后,策略收益的第一个有效数据点会是2013-01-02,QuantStats的回测报告也会从该日期开始分析。
内容的提问来源于stack exchange,提问作者Matteo Fratini
相关产品推荐
相关产品推荐

