2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含300篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
引言
在量化交易的世界里,找到一个盈利的策略只是第一步。真正的挑战在于:这个策略能否经受住市场变化的考验?
今天我们来拆解一篇关于 BNB-USD 交易策略的技术文章,看看作者是如何通过参数压力测试和前向优化验证,打造一个真正稳健的趋势跟踪系统。这篇文章非常适合正在学习 Python 量化交易的同学。
策略核心思路
这个策略的逻辑非常简单易懂:
入场信号:当价格站上简单移动平均线(SMA)时,说明市场处于上升趋势,此时开仓做多。
出场信号:当价格跌破加权移动平均线(WMA)时,说明趋势可能反转,此时平仓离场。
为什么用两种不同的均线?SMA 更稳定,适合判断大趋势;WMA 对近期价格更敏感,能更早发现趋势转弱的信号。
第一步:参数压力测试
很多人做量化的误区是:找到一组"最优参数"就万事大吉了。但市场是变化的,过度优化的参数往往在未来表现糟糕。
作者的做法是对 SMA 和 WMA 的周期参数进行全面测试:
import pandas as pd
import numpy as np
import yfinance as yf
import itertools
import vectorbt as vbt
import matplotlib.pyplot as plt
# -------------------------
# 下载 BNB-USD 历史数据
# -------------------------
symbol = "BNB-USD"
start_date = "2000-01-01"
end_date = "2026-01-01"
interval = "1d"
df_raw = yf.download(
symbol,
start=start_date,
end=end_date,
interval=interval,
multi_level_index=False
)
# -------------------------
# 定义参数测试范围
# -------------------------
# SMA 周期:5 到 50,步长为 5
sma_periods = list(range(5, 51, 5))
# WMA 周期:5 到 50,步长为 5
wma_periods = list(range(5, 51, 5))
# 生成所有参数组合
param_sets = list(itertools.product(sma_periods, wma_periods))
# -------------------------
# 指标计算函数
# -------------------------
def calculate_sma(series, period):
"""计算简单移动平均线"""
return series.rolling(period).mean()
def calculate_wma(series, period):
"""计算加权移动平均线,近期权重更高"""
weights = np.arange(1, period + 1)
return series.rolling(period).apply(
lambda x: np.dot(x, weights) / weights.sum(),
raw=True
)
# -------------------------
# 遍历所有参数组合进行回测
# -------------------------
equity_curves = {}
for sma_p, wma_p in param_sets:
df = df_raw.copy()
# 计算指标
df['SMA'] = calculate_sma(df['Close'], sma_p)
df['WMA'] = calculate_wma(df['Close'], wma_p)
# 入场条件:收盘价高于 SMA
df['entry_signal'] = df['Close'] > df['SMA']
# 出场条件:开盘价跌破 WMA(前一天还在 WMA 上方)
df['exit_signal'] = (
(df['Open'] < df['WMA']) &
(df['Open'].shift(1) > df['WMA'].shift(1))
)
# 信号延迟一天执行,模拟真实交易
entries = df['entry_signal'].shift(
1).astype(bool).fillna(False).to_numpy()
exits = df['exit_signal'].shift(1).astype(bool).fillna(False).to_numpy()
# 执行回测
pf = vbt.Portfolio.from_signals(
close=df['Open'],
entries=entries,
exits=exits,
init_cash=100, # 初始资金 100
fees=0.001, # 手续费 0.1%
slippage=0.002, # 滑点 0.2%
freq='1d'
)
label = f"SMA({sma_p}) / WMA({wma_p})"
equity_curves[label] = pf.value()
# -------------------------
# 绘制所有资金曲线
# -------------------------
plt.figure(figsize=(14, 7))
for label, curve in equity_curves.items():
plt.plot(curve.index, curve.values, alpha=0.35, linewidth=1)
plt.title(f"BNB-USD SMA/WMA Strategy Stress Test")
plt.xlabel("Date")
plt.ylabel("Portfolio Value")
plt.grid(True)
plt.show()
这段代码测试了 100 种不同的参数组合(10 × 10),生成了 100 条资金曲线。
压力测试的结果令人欣慰:大多数参数组合都呈现相似的增长轨迹,没有哪个参数特别突出,性能下降也是渐进的而非灾难性的。这说明策略的盈利能力来自于其结构设计,而不是依赖某个特定参数。
第二步:前向优化验证
参数压力测试证明了策略的稳健性,但还有一个关键问题:在真实交易中,我们不可能用未来的数据来优化参数。
前向优化(Walk-Forward Optimization)解决了这个问题。它的流程是:
import pandas as pd
import numpy as np
import yfinance as yf
import vectorbt as vbt
from itertools import product
# ======================================================
# 下载数据
# ======================================================
symbol = "BNB-USD"
start_date = "2017-01-01" # BNB 于 2017 年上线
end_date = "2026-01-01"
interval = "1d"
df = yf.download(symbol, start=start_date, end=end_date, interval=interval, multi_level_index=False)
df.index = pd.to_datetime(df.index)
# ======================================================
# 指标计算函数
# ======================================================
def calculate_sma(
df, period):
"""计算 SMA 并添加到 DataFrame"""
df = df.copy()
df['SMA'] = df["Close"].rolling(period).mean()
return df
def close_above_sma(df, period):
"""判断收盘价是否高于 SMA"""
df2 = calculate_sma(df, period)
return df2["Close"] > df2["SMA"]
def calculate_wma(df, period):
"""计算 WMA 并添加到 DataFrame"""
df = df.copy()
weights = np.arange(1, period + 1)
df["WMA"] = df["Close"].rolling(period).apply(
lambda x: np.dot(x, weights) / weights.sum(), raw=True
)
return df
def open_below_wma_after_above(df, period):
"""判断开盘价是否跌破 WMA(前一天在上方)"""
df2 = calculate_wma(df, period)
return (df2["Open"] < df2["WMA"]) & (df2["Open"].shift(1) > df2["WMA"].shift(1))
# ======================================================
# 前向优化核心函数
# ======================================================
def run_walk_forward(df, train_years=2, test_years=1,
sma_periods=[20], wma_periods=[20]):
"""
执行前向优化
train_years: 训练窗口年数
test_years: 测试窗口年数
"""
results = []
# 计算最大回望期,用于指标预热
max_lookback = max(max(sma_periods), max(wma_periods))
lookback = max_lookback + 5
start_year = df.index[0].year
end_year = df.index[-1].year
# 滚动窗口遍历
for train_start in range(start_year, end_year - train_years - test_years + 1):
train_end = train_start + train_years - 1
test_start = train_end + 1
test_end = test_start + test_years - 1
# 获取训练和测试数据的索引位置
train_start_idx = df.index.get_loc(df[df.index.year == train_start].index[0])
train_end_idx = df.index.get_loc(df[df.index.year == train_end].index[-1])
test_start_idx = df.index.get_loc(df[df.index.year == test_start].index[0])
test_end_idx = df.index.get_loc(df[df.index.year == test_end].index[-1])
# 包含回望期的数据切片,避免指标预热问题
train_slice = df.iloc[max(0, train_start_idx - lookback): train_end_idx +
1]
test_slice = df.iloc[max(0, test_start_idx - lookback): test_end_idx + 1]
# ========================================
# 在训练数据上寻找最优参数
# ========================================
best_perf = -np.inf
best_params = None
for sma_p, wma_p in product(sma_periods, wma_periods):
entry_raw = close_above_sma(train_slice, sma_p)
exit_raw = open_below_wma_after_above(train_slice, wma_p)
# 计算相对位置
train_rel_start = train_start_idx - max(0, train_start_idx - lookback)
train_rel_end = train_end_idx - max(0, train_start_idx - lookback)
entry = entry_raw.iloc[train_rel_start: train_rel_end + 1]
exit_ = exit_raw.iloc[train_rel_start: train_rel_end + 1]
# 回测训练期表现
pf_train = vbt.Portfolio.from_signals(
close=df["Open"].iloc[train_start_idx: train_end_idx + 1],
entries=entry.shift(1).astype(bool).fillna(False).to_numpy(),
exits=exit_.shift(1).astype(bool).fillna(False).to_numpy(),
freq="1d",
init_cash=100000,
fees=0.001,
slippage=0.002
)
# 记录最优参数
if pf_train.total_return() > best_perf:
best_perf = pf_train.total_return()
best_params = (sma_p, wma_p)
# 计算测试数据的相对位置
test_rel_start = test_start_idx - max(0, test_start_idx - lookback)
test_rel_end = test_end_idx - max(0, test_start_idx - lookback)
results.append({
"train_period": f"{train_start}-{train_end}",
"test_period": f"{test_start}-{test_end}",
"best_sma": best_params[0],
"best_wma": best_params[1],
"test_slice": test_slice,
"test_rel_start": test_rel_start,
"test_rel_end": test_rel_end
})
return results
# ======================================================
# 执行前向优化
# ======================================================
sma_periods = list(range(5, 51, 5))
wma_periods = list(range(5, 51, 5))
wf_results = run_walk_forward(
df,
train_years=2,
test_years=1,
sma_periods=sma_periods,
wma_periods=wma_periods
)
# ======================================================
# 合并所有测试窗口的结果
# ======================================================
combined = pd.DataFrame()
print("\n=== 各时期优化参数 ===")
for i, res in enumerate(wf_results):
sma_p = res["best_sma"]
wma_p = res["best_wma"]
test_slice = res["test_slice"]
print(f"{i+1}: 训练期 {res['train_period']} | 测试期 {res['test_period']} | SMA={sma_p}, WMA={wma_p}")
# 应用最优参数计算信号
test_slice["entry_signal"] = close_above_sma(test_slice, sma_p)
test_slice["exit_signal"] = open_below_wma_after_above(test_slice, wma_p)
trimmed = test_slice.iloc[res["test_rel_start"]: res["test_rel_end"] + 1]
combined = pd.concat([combined, trimmed])
combined = combined.sort_index()
# ======================================================
# 最终组合回测
# ======================================================
entries = combined["entry_signal"].shift(
1).astype(bool).fillna(False).to_numpy()
exits = combined["exit_signal"].shift(1).astype(bool).fillna(False).to_numpy()
pf = vbt.Portfolio.from_signals(
close=combined["Open"],
entries=entries,
exits=exits,
freq="1d",
init_cash=100000,
fees=0.001,
slippage=0.002
)
print("\n=== 组合策略表现 ===")
print(pf.stats())
pf.plot().show()
前向优化结果解读
从 2019 年到 2025 年底,策略的表现如下:
总收益率:18367%(是的,你没看错)
最大回撤:61%
总交易次数:63 次
胜率:41%
盈亏比:2.51
看到 41% 的胜率,你可能会觉得这个策略不太行。但这正是趋势跟踪策略的典型特征:输的次数多,但每次赢的时候赚得多。盈亏比 2.51 意味着平均每赚 2.51 元只亏 1 元,这种不对称的收益结构是长期复利增长的关键。
61% 的最大回撤确实很大,但考虑到 BNB 在这段时间经历了多次极端波动,策略依然能够在回撤后快速恢复并创出新高。
总结
这篇文章给我们展示了一个完整的量化策略开发流程:
首先,策略逻辑要简单。用 SMA 判断趋势方向,用 WMA 作为出场信号,逻辑清晰易于理解和维护。
其次,稳健性比最优更重要。与其寻找一组"完美"的参数,不如验证策略在各种参数下都能盈利。
最后,前向优化是必修课。只有模拟真实的交易环境,避免使用未来信息,才能得到可靠的回测结果。
对于学习 Python 量化的同学,这个案例涉及到数据获取(yfinance)、向量化回测(vectorbt)、参数优化(itertools)等常用技能,非常值得动手实践。
参考文章
加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取本文完整研究解析、代码实现细节。财经数据与量化投研知识社区
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
- 1. 双典系统赋能:获赠《财经数据宝典》与《量化投研宝典》完整文档,凝练多年实战经验,构建系统化知识框架;
- 2. 量化因子日更教程(2026重磅新增):每日更新「量化因子专题教程」,配套完整可运行代码与实战案例,深度拆解因子构建、回测与优化全流程;
- 3. 量化文章专题教程库:300+篇星球独有高质量教程式文章,系统覆盖策略开发、因子研究、风险管理等核心领域,内容基本每日更新,并配套精选学习资料与实战参考;
- 4. PyBroker实战课程:赠送《PyBroker-入门及实战》视频课程,手把手教学,快速掌握量化策略开发技能;
- 5. 财经数据支持:定期更新国内外财经数据,为策略研发提供精准、可靠的数据基础;
- 6. 顶尖学者与行业专家分享:年度邀请学术界博士与业界资深专家开展前沿论文精讲与实战案例分享,不少于4场,直击研究前沿与产业实践;
专家直连答疑:与核心开发者及领域专家实时互动,高效解决投研实战难题; - 7. 专业社群与专属福利:加入高质量交流社群,获取课程折扣及更多独家资源。
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!