Python 趋势跟踪策略
第 4 篇的双均线是趋势跟踪的最简形态。本篇升级到这一流派的巅峰之作——海龟交易法则的核心:唐奇安通道突破。
完成一个"20 日突破入场、10 日反向突破离场"的完整策略,并在茅台 2019-2024 数据上诚实评估它的表现。
海龟策略的来历
1983 年,交易大师理查德·丹尼斯与人打赌"交易能力可以后天培养",招募了一批学员称为"海龟",教授一套机械化规则。这套规则后来公开,成为量化入门的必修课。
其核心思想与双均线一致——追涨趋势,但入场离场更明确:
| 环节 | 规则 | 逻辑 |
|---|---|---|
| 入场 | 收盘价突破过去 20 日最高价 | 创阶段新高,趋势可能启动 |
| 离场 | 收盘价跌破过去 10 日最低价 | 趋势衰竭信号 |
| 仓位 | 按 ATR 波动率调整(原文用 2% 风险法则) | 波动大的品种少买 |
说明:完整海龟法则还包含加仓、止损与多品种分散,篇幅所限本篇实现其核心框架;仓位管理(ATR 风险法则)在风险管理一篇展开。
唐奇安通道
唐奇安通道就是"过去 N 日的最高价/最低价"画出的两条线。实现时有一个关键细节:必须 shift(1) 前推一天。
理由与回测公平性有关:今天的收盘价要和"截至昨天"的 20 日最高比较,如果把今天也算进通道,价格永远突破自己,策略就废了。
实例
close = yf.download("600519.SS", start="2019-01-01", end="2024-12-31",
progress=False, multi_level_index=False)["Close"]
# 通道用截至"昨天"的数据:rolling 之后必须 shift(1)
upper_ch = close.rolling(20).max().shift(1) # 入场参考:20 日最高
lower_ch = close.rolling(10).min().shift(1) # 离场参考:10 日最低
策略主循环
逐日遍历,空仓时看是否突破上轨,持仓时看是否跌破下轨:
实例
position = pd.Series(0, index=close.index) # 每日持仓状态:1 持有,0 空仓
pos, trades = 0, 0
for i in range(1, len(close)):
if pos == 0 and close.iloc[i] > upper_ch.iloc[i]:
pos = 1 # 突破 20 日高点,入场
trades += 1
elif pos == 1 and close.iloc[i] < lower_ch.iloc[i]:
pos = 0 # 跌破 10 日低点,离场
position.iloc[i] = pos
# 策略日收益 = 昨日持仓 x 今日涨跌(信号次日执行,防未来函数)
daily_ret = close.pct_change()
strat_ret = position.shift(1).fillna(0) * daily_ret.fillna(0)
代码结构你在回测与绩效评估已经见过:持仓序列 shift(1) 后乘以日收益,这次只是把信号来源换成了通道突破。
回测与诚实评估
实例
cum = (1 + strat_ret).cumprod()
days = len(strat_ret)
ann = cum.iloc[-1] ** (252 / days) - 1
vol = strat_ret.std() * np.sqrt(252)
mdd = (cum / cum.cummax() - 1).min()
sharpe = (ann - 0.03) / vol
bh = close.iloc[-1] / close.iloc[0] - 1
print(f"交易次数 {trades} | 策略累计 {cum.iloc[-1]-1:.2%} | 买入持有 {bh:.2%}")
print(f"年化 {ann:.2%} | 波动 {vol:.2%} | 最大回撤 {mdd:.2%} | 夏普 {sharpe:.2f}")
输出结果如下所示:
交易次数 33 | 策略累计 69.16% | 买入持有 187.58% 年化 9.55% | 波动 18.92% | 最大回撤 -28.53% | 夏普 0.35
买入持有同期对比(同一段数据计算):累计 187.58%、年化 20.12%、最大回撤 -47.48%、夏普 0.58。
结果值得认真解读,而不是只看收益率:
| 指标 | 海龟策略 | 买入持有 | 解读 |
|---|---|---|---|
| 累计收益 | 69.16% | 187.58% | 大幅跑输——趋势策略吃不到单边牛市全程 |
| 最大回撤 | -28.53% | -47.48% | 回撤大幅收敛——通道离场在 2021 年后的下跌中起了保护作用 |
| 夏普比率 | 0.35 | 0.58 | 风险调整后仍有差距,策略并非免费午餐 |
把 2023-2024 段的通道与买卖点画出来,策略行为一目了然:
实例
seg = close.loc["2023-01-01":"2024-12-31"]
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(seg.index, seg.values, color="#1f2933", linewidth=1.2, label="Close")
ax.plot(upper_ch.loc[seg.index], color="#14805e", linewidth=1.0,
linestyle="--", label="20-day High Channel (Entry)")
ax.plot(lower_ch.loc[seg.index], color="#c0563f", linewidth=1.0,
linestyle="--", label="10-day Low Channel (Exit)")
buy_pts = seg[(position.loc[seg.index].diff() == 1)]
sell_pts = seg[(position.loc[seg.index].diff() == -1)]
ax.scatter(buy_pts.index, buy_pts.values, marker="^",
color="#14805e", s=110, zorder=3, label="Buy")
ax.scatter(sell_pts.index, sell_pts.values, marker="v",
color="#c0563f", s=110, zorder=3, label="Sell")
ax.set_title("Maotai: Turtle Breakout (Donchian 20/10) 2023-2024")
ax.set_xlabel("Date")
ax.set_ylabel("Price (CNY)")
ax.legend()
ax.grid(alpha=0.25)
plt.show()
执行以上代码,输出的图表如下:

可以看到通道像两条随行的"护栏":价格突破上护栏时买入,跌穿下护栏时离场,全部交易在图上一目了然。
趋势策略的性格
结合第 9 篇双均线回测(同样跑输买入持有)与本篇结果,可以给趋势策略画出完整画像:
| 市场环境 | 趋势策略表现 | 原因 |
|---|---|---|
| 单边上涨 | 赚钱但跑输买入持有 | 突破才入场,永远慢半拍;离场规则又让它提前下车 |
| 单边下跌 | 大赚,或至少避开 | 空仓状态天然免疫,或做空获利 |
| 区间震荡 | 反复止损亏损 | 假突破不断触发假信号 |
这不是缺陷而是性格:趋势策略用"牛市少赚"换"熊市保命"。机构把趋势策略与均值回归策略搭配使用,正是为了互补——均值回归是下一篇的主角。
点我分享笔记