1983年,理查德·丹尼斯和威廉·埃克哈特做了一个震惊交易圈的实验:用广告招募普通人,经过短期训练后,用真金白银交易期货。结果这批被嘲笑为“海龟”的交易员,在五年里创造了惊人的收益纪录。这件事后来被整理成《海龟交易法则》一书,而整套规则的核心逻辑却非常朴素——突破入场、顺势加仓、分批止损,没有任何玄学指标。
到今天,海龟法则依然是很多人学习量化交易的第一课。原因很简单:它是极少数规则完整、参数公开、可完全用代码复现的经典策略。用Python把它完整实现一遍,你得到的不仅是一个能跑出净值曲线的回测程序,更是一套理解“从交易规则到工程代码”的完整脚手架。很多初学者以为海龟法则的价值在于“20日突破入场”这个信号,真正研究下去就会发现,入场只是最不起眼的部分,头寸管理、加仓节奏和离市规则才是决定长期收益的核心。
这篇文章我会从海龟法则的规则拆解讲起,再用可运行的Python代码实现指标计算、回测引擎、绩效评估和可视化,最后给出常见坑与工程建议。读完你不仅能跑通一个完整的量化回测项目,还能避开新手最容易踩的未来函数、头寸计算错误和数据源问题。
1. 为什么我建议你认真写一遍海龟法则
市面上的量化教程很多,但大部分要么停留在“画出几条均线”的演示层面,要么直接丢给你一个几百行、封装过度的回测框架。海龟法则恰好处在中间:规则足够简单,适合初学者完整理解;但它又具备真实交易系统的所有关键要素——信号、仓位、止损、加仓、离市。把这一套逻辑用代码落地,等于亲手搭建了一个最小可用的交易系统骨架。
从工程角度看,海龟法则还非常适合用来练习几个量化开发的基本功:
第一,数据预处理。历史行情下载、列名统一、缺失值处理、复权价格对齐,这些看起来琐碎,却决定了回测结果是否可信。
第二,技术指标计算。ATR和唐奇安通道虽然不复杂,但要算得“不引入未来函数”,需要理解滚动窗口和shift偏移,这是很多新手第一次踩坑的地方。
第三,事件循环回测。逐日遍历行情、维护持仓状态、处理止损和加仓,这个循环写清楚后,你再去看任何策略的回测代码都会轻松很多。
第四,绩效评估与可视化。收益、回撤、夏普比率、净值曲线,这些指标能帮你判断一个策略是真好还是数据拟合出来的幻觉。
所以,这篇文章不是让你去实盘,而是帮你建立一套可以复用的策略研究和验证工作流。如果你正在学Python数据分析,或者打算入门量化金融,把海龟法则完整实现一遍,性价比非常高。
2. 海龟法则的核心逻辑与关键参数
海龟法则本质是一套趋势跟踪系统。趋势跟踪不预测市场方向,只做一件事:当价格突破某个区间边界时认为趋势可能启动,然后顺势入场,并在趋势延续过程中加仓,在趋势反转时离场。它不追求每次都对,而是靠少数大趋势赚回多次小亏损的总和。
2.1 两类入场系统:20日突破与55日突破
原版海龟法则包含两套互相独立的入场规则。
- 系统1:价格突破过去20日的最高价时入场做多,跌破过去20日的最低价时入场做空。
- 系统2:价格突破过去55日的最高价时入场做多,跌破过去55日的最低价时入场做空。
系统1更敏感,交易频率更高;系统2更迟钝,但捕捉到的趋势往往更持久。原版中,如果上一次系统1入场是盈利的,系统1会被暂时“过滤”掉,只有上次亏损时才重新启用。这个过滤机制的目的是减少震荡行情中的反复开仓损耗。
本文的Python实现以做多为主,使用A股ETF日线数据演示。这样做有两个考虑:一是A股市场做空工具不普遍,二是把注意力集中在核心逻辑上,避免同时引入多空两条腿造成的复杂度。完整版海龟法则支持多空双向,你只需要把跌破下轨的判断对称地加进去即可。
2.2 头寸设定:波动率标准化才是海龟的精髓
很多初学者复刻海龟法则时,会把注意力放在突破信号上,但真正决定这套系统长期表现的是头寸管理。海龟法则用ATR(Average True Range,平均真实波幅)来衡量市场波动,并用它来标准化每一个交易单位的仓位大小。
单个交易单位的计算公式是:
1个单位 = 账户权益的1% / (ATR × 每点价值)对于A股ETF,每点价值约等于价格本身的1元,所以分母近似是ATR。这个公式的含义是:无论波动大小,每一个单位承受的波动风险都占账户权益的1%。如果ATR很大,说明市场波动剧烈,就买更少的数量;如果ATR很小,就买更多。这样做把“风险”而不是“资金”作为仓位计算基准,是海龟法则区别于普通固定仓位策略的核心。
ATR的计算分为两步。先算真实波幅TR,取以下三个值的最大值:
- 当日最高价减去当日最低价
- 当日最高价减去昨日收盘价的绝对值
- 当日最低价减去昨日收盘价的绝对值
然后用类似指数平滑的方式计算ATR,原版使用Wilder平滑,等价于alpha等于1/N的指数加权平均。这个指标反映的是市场最近的波动水平,N通常取20。
2.3 加仓、止损与离市
海龟法则的持仓管理非常激进,但每一步都有明确规则。
入场时先买入1个单位,然后以0.5N为间隔加仓。比如入场后价格每上涨0.5N,就再买入1个单位,最多持有4个单位(1次初始入场加3次加仓)。这样设计是为了在趋势初期仓位较轻,随着趋势确认逐步加码,让利润奔跑。
止损规则同样围绕ATR展开。每个单位的止损价是最近一次成交价减去2N。实际代码中,每次加仓完成后,所有持仓统一使用最新的止损价。这样做的好处是:加仓后止损同步上移,已经获得的浮盈不会因为趋势小幅回调而全部回吐。
离市规则与入场对称。系统1使用过去10日最低价的反向突破离场,系统2使用过去20日最低价的反向突破离场。当收盘价跌破对应下轨时,清仓全部头寸。离市比入场更灵敏,这是趋势跟踪系统的普遍设计——入场需要更长的确认,离场要尽量果断。
2.4 一张表看懂海龟参数
| 参数项 | 系统1 | 系统2 | 作用 |
|---|---|---|---|
| 入场突破 | 20日最高价 | 55日最高价 | 趋势启动确认 |
| 离市突破 | 10日最低价 | 20日最低价 | 趋势结束离场 |
| ATR周期 | 20日 | 20日 | 波动率基准N |
| 头寸单位 | 1%权益 / (N × 每点价值) | 同左 | 波动率标准化 |
| 初始止损 | 2N | 2N | 开仓后风险控制 |
| 加仓间隔 | 0.5N | 0.5N | 顺势加仓 |
| 最大单位数 | 4 | 4 | 限制单笔敞口 |
这套参数是《海龟交易法则》书中公开的原始参数。在你自己的实验中,可以修改这些参数观察结果变化,但要注意:任何参数调整都可能带来过拟合,后文会专门讨论这个问题。
3. Python环境准备与依赖安装
海龟法则的Python实现不需要重型框架,依赖相对简单。请确认你本机已经安装Python 3.8及以上版本,并且pip命令可用。如果你在Windows上安装过多个Python版本,建议在命令行中通过python -m pip来执行安装,避免环境错乱。
核心依赖包括:
| 库名称 | 用途 |
|---|---|
| pandas | 数据处理与滚动计算 |
| numpy | 数值计算 |
| matplotlib | 回测曲线可视化 |
| akshare | 获取A股ETF历史行情 |
| yfinance | 备选数据源,获取美股行情 |
安装命令如下:
pip install pandas numpy matplotlib akshare yfinance如果你习惯使用虚拟环境,建议先创建一个独立环境再安装依赖:
python -m venv turtle_env # Windows turtle_env\Scripts\activate # macOS / Linux source turtle_env/bin/activate pip install pandas numpy matplotlib akshare yfinance安装完成后,运行下面这行代码验证环境:
import pandas as pd import numpy as np import akshare as ak print("pandas:", pd.__version__) print("numpy:", np.__version__) print("akshare:", ak.__version__)如果能正常打印版本号,说明环境就绪。akshare的接口会随版本更新而变化,如果后续执行遇到接口报错,优先检查akshare版本并参考官方文档更新调用方式。
4. 数据获取与清洗:这次用A股ETF演示
回测数据是整个策略研究的地基。数据源出错、价格没有复权、字段名对不上,都会导致回测结果失真。本文选择沪深300ETF(代码510300)作为演示标的,理由是ETF没有个股停牌、财务暴雷、涨跌停板限制等干扰因素,价格连续性更好,适合作为策略原型验证。
4.1 使用akshare获取A股ETF日线
akshare是开源的中国金融数据接口库,不需要注册即可使用。用下面的代码下载510300的日线数据:
import akshare as ak import pandas as pd df = ak.fund_etf_hist_em( symbol="510300", period="daily", start_date="20180101", end_date="20231231", adjust="qfq" ) df.rename(columns={ "日期": "Date", "开盘": "Open", "收盘": "Close", "最高": "High", "最低": "Low", "成交量": "Volume" }, inplace=True) df["Date"] = pd.to_datetime(df["Date"]) df = df.set_index("Date") df = df[["Open", "High", "Low", "Close", "Volume"]] df = df.sort_index() print(df.head()) print(df.tail())这里有几个容易忽视的细节。
第一,adjust="qfq"表示前复权。历史回测一般建议使用前复权价格,这样能消除分红送股造成的价格跳空,让技术指标计算连续。
第二,akshare返回的列名是中文,需要统一成后续代码约定的英文列名,并且只保留必要字段。
第三,设置Date列为索引后,后续所有基于日期对齐的操作都会方便很多,pandas的滚动窗口、shift偏移都依赖这个索引。
下载完成后,建议把数据保存成CSV文件,避免每次研究都重新请求网络接口:
df.to_csv("510300_daily.csv")下次直接读取:
df = pd.read_csv("510300_daily.csv", index_col=0, parse_dates=True)4.2 使用yfinance获取美股数据(备选)
如果你更想用美股数据验证策略,可以使用yfinance下载SPY(标普500 ETF)的日线:
import yfinance as yf df = yf.download( "SPY", start="2018-01-01", end="2023-12-31", auto_adjust=True ) # yfinance返回多级列名,这里取第一层 df.columns = df.columns.get_level_values(0) df = df[["Open", "High", "Low", "Close", "Volume"]] df = df.sort_index() print(df.head())yfinance在部分环境可能因为网络问题无法访问,实际使用时应以你本机网络环境为准。数据源没有绝对优劣,关键是回测过程中保持数据一致、可复现。
5. 核心指标计算:ATR与唐奇安通道
指标计算是整个策略代码中最需要谨慎的部分。唐奇安通道和ATR都不算复杂,但“用哪一天的K线计算信号”是个容易出错的问题。如果使用当天的最高价或最低价来计算当天的突破信号,就引入了未来函数,回测结果会被严重高估。
正确的做法是:今天收盘后,用过去N日的数据计算指标,得到的信号在明天开盘或收盘执行。用pandas实现时,滚动窗口计算完后再调用一次shift(1),把指标整体向后平移一天。
下面实现ATR和唐奇安通道的计算:
import pandas as pd import numpy as np def calculate_atr(df, window=20): high = df["High"] low = df["Low"] close = df["Close"] prev_close = close.shift(1) tr = pd.concat([ high - low, (high - prev_close).abs(), (low - prev_close).abs() ], axis=1).max(axis=1) # Wilder平滑,等价于 alpha=1/window 的指数加权平均 atr = tr.ewm(alpha=1 / window, adjust=False).mean() return atr def prepare_data(df, atr_window=20, entry_window=20, exit_window=10): data = df.copy() data["ATR"] = calculate_atr(data, atr_window) # 唐奇安通道上轨:过去entry_window日最高价的最大值 data["EntryUpper"] = data["High"].rolling(entry_window).max().shift(1) # 唐奇安通道下轨:过去exit_window日最低价的最小值 data["ExitLower"] = data["Low"].rolling(exit_window).min().shift(1) return data代码里的shift(1)是整个指标计算的关键。data["High"].rolling(20).max()得到的是包含当天的20日最高价,shift(1)后,今天这一行保存的是截至昨天为止的20日最高价。也就是说,今天的突破信号完全由昨天及之前的数据决定。
验证一下计算结果:
data = prepare_data(df) print(data[["Close", "ATR", "EntryUpper", "ExitLower"]].tail(10))输出结果中,前20行左右的ATR和EntryUpper会是NaN,这是因为滚动窗口需要积累足够的历史数据。策略回测时应当跳过这些没有指标值的日期,而不是用0或其它值填充。
6. 海龟策略完整回测实现
回测引擎的任务是逐日模拟交易过程,更新持仓和现金,最后生成净值曲线。下面给出一个完整可运行的回测函数,包含入场、加仓、止损、离市四个主要事件。
6.1 回测主逻辑
def run_turtle_backtest( data, initial_cash=1_000_000, risk_per_trade=0.01, atr_stop=2.0, atr_add=0.5, max_entries=4 ): df = data.copy() df = df.dropna(subset=["ATR", "EntryUpper", "ExitLower"]) cash = initial_cash total_shares = 0 entry_count = 0 last_fill_price = 0.0 stop_price = 0.0 trades = [] equity_values = [] for dt, row in df.iterrows(): atr = row["ATR"] entry_upper = row["EntryUpper"] exit_lower = row["ExitLower"] high = row["High"] low = row["Low"] close = row["Close"] # 1) 止损检查 if total_shares > 0 and low <= stop_price: cash += total_shares * stop_price trades.append({ "date": dt, "side": "SELL", "shares": total_shares, "price": stop_price, "reason": "STOP" }) total_shares = 0 entry_count = 0 last_fill_price = 0.0 stop_price = 0.0 # 2) 离市检查 elif total_shares > 0 and close <= exit_lower: cash += total_shares * close trades.append({ "date": dt, "side": "SELL", "shares": total_shares, "price": close, "reason": "EXIT" }) total_shares = 0 entry_count = 0 last_fill_price = 0.0 stop_price = 0.0 equity = cash + total_shares * close # 3) 加仓 if ( total_shares > 0 and entry_count < max_entries and high >= last_fill_price + atr_add * atr ): add_price = last_fill_price + atr_add * atr add_shares = int(equity * risk_per_trade / (atr * 1.0)) if add_shares > 0 and cash >= add_shares * add_price: cash -= add_shares * add_price total_shares += add_shares entry_count += 1 last_fill_price = add_price stop_price = add_price - atr_stop * atr trades.append({ "date": dt, "side": "BUY", "shares": add_shares, "price": add_price, "reason": "ADD" }) # 4) 新入场 elif total_shares == 0 and close > entry_upper: unit_shares = int(equity * risk_per_trade / (atr * 1.0)) if unit_shares > 0 and cash >= unit_shares * close: cash -= unit_shares * close total_shares += unit_shares entry_count = 1 last_fill_price = close stop_price = close - atr_stop * atr trades.append({ "date": dt, "side": "BUY", "shares": unit_shares, "price": close, "reason": "ENTRY" }) # 当日收盘后的总权益 equity = cash + total_shares * close equity_values.append(equity) df["Equity"] = equity_values return df, trades这段代码是策略的核心,几个关键逻辑需要说明。
止损判断放在最前面,目的是优先保护本金。如果当日