You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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")

关键修改说明

  1. 提前数据起始日:将yf.download的start设为2012-12-31,确保pct_change()能生成2013-01-02的有效收益(基于前一日收盘价计算)。
  2. 重构策略循环:直接遍历数据覆盖的所有月份,从第一个月(2013年1月)开始计算收益,不再跳过起始月。
  3. 优化空值处理:去掉重复的dropna()调用,仅在最后过滤策略列全空的行;限制ffill填充范围,避免跨月填充导致的逻辑错误。
  4. 简化手续费逻辑:明确区分第一个月(无调仓手续费)和后续月份(扣除调仓手续费),逻辑更清晰。

运行修改后的代码后,策略收益的第一个有效数据点会是2013-01-02,QuantStats的回测报告也会从该日期开始分析。

内容的提问来源于stack exchange,提问作者Matteo Fratini

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 06:02:32