4.3.3.5 参数检验:过拟合是最大的坑

0 阅读19分钟

回测曲线漂亮得像整容脸?十有八九是过拟合整出来的。

上一章。

你搞了个策略。

回测——年化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。