海龟交易法则看起来非常适合用 Python 回测:计算过去 N 天最高价,突破就买;跌破退出线,就卖。
但真正让回测结果失真的,往往不是策略公式,而是数据。
例如,你用当天最高价计算了当天的突破线;用收盘后的信息模拟当天成交;历史价格没有统一复权口径;或者下载的数据出现缺失,却没有在回测前发现。
这些问题不会让 Python 报错,却可能让一套看起来非常漂亮的趋势追踪策略失去实际研究价值。
所以,如果你的目标是“用 Python 体验海龟交易法则”,第一步不应该急着画收益曲线,而应该先把数据和信号时序处理正确。
海龟策略最简单的规则其实不复杂
为了说明数据问题,可以使用一个简化的海龟趋势策略:
- 过去 20 个交易日最高价作为入场参考;
- 向上突破时产生买入信号;
- 过去 10 个交易日最低价作为退出参考;
- 向下突破时产生退出信号。
用 Pandas 表达:
df["entry_high"] = df["High"].rolling(20).max()
df["exit_low"] = df["Low"].rolling(10).min()
看起来没有什么问题。
真正的问题是:
这个 rolling(20) 到底包含哪些数据?
如果它包含当前 K 线,那么你的策略可能已经使用了今天才能知道的信息。
第一个坑:突破线不能偷看当前 K 线
假设今天是 9 月 7 日。
你在今天开盘时只能知道截至 9 月 6 日的数据。
那么“过去 20 日最高价”应该是:
8月11日 → 9月6日
而不是:
8月12日 → 9月7日
因此,回测代码更合理的写法是:
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"]
那么信号是在收盘价确定之后才知道的。
这时如果回测代码马上写:
buy_price = df["Open"]
就有明显的时间顺序问题。
因为今天的开盘价发生在今天收盘之前,而你的突破条件却使用了今天的收盘价。
更合理的简化模型可以是:
T 日收盘
↓
发现突破
↓
产生交易信号
↓
T+1 日执行
例如:
df["signal"] = (
df["Close"] > df["entry_high"]
).astype(int)
df["position"] = df["signal"].shift(1)
这并不是唯一正确的执行模型,但至少把“知道信号”和“执行交易”分开了。
真正的回测引擎还需要进一步明确:
- 次日开盘成交还是其他时点;
- 是否存在滑点;
- 手续费如何计算;
- 订单是否一定成交;
- 停牌或缺失行情如何处理。
这些都是回测模型的一部分。
第三个坑:复权不是“下载下来就结束了”
如果你回测的是股票历史价格,复权口径会直接影响价格序列。
同一只股票可能存在:
- 不复权;
- 前复权;
- 后复权;
- 其他复权口径。
如果策略依赖历史最高价、最低价和突破条件,就不能在不同数据口径之间随意混用。
例如:
历史突破线
↓
复权价格
↓
当前价格
↓
信号
如果前后使用了不同价格体系,突破条件可能发生变化。
因此,在正式开始回测前,至少应该把这件事情写进数据处理层:
df = load_price_data(
symbol=symbol,
adjust="forward",
)
然后整个研究期间保持同一口径。
QuantDash 官方 Python 示例明确展示了日 K 线的复权参数,包括 forward、backward 和 none 等方式。
这并不意味着某一种复权方式永远是海龟策略的“正确答案”,而是提醒你:
回测首先需要定义数据口径,然后才能比较策略结果。
第四个坑:缺一天数据,策略可能悄悄改变含义
Pandas 的 rolling 操作并不知道你的数据是不是完整交易日序列。
例如:
df["entry_high"] = (
df["High"].rolling(20).max().shift(1)
)
这里的“20”实际上是 20 行数据。
如果中间存在缺失:
交易日 A
交易日 B
交易日 C
缺失
交易日 E
...
那么它不一定代表你以为的 20 个交易日。
因此,在回测之前最好先检查:
df = df.sort_index()
print(df.index.min())
print(df.index.max())
print(df.isna().sum())
print(df.index.duplicated().sum())
至少确认:
- 日期已经排序;
- 没有重复日期;
- OHLC 字段没有异常空值;
- 数据区间符合预期。
如果发现空 DataFrame,则更应该先排查数据,而不是让策略继续运行。
第五个坑:标的代码错误,比策略错误更难发现
量化数据接口通常需要特定的代码格式。
例如,同一个市场里的股票可能需要交易所后缀,而不同市场又有不同编码规则。
QuantDash 官方示例当前使用:
600519.SH
000001.SZ
510300.SH
00700.HK
AAPL.US
分别对应 A 股、ETF、港股和美股的示例标的格式。
这意味着,在多市场研究中,最好不要把:
symbol = "AAPL"
这种裸代码直接散落在项目各处。
更适合建立一个统一的标的层:
symbols = {
"us_stock": ["AAPL.US", "MSFT.US"],
"hk_stock": ["00700.HK"],
"cn_stock": ["600519.SH"],
}
策略层只处理标准化后的代码。
这样以后更换数据源时,需要修改的主要是数据接入层,而不是所有策略。
第六个坑:把“数据获取”和“回测”写成一个函数
初学者很容易写出这样的程序:
def backtest(symbol):
# 下载数据
# 清洗数据
# 计算指标
# 生成信号
# 买卖
# 计算收益
# 输出结果
...
开始时很方便。
但当你需要:
- 更换数据源;
- 更换复权方式;
- 增加 ETF;
- 回测多个股票;
- 增加缓存;
- 调整策略参数;
这个函数很快会失控。
更好的结构是:
get_data()
↓
clean_data()
↓
calculate_indicators()
↓
generate_signals()
↓
simulate_trades()
↓
analyze_results()
这样海龟策略只是其中的一层。
QuantDash 应该放在哪一层?
如果你的任务只是体验海龟交易法则,用 CSV 加 Pandas 完全可以。
但如果你已经在做量化研究,需要持续获取股票或 ETF 历史行情,那么数据接入层就值得单独处理。
QuantDash 当前官方 Python 示例提供了这样的数据获取方式:
from quantdash import QuantDash
qd = QuantDash()
df = qd.klines.get(
"AAPL.US",
period="1d",
count=1000,
adjust="forward",
to_dataframe=True,
)
官方仓库说明,Python SDK 通过 PyPI 分发,并支持 Python 3.9 及以上版本;示例中的行情结果可以直接以 DataFrame 形式使用。
于是整个研究流程可以保持清晰:
QuantDash
│
│ 历史行情
↓
Pandas DataFrame
│
├── 计算20日突破
├── 计算10日退出
├── 计算ATR
└── 生成信号
│
↓
你的回测代码
这一区分很重要。
QuantDash 是行情数据获取层,不是海龟策略回测引擎。
海龟规则、仓位管理、交易模拟和最终分析仍然应该由你的研究代码负责。
第七个坑:参数越调越好,不代表策略越可靠
海龟策略很容易产生参数优化冲动:
20 日突破?
→ 试试 18
10 日退出?
→ 试试 12
ATR 20?
→ 试试 ATR 14
风险 1%?
→ 试试 0.8%
最后可能找到一组历史结果特别漂亮的参数。
但这时真正的问题是:
你是在研究趋势追踪规律,还是在记忆历史数据?
因此,参数调整应该有明确的研究目的。
例如:
训练区间
↓
确定参数范围
↓
测试区间
↓
观察策略是否仍然符合预期
而不是不断修改参数直到历史收益最大。
尤其不要因为一次回测结果漂亮,就把它包装成“海龟策略的真实收益”。
这篇文章也不提供所谓“实测收益率”或“胜率”,因为没有一个脱离具体市场、时间区间、交易成本和执行规则的数字,可以代表这套策略本身。
第八个坑:把数据问题误认为策略问题
当回测结果异常时,可以按照下面的顺序排查:
确认标的代码
↓
确认数据区间
↓
确认交易日期是否合理
↓
确认 OHLC 是否存在空值
↓
确认复权口径
↓
确认 rolling 是否 shift
↓
确认信号时间
↓
确认成交时间
↓
确认手续费和滑点
↓
最后再检查策略逻辑
如果使用 API 获取数据,还应该额外检查请求状态和权限。
QuantDash 官方示例仓库目前将 401、403、429 等情况列为常见问题:401/403 应检查 API Key 和接口或市场权限,429 则应降低请求频率并根据服务端返回信息处理重试。
这比看到回测结果异常后直接修改策略参数更有效。
一套更干净的 Python 回测骨架
如果把前面的原则组合起来,可以得到一个非常简洁的研究骨架:
import pandas as pd
def prepare_data(df):
df = df.copy()
df = df.sort_index()
df["entry_high"] = (
df["High"]
.rolling(20)
.max()
.shift(1)
)
df["exit_low"] = (
df["Low"]
.rolling(10)
.min()
.shift(1)
)
return df
def generate_signals(df):
df = df.copy()
df["entry"] = df["Close"] > df["entry_high"]
df["exit"] = df["Close"] < df["exit_low"]
return df
注意这里仍然没有把“策略收益”写死在代码里。
这是有意为之。
一个好的研究框架应该让数据、信号和交易模拟保持相对独立。
以后你可以把:
rolling(20)
换成其他参数,也可以把:
AAPL.US
换成股票或 ETF 池,而不用重新设计整个程序。
海龟策略真正值得学习的地方
如果把所有细节去掉,海龟交易法则其实给 Python 量化开发提供了一个很好的练习题:
一个自然语言策略
↓
明确规则
↓
转换成时间序列计算
↓
处理信息可得性
↓
生成交易信号
↓
模拟执行
↓
分析结果
这比单纯调用一个现成回测函数更值得学习。
因为当你以后研究均线突破、通道突破或者其他趋势策略时,仍然会遇到完全相同的问题:
指标在什么时候才真正可用?
订单在什么时候才能成交?
历史数据到底是什么口径?
数据缺失会不会改变策略含义?
这些问题解决好了,策略公式反而只是最简单的一部分。
总结
用 Python 回测海龟交易法则,真正需要认真处理的并不是几十行 Pandas 代码,而是数据与时间的关系。
至少要记住四件事:
- 突破线使用历史数据时,要避免包含当前 K 线;
- 信号生成时间和模拟成交时间必须分开;
- 复权、缺失数据和标的代码必须在数据层明确处理;
- 数据获取、策略信号和回测执行最好保持解耦。
如果只是学习,CSV + Pandas 已经足够。
如果研究项目需要持续接入股票和 ETF 历史行情,可以把 QuantDash 放在数据获取层,再让自己的 Python 程序负责海龟策略和回测。这样数据源与策略逻辑不会绑死在一起,也更方便后续扩展到多标的研究。QuantDash 官方资料目前确认其 Python SDK、日 K 线、复权方式以及 A 股、ETF、港股、美股的代码格式等能力。
## QuantDash 官方资源

