在量化交易的世界里,一个常见的误区是追求模型的复杂度。许多人热衷于叠加数十个指标、采用前沿的机器学习算法,却忽略了金融交易中的一个核心原则——简洁即力量。
今天将要拆解的这套策略,便是“大道至简”的有力证明。它仅依赖于两个经典技术指标:Williams %R 和 QStick。在对 CF Industries (股票代码: CF) 超过20年的历史回测中,该策略实现了4644%的总收益率,显著超越了同期“买入并持有”策略的3325%收益。
更关键的是,策略的风险控制也更为出色,其最大回撤为60%,远低于买入持有策略78%的回撤幅度。这意味着它在获取更高回报的同时,也更好地保护了资本。
本文将深入解析这套双指标策略的核心逻辑,并提供完整的Python实现代码。你可以跟随步骤,亲手复现这个回测过程。
一、数据准备
任何量化策略的第一步都是获取可靠的历史数据。我们使用 yfinance 库来下载 CF 公司从2000年初到2025年底的日线行情数据。
import pandas as pd
import numpy as np
import yfinance as yf
import vectorbt as vbt
# -------------------------
# 下载数据
# -------------------------
symbol = "CF" # 股票代码
start_date = "2000-01-01" # 起始日期
end_date = "2026-01-01" # 结束日期
interval = "1d" # 日线级别
# 通过 yfinance 下载历史行情数据
df = yf.download(symbol, start=start_date, end=end_date, interval=interval)
# 保存为 CSV 文件备用
df.to_csv("CF_clean.csv", index=False)
代码执行后,我们将得到一个包含 Open(开盘价)、High(最高价)、Low(最低价)、Close(收盘价)和 Volume(成交量)的DataFrame,时间跨度约为25年,为后续回测提供了坚实的基础。
二、第一个守门人——Williams %R(市场位置传感器)
2.1 指标含义
Williams %R 由拉里·威廉姆斯(Larry Williams)提出,它衡量的是当前收盘价在最近一定周期内最高价与最低价所构成区间中的相对位置。其数值范围在 0 到 -100 之间。
这个指标回答了一个直观而有力的问题:当前价格是更接近近期的强势区域还是弱势区域?
- 当数值接近 0 时,表明收盘价位于近期区间的顶部,市场处于相对强势状态。
- 当数值接近 -100 时,表明收盘价位于近期区间的底部,市场处于相对弱势状态。
2.2 策略中的用法
在本策略中,我们将Williams %R的阈值设定为 -50,即区间的中点。交易信号并不来自于常见的超买超卖区域(如-20、-80),而是关注其能否上穿-50这一中线。
当 Williams %R 从低于-50的位置向上突破-50时,它发出一个信号:价格已经脱离了近期波动区间的下半部分(弱势区域),市场的短期动能正在发生由弱转强的变化。
因此,这是我们的入场过滤条件:只有市场用行动证明自己“不再弱势”时,系统才会考虑入场。
# -------------------------
# Williams %R 参数设置
# -------------------------
WR_LEVEL = -50 # 阈值:区间中点
WR_PERIOD = 14 # 回望周期:14 天
def calculate_williams_r(df, period=WR_PERIOD):
"""计算 Williams %R 指标"""
# 计算过去 period 天的最高价
highest_high = df['High'].rolling(window=period).max()
# 计算过去 period 天的最低价
lowest_low = df['Low'].rolling(window=period).min()
# Williams %R 公式
df['Williams_%R'] = -100 * (highest_high - df['Close']) / (highest_high - lowest_low)
return df
def wr_higher_than_level(df, level=WR_LEVEL):
"""判断 Williams %R 是否高于阈值"""
df = calculate_williams_r(df)
return df['Williams_%R'] > level
2.3 举个例子
假设某只股票过去14天的最高价为100元,最低价为80元。若今日收盘价为95元,则:
Williams %R = -100 × (100 - 95) / (100 - 80) = -100 × 5 / 20 = -25
计算得到的-25大于我们的阈值-50。这表明当前价格已处于近期价格区间的上半部分,满足了我们定义的“市场不弱”的入场条件。
三、第二个守门人——QStick(K线内在压力计)
3.1 指标含义
QStick指标的计算方式非常简洁:它是对过去N个交易日的(收盘价 - 开盘价)差值求移动平均值。
- QStick为正值:意味着在观察期内,收盘价持续高于开盘价的交易日占主导(阳线居多),反映买方力量更强。
- QStick为负值:意味着在观察期内,收盘价持续低于开盘价的交易日占主导(阴线居多),反映卖方力量更强。
因此,QStick可以被视为衡量K线“内在偏向”或“内在压力”的指标。
3.2 策略中的用法
在本策略中,我们并非简单依据QStick的正负来做决策,而是敏锐地捕捉其从零线下方上穿零线的瞬间。这个交叉点代表了一次关键的“政权更迭”——市场内部压力由净卖压(卖方主导)转向了净买压(买方主导)。
这个交叉点被定义为出场信号:当推动价格上涨的内在动力性质发生转变时,说明当前行情的延续性可能面临挑战,是时候考虑获利了结或离场观望。
# -------------------------
# QStick 参数设置
# -------------------------
QSTICK_LEVEL = 0 # 阈值:零线
QSTICK_PERIOD = 10 # 回望周期:10 天
def qstick(df, period=QSTICK_PERIOD):
"""计算 QStick 指标"""
df = df.copy()
# QStick = 过去 period 天(收盘价 - 开盘价)的均值
df['QStick'] = (df['Close'] - df['Open']).rolling(period).mean()
return df
def qstick_cross_above_level(df, level=QSTICK_LEVEL):
"""判断 QStick 是否从下方上穿零线"""
df = qstick(df)
qs = df['QStick']
# 当前值大于阈值,且前一天的值小于等于阈值 → 形成上穿
return (qs > level) & (qs.shift(1) <= level)
四、信号生成与回测执行
4.1 交易规则
将两个指标组合起来,便形成了清晰、可执行的交易系统规则:
- 入场条件:Williams %R 高于 -50(确认市场脱离弱势区域)。
- 出场条件:QStick 从零线下方向上穿越零线(确认内在买压开始显现)。
- 信号处理:所有信号都向前平移一根K线,以避免使用未来数据(Look-ahead Bias),确保回测的严谨性。
- 成交价格:在产生信号的下一个交易日以开盘价进行成交,更贴近实际操作场景。
4.2 完整回测代码
以下是整合信号生成与回测的完整代码。我们使用强大的回测库 vectorbt 来构建投资组合并计算绩效。
# -------------------------
# 生成入场信号
# -------------------------
df["WR_is_Higher_Than_Level"] = wr_higher_than_level(df)
# -------------------------
# 生成出场信号
# -------------------------
df["QStick_Cross_Above_Zero"] = qstick_cross_above_level(df)
# -------------------------
# 组合信号
# -------------------------
entry_conditions = [
'WR_is_Higher_Than_Level', # 入场条件
]
exit_conditions = [
'QStick_Cross_Above_Zero', # 出场条件
]
# 所有入场条件同时满足时触发买入
df['entry_signal'] = df[entry_conditions].all(axis=1)
# 所有出场条件同时满足时触发卖出
df['exit_signal'] = df[exit_conditions].all(axis=1)
# -------------------------
# 回测执行
# -------------------------
# 信号前移一根 K 线,避免未来函数
shift_entries = df['entry_signal'].shift(1).astype(bool).fillna(False).to_numpy()
shift_exits = df['exit_signal'].shift(1).astype(bool).fillna(False).to_numpy()
# 使用 vectorbt 构建投资组合并回测
pf = vbt.Portfolio.from_signals(
close=df['Open'], # 以开盘价成交
entries=shift_entries, # 入场信号
exits=shift_exits, # 出场信号
init_cash=100_000, # 初始资金:10 万美元
fees=0.001, # 手续费:0.1%
slippage=0.002, # 滑点:0.2%
freq='1d' # 日线频率
)
# -------------------------
# 输出回测结果
# -------------------------
print(pf.stats())
pf.plot().show()
4.3 买入持有基准对比
为了客观评估策略的主动管理能力,我们同时计算“买入并持有”(Buy and Hold)同期的表现作为业绩基准。
# 买入持有基准回测
df_holding = df['Open']
pf_holding = vbt.Portfolio.from_holding(df_holding, init_cash=100_000, freq='1d')
print(pf_holding.stats())
五、回测结果分析
策略回测(2005-08-11 至 2025-12-31)与买入持有基准的核心绩效对比如下:
| 指标 |
策略表现 |
买入持有 |
| 总收益率 |
4644% |
3325% |
| 最大回撤 |
60% |
78% |
| 总交易次数 |
79 笔 |
1 笔 |
| 胜率 |
56% |
— |
| 盈亏比(Profit Factor) |
1.59 |
— |
| Sharpe 比率 |
0.80 |
0.73 |
| Sortino 比率 |
1.19 |
1.07 |
| 平均获利交易 |
+21.9% |
— |
| 平均亏损交易 |
-9.2% |
— |
从数据中我们可以得出几个关键结论:
- 收益与风险的“双优”:策略不仅在总收益上超越了基准约1300个百分点,更重要的是将最大回撤控制了更低的水平。这证明两个指标的组合有效地进行了择时,过滤掉了部分大幅下跌的行情。
- 健康的盈亏结构:56%的胜率并不算高,这意味着有近一半的交易以亏损告终。然而,策略的平均盈利(+21.9%)远大于平均亏损(-9.2%),这正是交易中“截断亏损,让利润奔跑”理念的量化体现。优秀的盈亏比(1.59)是策略盈利的核心。
- 极低的交易频率:在超过20年的周期里,策略仅触发了79次交易,平均每年不到4笔。这揭示出策略具有极强的耐心,只在它定义的“高概率时刻”出手,避免了因频繁交易产生的摩擦成本和决策噪音。
六、策略的核心思想
这套Williams %R + QStick的策略,体现的是一种独特的交易“性格”:
- 耐心等待,而非预测走势。它不试图猜测顶部或底部,而是等待市场自身发出确认信号。
- 舒服地空仓,而非害怕踏空。当条件不满足时,它乐于持有现金。
- 确认跟随,内部动量的转变是它行动的唯一依据。
- 接受回撤但确保可控,通过出场机制主动管理下跌风险。
- 关注结构性变化,如价格在区间内的相对位置和K线内在压力的根本性扭转,而非追逐简单的价格突破。
总而言之,它不追逐市场热点,而是冷静地观察市场内部力量的角力,并在力量对比发生转变时果断行动。这种基于算法和规则的系统性方法,正是量化交易试图剥离情绪干扰、实现稳定收益的初衷。
总结
这套简洁的Williams %R + QStick双指标策略,用长达20年的历史数据向我们传递了一个深刻的道理:在量化交易中,复杂并不等同于有效。实现稳健盈利的关键,往往在于清晰的规则逻辑、严格一致的执行纪律,以及建立在大量历史数据之上的诚实测试。
两个指标,一组数据,二十年验证——这便是代码所讲述的关于“简单与坚持”的故事。
当然,我们必须清醒认识到,历史回测的优秀表现绝不能等同于未来实盘的成功。在将任何策略投入实战前,都必须进行更全面的稳健性检验,例如参数敏感性分析、样本外测试、跨市场品种测试以及Walk-Forward优化等。
风险提示:本文内容仅限量化策略研究与技术交流,所涉及标的仅供示例,不构成任何具体的投资建议。金融市场风险莫测,任何交易系统都存在失效的可能,请务必进行充分的独立研究与测试。
对量化策略开发、回测和数据分析感兴趣?欢迎访问云栈社区,与更多开发者交流实战经验,探索数据驱动的无限可能。