用Python复现海龟交易法则:从规则拆解到完整回测

用Python复现海龟交易法则:从规则拆解到完整回测 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 头寸设定波动率标准化才是海龟的精髓很多初学者复刻海龟法则时会把注意力放在突破信号上但真正决定这套系统长期表现的是头寸管理。海龟法则用ATRAverage 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 × 每点价值)同左波动率标准化初始止损2N2N开仓后风险控制加仓间隔0.5N0.5N顺势加仓最大单位数44限制单笔敞口这套参数是《海龟交易法则》书中公开的原始参数。在你自己的实验中可以修改这些参数观察结果变化但要注意任何参数调整都可能带来过拟合后文会专门讨论这个问题。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( symbol510300, perioddaily, start_date20180101, end_date20231231, adjustqfq ) df.rename(columns{ 日期: Date, 开盘: Open, 收盘: Close, 最高: High, 最低: Low, 成交量: Volume }, inplaceTrue) 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())这里有几个容易忽视的细节。第一adjustqfq表示前复权。历史回测一般建议使用前复权价格这样能消除分红送股造成的价格跳空让技术指标计算连续。第二akshare返回的列名是中文需要统一成后续代码约定的英文列名并且只保留必要字段。第三设置Date列为索引后后续所有基于日期对齐的操作都会方便很多pandas的滚动窗口、shift偏移都依赖这个索引。下载完成后建议把数据保存成CSV文件避免每次研究都重新请求网络接口df.to_csv(510300_daily.csv)下次直接读取df pd.read_csv(510300_daily.csv, index_col0, parse_datesTrue)4.2 使用yfinance获取美股数据备选如果你更想用美股数据验证策略可以使用yfinance下载SPY标普500 ETF的日线import yfinance as yf df yf.download( SPY, start2018-01-01, end2023-12-31, auto_adjustTrue ) # 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, window20): 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() ], axis1).max(axis1) # Wilder平滑等价于 alpha1/window 的指数加权平均 atr tr.ewm(alpha1 / window, adjustFalse).mean() return atr def prepare_data(df, atr_window20, entry_window20, exit_window10): 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_cash1_000_000, risk_per_trade0.01, atr_stop2.0, atr_add0.5, max_entries4 ): 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这段代码是策略的核心几个关键逻辑需要说明。止损判断放在最前面目的是优先保护本金。如果当日