回测曲线漂亮得像整容脸?十有八九是过拟合整出来的。
上一章。
你搞了个策略。
回测——年化80%。
你飘了。
发朋友圈。
截图。
配文:"价值投资的尽头是量化。"
呵呵。
充了钱。
上了实盘。
一个月——还行。
第二个月——微亏。
第三个月——
亏了30%。
操。
怎么回事?
你回去翻代码。
翻回测。
翻参数。
答案就两个字——
过拟合。
你品品。
回测的时候,你调了多少参数?
持有天数?止损比例?均线周期?入场阈值?RSI超卖线?ATR倍数?
调了十遍?二十遍?五十遍?
每调一次,你就离真理远了一步。
为什么?
因为你在拟合历史。
不是在发现规律。
这两件事长得一样。
但本质完全不同。
拟合历史——是拿放大镜找"过去发生了什么"。
发现规律——是找到"未来大概率还会发生什么"。
你拿着放大镜在K线图上找最优参数。
找到了。
年化80%。
夏普2.5。
但这参数为什么好?
因为它恰好适配了这段数据里的噪音。
不是因为它捕捉到了什么永恒不变的规律。
回测是开卷考试。参数调得越多,你越像抄答案的好学生——合上卷子就是废柴。
一、参数敏感性:动一个参数,结果崩了
先说什么叫参数敏感性。
你的策略——
持有5天。
止损3%。
MA20金叉MA60入场。
回测年化45%。漂亮。
但你有没有想过——
凭什么持有5天?
持有4天呢?6天呢?
凭什么止损3%?
2%呢?4%呢?
凭什么MA20和MA60?
MA19和MA59呢?
你从来没问过这些问题。
你只看到了"最优参数"。
然后就直接上了。
现在,改一个数字。
结果差一半。
那这个策略就是垃圾。
参数敏感性高 = 过拟合。
为什么?
因为你调出来的"最优参数",不过是恰好适配了那段历史数据里的噪音分布。
那个5天,那个3%,那个20和60——
它们不是"规律"。
它们是"巧合"。
往前挪一点——崩。
往后挪一点——崩。
这说明什么?
说明那个参数根本不是市场规律。
是噪音的产物。
你拟合了噪音。
就像你背下了一套答案。
考试出了原题——满分。
换个数字——零分。
看出区别没?
真正的规律,是稳健的。
不挑参数。
持有4天、5天、6天——都能赚。
止损2%、3%、4%——都能活。
MA15到MA30——都不差。
这才叫规律。
为什么?
因为规律反映的是市场结构。
不是某段数据的特殊性。
市场结构不会因为你的参数从5变成4就消失了。
如果消失了——说明那不是规律。
好策略不怕你动参数。
烂策略一碰就碎。
来,看代码。
用真实数据跑一遍。
// python
# pip install akshare pandas numpy matplotlib tqdm
import akshare as ak
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import warnings
warnings.filterwarnings('ignore')
# ========== 获取数据 ==========
# 用沪深300ETF做演示
df = ak.fund_etf_hist_em(symbol="510300", adjust="qfq")
df['日期'] = pd.to_datetime(df['日期'])
df = df.sort_values('日期').reset_index(drop=True)
close = df['收盘'].values
dates = df['日期'].values
# ========== 策略函数 ==========
def backtest(close_prices, dates, hold_days, stop_loss, ma_fast, ma_slow):
"""
均线交叉 + 持有N天 + 止损策略
hold_days: 买入后持有天数
stop_loss: 止损比例(如0.03表示3%)
ma_fast: 快线周期
ma_slow: 慢线周期
返回:年化收益率、最大回撤、交易次数
"""
n = len(close_prices)
ma_f = pd.Series(close_prices).rolling(ma_fast).mean().values
ma_s = pd.Series(close_prices).rolling(ma_slow).mean().values
capital = 100000.0
peak = capital
max_dd = 0
trades = 0
position = False
i = ma_slow # 从慢线能计算出来开始
while i < n - 1:
if not position:
# 金叉入场
if ma_f[i] > ma_s[i] and ma_f[i-1] <= ma_s[i-1]:
entry_price = close_prices[i]
buy_date_idx = i
position = True
trades += 1
else:
days_held = i - buy_date_idx
pnl = (close_prices[i] - entry_price) / entry_price
# 止损 or 持有到期
if pnl <= -stop_loss or days_held >= hold_days:
capital *= (1 + pnl)
position = False
# 更新最大回撤
peak = max(peak, capital)
dd = (peak - capital) / peak
max_dd = max(max_dd, dd)
i += 1
total_return = capital / 100000 - 1
years = (dates[-1] - dates[0]) / np.timedelta64(365, 'D')
if years > 0 and capital > 0:
annual_return = (capital / 100000) ** (1 / years) - 1
else:
annual_return = -1
return annual_return, max_dd, trades
# ========== 参数敏感性分析 ==========
# 固定其他参数,每次只动一个
base_params = {
'hold_days': 5,
'stop_loss': 0.03,
'ma_fast': 20,
'ma_slow': 60
}
results = {}
# 测试持有天数:1到15天
print("=" * 60)
print("【持有天数敏感性】")
print(f"{'天数':>6} | {'年化收益':>10} | {'最大回撤':>10} | {'交易次数':>8}")
print("-" * 60)
hold_results = {}
for hd in range(1, 16):
ret, dd, tr = backtest(close, dates, hd, 0.03, 20, 60)
hold_results[hd] = ret
print(f"{hd:>6} | {ret:>10.2%} | {dd:>10.2%} | {tr:>8}")
# 测试止损比例:1%到8%
print("\n" + "=" * 60)
print("【止损比例敏感性】")
print(f"{'止损%':>6} | {'年化收益':>10} | {'最大回撤':>10} | {'交易次数':>8}")
print("-" * 60)
sl_results = {}
for sl in [0.01, 0.015, 0.02, 0.025, 0.03, 0.04, 0.05, 0.06, 0.08]:
ret, dd, tr = backtest(close, dates, 5, sl, 20, 60)
sl_results[sl] = ret
print(f"{sl*100:>5.1f}% | {ret:>10.2%} | {dd:>10.2%} | {tr:>8}")
# 测试均线周期
print("\n" + "=" * 60)
print("【均线周期敏感性】")
print(f"{'快线':>6} | {'慢线':>6} | {'年化收益':>10} | {'最大回撤':>10}")
print("-" * 60)
ma_results = {}
for fast in [5, 10, 15, 20, 25, 30]:
for slow in [40, 50, 60, 80, 100, 120]:
if fast >= slow:
continue
ret, dd, tr = backtest(close, dates, 5, 0.03, fast, slow)
ma_results[(fast, slow)] = ret
print(f"{fast:>6} | {slow:>6} | {ret:>10.2%} | {dd:>10.2%}")
# ========== 可视化 ==========
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# 持有天数
axes[0].plot(list(hold_results.keys()), list(hold_results.values()), 'o-', color='red', linewidth=2)
axes[0].set_title('Hold Days Sensitivity', fontsize=14)
axes[0].set_xlabel('Hold Days')
axes[0].set_ylabel('Annual Return')
axes[0].axhline(y=0, color='gray', linestyle='--')
# 止损比例
axes[1].plot([x*100 for x in sl_results.keys()], list(sl_results.values()), 's-', color='blue', linewidth=2)
axes[1].set_title('Stop Loss Sensitivity', fontsize=14)
axes[1].set_xlabel('Stop Loss %')
axes[1].set_ylabel('Annual Return')
axes[1].axhline(y=0, color='gray', linestyle='--')
# 均线热力图
fast_vals = sorted(set([k[0] for k in ma_results.keys()]))
slow_vals = sorted(set([k[1] for k in ma_results.keys()]))
heatmap_data = np.full((len(fast_vals), len(slow_vals)), np.nan)
for i, f in enumerate(fast_vals):
for j, s in enumerate(slow_vals):
if (f, s) in ma_results:
heatmap_data[i, j] = ma_results[(f, s)]
im = axes[2].imshow(heatmap_data, cmap='RdYlGn', aspect='auto')
axes[2].set_xticks(range(len(slow_vals)))
axes[2].set_xticklabels(slow_vals)
axes[2].set_yticks(range(len(fast_vals)))
axes[2].set_yticklabels(fast_vals)
axes[2].set_xlabel('Slow MA')
axes[2].set_ylabel('Fast MA')
axes[2].set_title('MA Period Heatmap', fontsize=14)
plt.colorbar(im, ax=axes[2], shrink=0.8)
plt.tight_layout()
plt.savefig('parameter_sensitivity.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n✅ 参数敏感性分析完成,图片已保存")
跑完你看——
持有天数从3天到7天,收益可能从+20%蹦到-15%。
止损从2%到4%,收益从-10%跳到+30%。
热力图更直观。
红色一片。
绿色一片。
相邻格子差了一倍。
这他妈叫策略?
这叫赌博。
参数敏感性这么高的策略,上实盘等于送钱。
你知道为什么吗?
因为市场在未来呈现的参数组合——
大概率不恰好是你回测出来的那个"最优值"。
而在"最优值"旁边的参数,收益可能断崖式下跌。
参数动一动就变脸的,不是策略,是算命。
二、样本外检验:训练集和测试集
过拟合最经典的检验方法。
做机器学习的人都知道——
数据要分两份。
训练集:用来调参数。用来"学习"。
测试集:用来验证。用来"考试"。
训练集上表现好,那叫"学会了"。
测试集上表现好,那叫"真学会了"。
训练集上好、测试集上崩——过拟合。实锤。没救。
量化也一样。
一个残酷的事实——
你回测用的所有数据,本质上都是"训练集"。
因为你看到了数据,然后据此调整了参数。
你调参数的过程,就是在"训练"。
那检验怎么办?
留一段数据不碰。
绝对不碰。
不看。
不调。
碰都不碰一下。
你用2015年到2021年的数据调参数。
那2022年到2025年的数据,锁在抽屉里。
等参数定死了。
打开抽屉。
拿2022-2025的数据跑一遍。
如果年化收益跟训练集差不多——真策略。
如果年化收益断崖下跌——过拟合,拜拜。
这叫样本外检验。
也叫留一手法。
代码来了。
// python
# pip install akshare pandas numpy matplotlib
import akshare as ak
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
# ========== 获取数据 ==========
df = ak.fund_etf_hist_em(symbol="510300", adjust="qfq")
df['日期'] = pd.to_datetime(df['日期'])
df = df.sort_values('日期').reset_index(drop=True)
close = df['收盘'].values
dates = df['日期'].values
# ========== 策略函数 ==========
def backtest_detail(close_prices, dates, hold_days, stop_loss, ma_fast, ma_slow):
"""返回:年化收益、最大回撤、交易次数、每日净值曲线"""
n = len(close_prices)
ma_f = pd.Series(close_prices).rolling(ma_fast).mean().values
ma_s = pd.Series(close_prices).rolling(ma_slow).mean().values
capital = 100000.0
nav = [100000.0] # 净值曲线
peak = capital
max_dd = 0
trades = 0
position = False
for i in range(1, n):
if not position and i > ma_slow:
if ma_f[i] > ma_s[i] and ma_f[i-1] <= ma_s[i-1]:
entry_price = close_prices[i]
buy_idx = i
position = True
trades += 1
elif position:
days_held = i - buy_idx
pnl = (close_prices[i] - entry_price) / entry_price
if pnl <= -stop_loss or days_held >= hold_days:
capital *= (1 + pnl)
position = False
peak = max(peak, capital)
dd = (peak - capital) / peak
max_dd = max(max_dd, dd)
nav.append(capital)
total_return = capital / 100000 - 1
years = (dates[-1] - dates[0]) / np.timedelta64(365, 'D')
annual_return = (capital / 100000) ** (1 / max(years, 0.01)) - 1 if capital > 0 else -1
return annual_return, max_dd, trades, nav
# ========== 样本外检验 ==========
# 前70%训练,后30%测试
split_idx = int(len(close) * 0.7)
train_close = close[:split_idx]
test_close = close[split_idx:]
train_dates = dates[:split_idx]
test_dates = dates[split_idx:]
print(f"总数据量: {len(close)} 天")
print(f"训练集: {dates[0]} ~ {train_dates[-1]} ({len(train_close)} 天)")
print(f"测试集: {test_dates[0]} ~ {dates[-1]} ({len(test_close)} 天)")
print()
# 用训练集跑
params = {'hold_days': 5, 'stop_loss': 0.03, 'ma_fast': 20, 'ma_slow': 60}
ret_train, dd_train, tr_train, nav_train = backtest_detail(
train_close, train_dates, **params
)
ret_test, dd_test, tr_test, nav_test = backtest_detail(
test_close, test_dates, **params
)
print("=" * 50)
print(" 样本外检验结果")
print("=" * 50)
print(f"{'指标':>10} | {'训练集':>12} | {'测试集':>12}")
print("-" * 50)
print(f"{'年化收益':>10} | {ret_train:>12.2%} | {ret_test:>12.2%}")
print(f"{'最大回撤':>10} | {dd_train:>12.2%} | {dd_test:>12.2%}")
print(f"{'交易次数':>10} | {tr_train:>12} | {tr_test:>12}")
print()
# 衰减率
if ret_train != 0:
decay = 1 - ret_test / ret_train
print(f"收益衰减率: {decay:.1%}")
if decay > 0.5:
print("⚠️ 警告:收益衰减超过50%,策略大概率过拟合!")
elif decay > 0.3:
print("⚡ 注意:收益衰减30%-50%,需要谨慎。")
else:
print("✅ 衰减在可接受范围内。")
# ========== 可视化 ==========
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
# 净值曲线
full_nav = nav_train + nav_test[1:]
axes[0].plot(range(len(full_nav)), full_nav, color='black', linewidth=1.5, label='Full Period')
axes[0].axvline(x=split_idx, color='red', linestyle='--', linewidth=2, label='Train/Test Split')
axes[0].fill_between(range(split_idx), 0, max(full_nav)*1.1, alpha=0.05, color='blue', label='Training')
axes[0].fill_between(range(split_idx, len(full_nav)), 0, max(full_nav)*1.1, alpha=0.05, color='green', label='Testing')
axes[0].set_title('Equity Curve: In-Sample vs Out-of-Sample', fontsize=14)
axes[0].set_xlabel('Trading Days')
axes[0].set_ylabel('Capital')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 对比柱状图
metrics = ['Annual Return', 'Max Drawdown']
train_vals = [ret_train, dd_train]
test_vals = [ret_test, dd_test]
x = np.arange(len(metrics))
width = 0.35
axes[1].bar(x - width/2, [v*100 for v in train_vals], width, label='Training', color='steelblue')
axes[1].bar(x + width/2, [v*100 for v in test_vals], width, label='Testing', color='coral')
axes[1].set_xticks(x)
axes[1].set_xticklabels(metrics)
axes[1].set_ylabel('Percentage (%)')
axes[1].set_title('Training vs Testing Comparison', fontsize=14)
axes[1].legend()
axes[1].grid(True, alpha=0.3, axis='y')
for i, (tv, tt) in enumerate(zip(train_vals, test_vals)):
axes[1].text(i - width/2, tv*100 + 1, f'{tv:.1%}', ha='center', va='bottom', fontsize=10)
axes[1].text(i + width/2, tt*100 + 1, f'{tt:.1%}', ha='center', va='bottom', fontsize=10)
plt.tight_layout()
plt.savefig('out_of_sample_test.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n✅ 样本外检验完成,图片已保存")
你跑完看——
训练集年化30%。
测试集年化3%。
衰减了90%。
还觉得自己牛逼?
训练集里巴菲特,测试集里韭菜王。
不用我说了吧。
样本外检验是量化的底线。
没有样本外检验的回测——
就跟没有体检的身体一样。
你不知道什么时候炸。
但一定会炸。
三、参数优化的陷阱:参数越多,死得越快
有些兄弟。
回测的时候搞了个网格搜索。
for循环嵌套八层。
把持有天数、止损比例、快线、慢线、止盈比例、仓位比例、ATR倍数、RSI阈值——
八个参数。
每个参数十个值。
一亿种组合。
跑了一晚上。
GPU都快烧了。
找那个年化最高的。
找到了。
年化180%。
最大回撤8%。
夏普比率4.5。
完美。
完美你妈。
八个参数。一亿种组合。
你找到的不是规律。
是随机噪音里碰巧最好看的那个点。
这跟猴子打字一样。
无限只猴子敲无限台打字机。
总有一只能敲出莎士比亚全集。
但它会写十四行诗吗?
不会。
它只是碰巧敲出了那些字母。
参数越多,回测越美。参数越多,实盘越惨。这是铁律。
量化圈有个经验法则——
自由度越高,过拟合越严重。
什么叫自由度?
就是你的参数数量。
一个参数。你很难过拟合。
三个参数。有点危险。
五个参数。大概率过拟合。
八个参数。百分之百过拟合。
怎么判断你的参数是不是太多了?
看参数数量跟交易次数的比值。
有个硬标准——
每增加一个参数,至少需要50笔以上的独立交易来支撑。
你一共就30笔交易。
调了6个参数。
那不是在优化。
那是在编故事。
你自己都说服不了自己。
打个比方。
你有30个数据点。
然后用6次多项式去拟合。
曲线完美穿过每一个点。
R方=0.9999。
但这条曲线有任何预测价值吗?
没有。
它只是在记忆。
量化也一样。
参数越多——记忆越强——预测越弱。
这是数学规律。
不是我的偏见。
那怎么办?
少调参数。
真的。
能三个参数解决的问题,别用八个。
能用简单逻辑的,别搞复杂模型。
佛王课里24个实盘策略,我反复跟学员强调一件事——
策略的参数,越少越好。
逻辑,越简单越好。
能一行代码说清楚的,别写十行。
为什么?
因为简单才能稳健。
稳健才能在实盘活下来。
这不是偷懒。
这是敬畏市场。
四、参数稳健性:怎么才算靠谱
说了这么多。
到底怎么判断一个策略靠不靠谱?
三条标准。
记住。
第一条:邻近参数一致性。
持有4天、5天、6天——收益不能差太多。
止损2%、3%、4%——都得是正的。
快线15到25——都得能赚钱。
如果5天赚50%,4天亏20%——这个5天就是噪音。不可靠。
如果止损3%赚30%,止损2%亏40%——这个3%就是巧合。不可靠。
判断标准:邻近参数的收益变化,不超过50%。
第二条:样本外不崩盘。
训练集到测试集。
收益衰减不超过40%。
超过40%,基本有鬼。
理想情况下,衰减在20%以内。
说明策略捕捉到了跨时间段的稳定规律。
第三条:多品种通用。
你在沪深300上跑出来的参数。
拿到中证500上试试。
拿到创业板ETF上试试。
拿到纳斯达克上试试。
如果只有沪深300赚钱,其他全亏——
这不是规律。
这是沪深300那段数据的特殊性。
换了个品种就不灵了。
如果四个品种有三个赚钱——
这才像那么回事。
真规律跨品种,假规律挑品种。
三条都过了——
恭喜你。
这策略可以上实盘。
至少你心里有底。
三条过不了——
回去重新想。
别调参数了。
调也没用。
换思路。
可能你的入场逻辑本身就有问题。
可能你选的这个品种不适合这个策略。
可能你的止损方式需要重新设计。
总之——别在参数上死磕。
五、实战:一夜持股法的参数检验
来个真实案例。
之前讲过一夜持股法——
收盘买入,次日开盘卖出。
持有一夜。
简单粗暴。
没有均线。没有止损。没有仓位管理。没有止盈。
就一个动作。
现在来检验。
看看它到底经不经得起参数检验。
// python
# pip install akshare pandas numpy matplotlib
import akshare as ak
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
# ========== 获取数据 ==========
# 沪深300ETF
df = ak.fund_etf_hist_em(symbol="510300", adjust="qfq")
df['日期'] = pd.to_datetime(df['日期'])
df = df.sort_values('日期').reset_index(drop=True)
# ========== 一夜持股法 ==========
# 规则:收盘价买入,次日开盘价卖出
df['次日开盘'] = df['开盘'].shift(-1)
df['隔夜收益'] = (df['次日开盘'] - df['收盘']) / df['收盘']
df = df.dropna()
# 基础统计
total_trades = len(df)
win_rate = (df['隔夜收益'] > 0).mean()
avg_return = df['隔夜收益'].mean()
cumulative = (1 + df['隔夜收益']).cumprod()
total_return = cumulative.iloc[-1] - 1
years = (df['日期'].iloc[-1] - df['日期'].iloc[0]) / pd.Timedelta(days=365)
annual_return = (1 + total_return) ** (1/years) - 1
print("=" * 50)
print("一夜持股法 - 基础统计")
print("=" * 50)
print(f"交易次数: {total_trades}")
print(f"胜率: {win_rate:.2%}")
print(f"平均隔夜收益: {avg_return:.4%}")
print(f"累计收益: {total_return:.2%}")
print(f"年化收益: {annual_return:.2%}")
print()
# ========== 变体参数检验 ==========
df['MA20'] = df['收盘'].rolling(20).mean()
df['MA60'] = df['收盘'].rolling(60).mean()
variants = {}
# 原版:无过滤
variants['原版(无过滤)'] = {
'trades': total_trades,
'win_rate': win_rate,
'avg_return': avg_return,
'annual': annual_return
}
# 变体:MA20之上才买
df_ma20 = df[df['收盘'] > df['MA20']].dropna()
if len(df_ma20) > 0:
cum_ma20 = (1 + df_ma20['隔夜收益']).cumprod()
yr_ma20 = (df_ma20['日期'].iloc[-1] - df_ma20['日期'].iloc[0]) / pd.Timedelta(days=365)
ar_ma20 = cum_ma20.iloc[-1] ** (1/max(yr_ma20,0.01)) - 1
variants['MA20之上买入'] = {
'trades': len(df_ma20),
'win_rate': (df_ma20['隔夜收益'] > 0).mean(),
'avg_return': df_ma20['隔夜收益'].mean(),
'annual': ar_ma20
}
# 变体:MA60之上才买
df_ma60 = df[df['收盘'] > df['MA60']].dropna()
if len(df_ma60) > 0:
cum_ma60 = (1 + df_ma60['隔夜收益']).cumprod()
yr_ma60 = (df_ma60['日期'].iloc[-1] - df_ma60['日期'].iloc[0]) / pd.Timedelta(days=365)
ar_ma60 = cum_ma60.iloc[-1] ** (1/max(yr_ma60,0.01)) - 1
variants['MA60之上买入'] = {
'trades': len(df_ma60),
'win_rate': (df_ma60['隔夜收益'] > 0).mean(),
'avg_return': df_ma60['隔夜收益'].mean(),
'annual': ar_ma60
}
# 变体:排除周四买入(规避周末不确定性)
df_weekday = df[df['日期'].dt.dayofweek < 4].copy()
cum_wd = (1 + df_weekday['隔夜收益']).cumprod()
yr_wd = (df_weekday['日期'].iloc[-1] - df_weekday['日期'].iloc[0]) / pd.Timedelta(days=365)
ar_wd = cum_wd.iloc[-1] ** (1/max(yr_wd,0.01)) - 1
variants['排除周四买入'] = {
'trades': len(df_weekday),
'win_rate': (df_weekday['隔夜收益'] > 0).mean(),
'avg_return': df_weekday['隔夜收益'].mean(),
'annual': ar_wd
}
# 变体:连亏熔断(连亏2次后跳过1天)
df_circuit = df.copy()
df_circuit['亏'] = (df_circuit['隔夜收益'] < 0).astype(int)
df_circuit['连亏天数'] = df_circuit['亏'].groupby(
(df_circuit['亏'] != df_circuit['亏'].shift()).cumsum()
).cumsum()
skip = df_circuit['连亏天数'] >= 2
df_circuit.loc[skip, '隔夜收益'] = 0
df_circuit = df_circuit[df_circuit['隔夜收益'] != 0]
cum_cb = (1 + df_circuit['隔夜收益']).cumprod()
yr_cb = (df_circuit['日期'].iloc[-1] - df_circuit['日期'].iloc[0]) / pd.Timedelta(days=365)
ar_cb = cum_cb.iloc[-1] ** (1/max(yr_cb,0.01)) - 1 if len(df_circuit) > 0 else -1
variants['连亏熔断(2次)'] = {
'trades': len(df_circuit),
'win_rate': (df_circuit['隔夜收益'] > 0).mean(),
'avg_return': df_circuit['隔夜收益'].mean(),
'annual': ar_cb
}
# 打印结果
print("=" * 70)
print("一夜持股法 - 参数稳健性检验")
print("=" * 70)
print(f"{'变体':>18} | {'交易数':>6} | {'胜率':>8} | {'日均收益':>10} | {'年化':>10}")
print("-" * 70)
for name, v in variants.items():
print(f"{name:>18} | {v['trades']:>6} | {v['win_rate']:>8.2%} | {v['avg_return']:>10.4%} | {v['annual']:>10.2%}")
# ========== 样本外检验 ==========
split = int(len(df) * 0.7)
train = df.iloc[:split]
test = df.iloc[split:]
train_ret = (1 + train['隔夜收益']).cumprod().iloc[-1] - 1
test_ret = (1 + test['隔夜收益']).cumprod().iloc[-1] - 1
train_yr = (train['日期'].iloc[-1] - train['日期'].iloc[0]) / pd.Timedelta(days=365)
test_yr = (test['日期'].iloc[-1] - test['日期'].iloc[0]) / pd.Timedelta(days=365)
train_ar = (1 + train_ret) ** (1/max(train_yr,0.01)) - 1
test_ar = (1 + test_ret) ** (1/max(test_yr,0.01)) - 1
print("\n" + "=" * 50)
print("一夜持股法 - 样本外检验")
print("=" * 50)
print(f"训练集年化: {train_ar:.2%}")
print(f"测试集年化: {test_ar:.2%}")
if train_ar != 0:
decay = 1 - test_ar / train_ar
print(f"衰减率: {decay:.1%}")
if abs(decay) < 0.4:
print("✅ 衰减可接受,策略稳健。")
else:
print("⚠️ 衰减过大,谨慎实盘。")
# ========== 可视化 ==========
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
# 累计净值
cum_full = (1 + df['隔夜收益']).cumprod()
axes[0].plot(df['日期'], cum_full, color='black', linewidth=1.5, label='Overnight Strategy')
axes[0].axvline(x=df['日期'].iloc[split], color='red', linestyle='--', linewidth=2, label='Train/Test Split')
axes[0].set_title('Overnight Holding Strategy - Equity Curve', fontsize=14)
axes[0].set_ylabel('Cumulative Return')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].tick_params(axis='x', rotation=45)
# 各变体年化收益对比
names = list(variants.keys())
annuals = [variants[n]['annual'] * 100 for n in names]
colors = ['steelblue' if a > 0 else 'coral' for a in annuals]
axes[1].barh(range(len(names)), annuals, color=colors)
axes[1].set_yticks(range(len(names)))
axes[1].set_yticklabels(names)
axes[1].set_xlabel('Annual Return (%)')
axes[1].set_title('Variant Comparison', fontsize=14)
axes[1].axvline(x=0, color='black', linewidth=0.5)
axes[1].grid(True, alpha=0.3, axis='x')
for i, a in enumerate(annuals):
axes[1].text(a + 0.5 if a >= 0 else a - 0.5, i, f'{a:.1f}%',
va='center', ha='left' if a >= 0 else 'right', fontsize=10)
plt.tight_layout()
plt.savefig('overnight_robustness.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n✅ 一夜持股法参数检验完成,图片已保存")
一夜持股法的好处是——
几乎没有参数。
持有一夜。
没有均线。没有止损。没有仓位。
就一个动作:收盘买,开盘卖。
你没法过拟合。
因为根本没参数可调。
它的变体——加MA20过滤、加MA60过滤、排除周四、加连亏熔断——
你会发现,不管怎么变,核心逻辑都不变。
这就是稳健。
这就是为什么简单的策略反而活得久。
你加了再多花里胡哨的过滤器。
收益可能好了一点。
也可能差了一点。
但不会从正变负。
这才叫策略。
大道至简。参数越少,活得越久。
总结
参数检验三件套——
1. 参数敏感性:动一个参数,结果别崩。
2. 样本外检验:训练集赚钱,测试集也得赚。
3. 多品种验证:沪深300行,中证500也得行。
三条过了。
你才配叫策略。
三条没过——
你那叫曲线拟合。
你那叫自欺欺人。
你那叫花钱买教训。
记住——
回测越完美,越要警惕。
参数越少,越要相信。
市场专治各种花里胡哨。简单活着,才是正道。
OVER。