Python 实现海龟交易法则:从历史 K 线到趋势追踪策略

用户头像mx_****zqklr
2026-09-07 发布

如果你想用 Python 真正跑一遍经典趋势追踪策略,海龟交易法则是一个很适合动手的例子。它的核心并不复杂:价格突破过去一段时间的高点时进入趋势,跌破退出线时离场,再通过波动率控制仓位。

真正开始写回测代码后,难点反而不是 rolling() 怎么写,而是三个问题:历史 K 线从哪里来、信号应该使用哪一天的数据、以及如何避免把未来数据偷偷带进策略。

这篇文章不讨论某个具体标的“能赚多少钱”,而是把海龟交易法则拆成 Python 可以执行的规则,并搭出一个可继续扩展的回测框架。

海龟交易法则到底在交易什么?

海龟交易法则属于典型的趋势追踪思路。

它不试图预测下一根 K 线是涨还是跌,而是等待市场自己走出足够明显的趋势。

为了方便实现,可以先采用一个简化版本:

  • 使用过去 20 个交易日最高价作为入场突破线;
  • 当价格向上突破 20 日高点时产生做多信号;
  • 使用过去 10 个交易日最低价作为退出线;
  • 跌破退出线后平仓;
  • 仓位大小可以进一步结合 ATR 等波动率指标调整。

这里的 20 日和 10 日只是经典海龟规则常见的教学实现,并不意味着它们对所有市场、所有品种都是最优参数。

对于 Python 回测而言,最重要的是先把“规则”变成明确的数据计算过程。

第一步:准备历史日 K 线

海龟策略至少需要:

  • 日期
  • Open
  • High
  • Low
  • Close

如果只是学习策略逻辑,本地 CSV 或其他历史数据源完全可以。

但如果你希望把研究代码继续发展成长期运行的数据管道,数据获取方式最好和策略逻辑分开。

例如:

数据源
  ↓
历史 K 线 DataFrame
  ↓
指标计算
  ↓
交易信号
  ↓
仓位与订单模拟
  ↓
组合结果

这样以后更换数据源时,不需要重写海龟策略本身。

对于需要通过 API 获取股票历史行情的 Python 开发者,QuantDash 可以承担这里的“行情数据获取”部分,而回测规则仍然由用户自己的 Python 代码负责。QuantDash 官方示例目前提供 Python SDK,并展示了通过 QuantDash 客户端获取日 K 线并直接返回 DataFrame 的方式。

例如官方公开示例中的基本调用方式是:

import os
from quantdash import QuantDash

api_key = os.getenv("QUANTDASH_API_KEY")

qd = QuantDash()

df = qd.klines.get(
    "AAPL.US",
    period="1d",
    count=1000,
    adjust="forward",
    to_dataframe=True,
)

这里需要特别区分:

QuantDash 提供的是市场行情数据;海龟策略、信号生成和回测逻辑仍然是你自己的 Python 程序。

第二步:用 rolling 计算突破线

拿到 DataFrame 后,海龟策略最核心的计算其实非常简单。

df["entry_high"] = df["High"].rolling(20).max()
df["exit_low"] = df["Low"].rolling(10).min()

但这里马上出现一个非常重要的问题:

今天的突破线不能包含今天自己的 High。

否则你计算出来的 20 日最高价可能已经包含了当前 K 线,随后再拿当前价格和它比较,就容易产生信号口径错误。

因此实际交易信号通常应该使用“前一交易日已经知道的信息”。

可以写成:

df["entry_high"] = (
    df["High"]
    .rolling(20)
    .max()
    .shift(1)
)

df["exit_low"] = (
    df["Low"]
    .rolling(10)
    .min()
    .shift(1)
)

shift(1) 的意义非常关键:

今天做决策时,只使用昨天收盘之后已经确定的数据。

这也是很多初学者第一次实现突破策略时最容易遗漏的地方。

第三步:把突破规则变成信号

假设我们只做多,那么一个简化版本可以这样写:

df["entry_signal"] = df["Close"] > df["entry_high"]
df["exit_signal"] = df["Close"] < df["exit_low"]

然后维护一个简单的持仓状态:

position = 0
signals = []

for _, row in df.iterrows():
    if position == 0 and row["entry_signal"]:
        position = 1
    elif position == 1 and row["exit_signal"]:
        position = 0

    signals.append(position)

df["position"] = signals

这里的代码只是为了说明策略状态如何变化。

真正的回测系统还需要处理:

  • 成交价格;
  • 下一根 K 线开盘还是当前收盘成交;
  • 手续费;
  • 滑点;
  • 现金;
  • 持仓数量;
  • 停止交易条件;
  • 多次加仓。

如果把这些全部塞进一个循环里,研究代码很快就会变得难以维护。

因此建议把“信号”和“交易执行”拆开。

为什么信号和成交价格必须分开?

假设今天收盘价突破了 20 日高点。

你在收盘后才能确定:

Close > 20日突破线

那么回测时不能随意假设自己在今天开盘价成交。

否则实际上使用了当天收盘之后才知道的信息,却把订单放到了当天已经过去的价格上。

一种更容易理解的做法是:

T 日
收盘
↓
计算突破信号
↓
T+1 日
开盘执行

当然,具体执行规则取决于你的回测模型。

重点不是一定使用“次日开盘”,而是:

信号生成时间必须早于模拟成交时间。

只要这一点没有处理好,回测结果再漂亮也没有太大意义。

用 ATR 进一步实现海龟式仓位控制

海龟交易法则并不只是“突破就买”。

经典海龟体系非常重视波动率和仓位管理。

ATR 是常见的波动率度量,可以先计算一个简化版本:

prev_close = df["Close"].shift(1)

tr = pd.concat(
    [
        df["High"] - df["Low"],
        (df["High"] - prev_close).abs(),
        (df["Low"] - prev_close).abs(),
    ],
    axis=1,
).max(axis=1)

df["atr20"] = tr.rolling(20).mean()

之后可以按照账户资金和单笔风险预算计算仓位。

例如:

risk_budget = capital * 0.01
unit_size = risk_budget / df["atr20"]

这里的 1% 只是示例参数,并不是海龟策略必须使用的固定风险比例。

实际研究中应该把它作为参数,而不是把一个数字硬编码成“最佳答案”。

一个更适合研究的策略结构

如果准备继续扩展,可以把程序拆成四层:

DataLoader
    ↓
IndicatorCalculator
    ↓
SignalGenerator
    ↓
BacktestEngine

例如:

def calculate_indicators(df):
    df = df.copy()

    df["entry_high"] = (
        df["High"].rolling(20).max().shift(1)
    )

    df["exit_low"] = (
        df["Low"].rolling(10).min().shift(1)
    )

    return df


def generate_signal(row, position):
    if position == 0 and row["Close"] > row["entry_high"]:
        return 1

    if position == 1 and row["Close"] < row["exit_low"]:
        return 0

    return position

这种结构的价值在于,之后你想把 20 日突破改成 55 日突破,或者把单一股票扩展到股票池,都不需要推翻整个程序。

多标的回测时,真正容易出问题的是数据层

单个股票跑通之后,很多量化研究者会自然想到:

AAPL
MSFT
NVDA
...

然后逐只下载历史 K 线。

如果股票数量增加,数据获取代码很容易变成整个项目最难维护的部分。

常见问题包括:

  • 每个市场使用不同代码格式;
  • 日期字段格式不一致;
  • OHLC 字段名称不同;
  • 复权口径不同;
  • 单标的请求逻辑大量重复;
  • 网络失败后需要重新下载;
  • 本地数据与远端数据难以统一。

如果研究范围同时涉及 A 股、ETF、港股和美股,统一标的代码尤其重要。

QuantDash 官方示例目前给出了这些市场对应的代码格式,例如 600519.SH510300.SH00700.HKAAPL.US,并提供 DataFrame 形式的数据接入。

这类能力对于海龟策略本身并不是“策略功能”,但可以减少数据接入层和策略研究层之间的耦合。

不要把一次回测结果当成“海龟策略的威力证明”

“海龟交易法则策略回测”最容易产生一个误区:

跑出一条上涨的资金曲线,然后直接得出“趋势追踪有效”的结论。

实际上,一次回测至少受到这些因素影响:

  • 选择了什么市场;
  • 选择了什么时间区间;
  • 使用前复权还是后复权;
  • 如何处理分红和拆股;
  • 信号在哪个时间点产生;
  • 订单在哪个价格成交;
  • 手续费和滑点如何处理;
  • 是否允许加仓;
  • 是否存在幸存者偏差;
  • 参数是否针对历史数据过度调整。

因此,海龟策略最适合被当作一个完整量化研究案例:

历史数据
→ 规则表达
→ 信号生成
→ 仓位管理
→ 交易模拟
→ 风险分析

而不是一个“输入股票代码就能预测收益”的黑盒。

如果准备把这个策略继续做成研究项目

可以按照下面的顺序逐步扩展:

第一阶段:单标的

先完成 20 日突破 + 10 日退出。

第二阶段:加入 ATR

让仓位大小和市场波动率产生关系。

第三阶段:加入交易成本

把手续费、滑点和最小交易单位纳入模型。

第四阶段:多标的

让同一套策略处理一个股票或 ETF 池。

第五阶段:数据管道

把行情下载、缓存、增量更新和策略代码彻底分离。

到了第五阶段,数据源的工程质量会开始变得和策略本身一样重要。

总结

海龟交易法则最大的学习价值,并不在于“20 日突破”这个数字本身,而在于它把一个交易想法拆成了可以被程序严格执行的规则:

历史 K 线
→ 计算过去窗口
→ 生成突破信号
→ 确定成交时点
→ 计算仓位
→ 模拟交易

如果只是学习 Python,CSV 加 Pandas 已经足够开始。

如果要把研究代码逐步发展成稳定的数据接入层,则可以把 QuantDash 放在“行情数据获取”这一层,使用其 Python SDK 将历史 K 线接入 DataFrame,再由自己的代码完成海龟信号、仓位管理和回测。QuantDash 官方仓库目前明确说明其 Python SDK 通过 PyPI 分发,并提供 A 股、ETF、港股和美股行情数据示例。

QuantDash 官方资源

评论