简介基于Python的pandas库实现的海龟交易法则量化策略完整方案面向量化交易、程序化交易及Python数据分析学习者解决从经典策略理论到可运行代码的落地难题。压缩包共3个文件包含1个Python主程序脚本和2个CSV格式数据文件整体仅41KB轻量便携其中数据文件提供了股票历史行情样例可供直接调用。已有1305人学习下载代码实现覆盖历史行情数据读取与清洗、多周期重采样、缺失值填充、SMA与EMA趋势指标计算、基于唐奇安通道的突破信号识别、ATR动态止损止盈、交易信号记录及模拟回测统计等环节。借助内置的股票样例数据使用者可直接运行验证策略表现并通过盈亏比、夏普比率、最大回撤等指标评估效果进而调整周期参数和风险管理规则建立自己的趋势跟踪模型。无论是用于教学演示、策略研究还是实盘前的逆向测试都有助于快速掌握海龟交易法则的核心思想。1. 海龟交易法则的Python实现一套不到百行核心逻辑就能跑通的量化交易复盘如果你拿过股票软件里的唐奇安通道写过策略大概率会有一个感觉指标能画出来但真拿它下单又不敢。这份用pandas实现的海龟交易法则源码把上世纪八十年代训练交易员用的那套完整规则——入场、加仓、止损、离场、仓位管理——压缩进不到一百行核心逻辑跑完一遍日线回测你不仅能看懂这套策略为什么能赚钱还能直接把回测引擎改造成自己的程序化交易工具。它适合两类人一是想用Python做量化交易但不想碰复杂框架的开发者二是在做pandas数据分析实战、想看看DataFrame怎么从头到尾串起一个业务闭环的同学。别把它当成又一个股票软件脚本它是把规则翻译成可执行代码的完整示范。2. 策略原理与系统设计从唐奇安通道到ATR仓位管理先还原规则再谈代码2.1 海龟法则的规则拆解入场、加仓、止损、离场到底在做什么海龟交易法则不是某个公式而是一套决策系统。把它拆开看核心就四块入场、加仓、止损、离场。入场规则用的是唐奇安通道——取过去20个交易日的最高价和最低价价格向上突破20日最高价就做多向下跌破20日最低价就做空这是趋势跟踪里最经典的突破逻辑。加仓规则是海龟策略区别于普通突破策略的关键。它不搞一次性满仓而是用ATR平均真实波幅作为波动率单位价格每向有利方向移动0.5个ATR就加一次仓最多加到4个单位。这样做的好处在于波动大的品种仓位自动变小波动小的品种仓位自动变大资金曲线不会因为某一次极端行情而剧烈抖动。止损和离场是分开的两条规则。止损用2倍ATR入场后价格反向波动超过2个ATR就砍仓离场每加一次仓止损位向有利方向移动0.5个ATR离场规则用的是另一个唐奇安通道——10日通道反向突破比如做多后价格跌破10日最低价就全部平仓不再等2倍ATR的止损线。这套规则最反直觉的地方在于它的胜率很低。单笔交易亏损可能达到2个ATR但盈利的单子会拿住整个趋势盈亏比通常在3比1以上。量化交易里常说的「截断亏损、让利润奔跑」海龟法则就是最原始也最忠实的执行者。2.2 系统整体设计数据层、策略层、回测层三层分离拿到这份源码我建议你先看它的总体结构再看具体函数。整个项目按数据层、策略层、回测层三层划分每一层只依赖上一层的输出互不混写。数据层负责把原始行情文件读成标准化DataFrame字段固定为date、open、high、low、close、volume这六列策略层消费这个DataFrame算出signal和position两列回测层拿到position后模拟逐日撮合输出账户净值曲线和各项绩效指标。三层的接口就是DataFrame字段约定写在代码最上方的注释里。这种分层带来的好处是替换成本极低。想换一个品种或数据源只需要改数据层想调整策略逻辑只需要改策略层并保证输出还是position那一列想换一套绩效评估标准回测层单独改。我在做程序化交易改造时最怕的就是信号、撮合、统计全揉在一个函数里改一个地方牵一发动全身这个项目的分层方式正好避开了那个坑。2.3 目录结构与参数配置把可调参数集中到一个文件里项目目录结构如下各文件职责一目了然turtle_trading/ ├── config.py # 全局参数配置周期、ATR倍数、风险管理 ├── data_loader.py # 数据层读取CSV并清洗成标准OHLCV ├── indicators.py # 策略层唐奇安通道、ATR计算 ├── strategy.py # 策略层生成signal和position ├── backtest.py # 回测层撮合、资金曲线、绩效统计 ├── run.py # 入口脚本串起完整流程 ├── data/ │ └── example.csv # 示例日线数据 └── output/ ├── equity_curve.png └── trades.csvconfig.py负责集中管理所有可调参数这是二次开发最友好的设计。关键参数见下表参数名默认值含义entry_window20入场唐奇安通道周期exit_window10离场唐奇安通道周期atr_window20ATR计算周期risk_ratio0.01单笔风险占账户净值的比例max_units4最大加仓次数unit_step0.5每多少个ATR加一次仓init_capital1000000初始资金把参数集中在config.py里跑回测和调优时只需要改这一个文件不需要到处找硬编码的魔法数字。这也是源码里最值得借鉴的工程习惯。3. 核心模块拆解pandas如何完成信号生成与回测撮合3.1 数据清洗read_csv之后必须做的三件事pandas项目第一步永远是数据清洗这份源码也不例外。原始CSV大概率是第三方软件导出的格式列名可能是中文日期可能是字符串还可能存在重复行。标准化的清洗逻辑如下# data_loader.py import pandas as pd def load_data(filepath): df pd.read_csv(filepath) # 列名统一为英文避免后续反复处理中文字段 df.columns [date, open, high, low, close, volume] # 日期必须解析成datetime类型否则rolling按时间窗口计算会出错 df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 去重同一天多条记录只保留最后一条 df df.drop_duplicates(subsetdate, keeplast) # 缺失值直接丢弃回测里宁可少一段数据也不要插入假数据 df df.dropna() return df这段代码做了四件事列名重命名、日期解析、按时间排序去重、缺失值丢弃。其中最容易翻车的是日期解析——很多CSV导出的日期是2023/1/5这种格式pd.to_datetime默认按%Y-%m-%d解析会直接报错更稳妥的写法是加一个format参数比如pd.to_datetime(df[date], format%Y/%m/%d)。为什么排序和去重这么重要pandas的rolling是严格按行序号滑动的一旦数据里有乱序或重复行算出来的唐奇安通道和ATR全是错的而且这种错在最终结果上很难肉眼察觉属于最阴险的那类bug。我一般在数据层就把它彻底解决后面所有模块都不再关心数据脏不脏。3.2 唐奇安通道与ATR计算rolling窗口与Wilder平滑的取舍策略层的核心是两个指标唐奇安通道和ATR。唐奇安通道的实现很简单用rolling窗口取最大值和最小值# indicators.py def donchian_channel(df, window20): # 上轨过去window个交易日最高价的最大值 df[upper] df[high].rolling(window).max() # 下轨过去window个交易日最低价的最小值 df[lower] df[low].rolling(window).min() return df def atr(df, window20): # 真实波幅的三个来源当日振幅、跳空后的最高价到昨收、昨收到最低价 tr pd.concat([ df[high] - df[low], (df[high] - df[close].shift(1)).abs(), (df[low] - df[close].shift(1)).abs() ], axis1).max(axis1) # 海龟原版用Wilder平滑等价于alpha1/window的EWMA df[atr] tr.ewm(alpha1/window, adjustFalse).mean() return df唐奇安通道没有任何玄学成分rolling(window).max()就是纯粹地取过去N天最高价的最大值。ATR稍微讲究一点真实波幅TR取三个值的最大值——当日最高最低的差、当日最高到昨收的距离、昨收到当日最低的距离这样能把跳空缺口也算进波动里。海龟原版对ATR用的不是简单移动平均而是Wilder平滑这种平滑方式对近期波动反应更快。pandas里没有现成的Wilder函数但ewm(alpha1/window, adjustFalse)在数学上等价于Wilder消除初始偏差前的结果作为近似完全够用。如果你希望更贴近原版可以在前window个周期用简单平均初始化EWMA但实际回测差异极小可以直接用这个近似实现。3.3 信号生成position的构建是向量化最难的一步很多pandas教程会教你所有操作都向量化但海龟策略的加仓逻辑是状态依赖的——当前仓位决定了下一步能否加仓、止损位在哪这违反了向量化的前提。这份源码的处理方式是用Python循环逐bar推演状态再把结果组装成Series性能完全够用逻辑却清晰一百倍。# strategy.py import numpy as np def generate_position(df, config): position 0 # 当前持有单位数正为多、负为空 entry_price 0.0 # 当前持仓的入场价 stop_price 0.0 # 当前止损价 positions [] # 记录每天的持仓状态 risk_amount config.init_capital * config.risk_ratio # 单笔风险金额 unit_size risk_amount / df[atr].iloc[0] # 一个单位的股数 for i in range(len(df)): row df.iloc[i] if position 0: # 空仓时判断突破入场 if row[close] row[upper]: position 1 entry_price row[close] stop_price entry_price - 2 * row[atr] elif row[close] row[lower]: position -1 entry_price row[close] stop_price entry_price 2 * row[atr] else: # 持仓中先检查止损和离场 if position 0: if row[low] stop_price or row[close] row[lower_short]: positions.append(0) position 0 continue # 加仓每上涨0.5个ATR加一个单位最多4个单位 while position config.max_units and \ row[high] entry_price position * config.unit_step * row[atr]: position 1 stop_price 0.5 * row[atr] else: # 空头对称逻辑 if row[high] stop_price or row[close] row[upper_short]: positions.append(0) position 0 continue while position -config.max_units and \ row[low] entry_price position * config.unit_step * row[atr]: position - 1 stop_price - 0.5 * row[atr] positions.append(position) df[position] positions return df这段代码是整个项目最核心的部分我拆开讲三个要点。第一持仓状态下先判断止损和离场再判断加仓顺序不能颠倒——如果当天价格既触发了止损又满足了加仓条件必须先按离场处理否则会出现止损之后还加仓的荒谬状态。第二加仓循环用while而不是if因为价格可能一跳越过多个加仓档位比如一根大阳线直接涨了1.5个ATR那么应该从1个单位直接加到4个单位。第三入场后止损价不是固定的每加一次仓向有利方向移动0.5个ATR这是海龟法则里「移动止损」的具体落点。这里还有个细节容易被忽略unit_size的计算放在了循环外用的是第一天的ATR。更严谨的做法是每笔新交易开始时用当天的ATR重新计算单位数但这样会让代码复杂度上一个台阶作为入门版先固定住二次开发时再改成动态计算。3.4 回测引擎从position到权益曲线的最后拼图有了每天的仓位回测就是纯算术问题。逐日计算持仓市值、扣除手续费和滑点累计得到账户净值# backtest.py def run_backtest(df, config): capital config.init_capital equity_curve [] trades [] position 0 prev_close 0.0 for i in range(len(df)): row df.iloc[i] position row[position] if position 0: equity_curve.append(capital) prev_close row[close] continue # 持仓市值变化当日收盘价相对于昨收的变动 * 持仓单位数 * 单位股数 pnl (row[close] - prev_close) * position * position_size capital pnl # 当日有新开仓或平仓时记录一笔交易 equity_curve.append(capital) prev_close row[close] return equity_curve, trades回测引擎的核心逻辑就是逐行累加盈亏每天账户净值等于昨天的净值加上今天的持仓盈亏。这里position_size是前面算出来的单位股数它在实际项目中通常放在一个全局变量里由策略层和回测层共享。这个简化版回测有一个明显的取舍它以收盘价作为成交价没有模拟盘中价格触及止损线时的具体成交。在真实程序化交易里止损单是盘中触发的价格通常比收盘价更不利所以这份源码的回测结果会略微偏乐观。想更严谨可以在止损触发时用stop_price作为成交价替代收盘价这个改动放到后面二次开发部分讲。绩效统计部分用pandas直接算年化收益、最大回撤、夏普比率和胜率都是标准的金融指标计算逻辑简单但每天都用到。4. 运行复现与常见问题排查拿到代码后先跑通再踩坑4.1 运行环境准备与一键跑通运行这个项目只需要最基本的科学计算环境。Python 3.8以上版本pandas 1.x再加一个matplotlib画图不需要安装额外的量化框架。没有装过环境的同学可以直接用下面这组命令pip install pandas matplotlib python run.pyrun.py是入口脚本内部依次调用数据加载、指标计算、策略信号生成、回测撮合、绩效统计和画图执行完成后会在output/目录下生成equity_curve.png和trades.csv两个文件。整个过程跑完大概几秒钟你会看到终端打印出年化收益率、最大回撤、夏普比率、交易次数四个核心指标。如果你手头有自己下载的股票数据替换data/example.csv的时候注意保持列顺序和格式一致。数据量方面这份代码没有做任何性能优化日线级别三五千行跑起来毫无压力但如果你要塞进分钟线几十万行数据建议先把加仓循环的逻辑改成用numba加速否则回测会比较慢。4.2 回测输出怎么读别盯着胜率盯着盈亏比跑完回测终端会输出一行绩效摘要trades.csv里记录了每一笔交易的入场时间、入场价、离场时间、离场价、盈亏。我第一次跑通时看到胜率只有38%第一反应是策略是不是写错了后来仔细看了盈亏比——平均盈利是平均亏损的4倍多这才意识到海龟策略本来就是这样大部分交易小亏离场偶尔一波大趋势把前面所有亏损一次性赚回来。阅读回测结果时重点看三个指标最大回撤决定你能不能拿住这套策略夏普比率衡量风险调整后收益盈亏比决定策略是否有正的期望收益。如果某个参数改完胜率上升了但盈亏比掉了一半那这笔参数调整大概率是负优化因为海龟体系里「让利润奔跑」才是盈利的主要来源。4.3 踩坑清单五个反复出现的坑现象、原因、解决一次说清第一个坑日期列变成object类型。现象是画出来的净值曲线横轴乱序或者说rolling报错提示无法比较。原因是pd.read_csv默认把日期当成字符串。解决办法是显式加pd.to_datetime并指定格式已经在上文load_data里写好了。第二个坑未来函数导致收益虚高。现象是回测年化收益高得离谱完全不合理。原因是用当日收盘价确认信号后又用当日收盘价成交相当于偷看了收盘结果。正确做法是信号在收盘后产生最早到次日开盘才成交这份源码是直接在信号产生当日的收盘价成交的属于简化处理回测结果会偏乐观。改成次日开盘成交只需把成交逻辑整体往后挪一行。第三个坑ATR前段出现NaN。现象是equity_curve前20个点空缺或者计算中途报错。原因是窗口期不足前19天算不出20日ATR。解决办法是直接丢弃前段数据从第20天开始回测海龟原版本来就是等20根K线数据齐全后才开始交易这个行为符合策略定义。第四个坑加仓循环里用iloc造成数据错位。现象是多头加仓后仓位从1跳到4但止损价只上移了一次。原因是同时操作DataFrame行和循环变量索引容易错位。解决办法是把止损价和仓位都用Python原生变量维护循环结束后再统一写入DataFrame不要边算边写回。第五个坑止损价比较方向搞反。现象是做多时价格大跌但止损不触发做空时大涨也不触发。原因是把止损比较条件写成了单一方向比如永远判断low stop但空单的止损用的是high stop。解决办法是严格按持仓方向分流多空逻辑单独写不要试图合并成一条条件。5. 二次开发把海龟源码改造成自己的多品种组合回测框架跑通只是第一步真正让这份源码值钱的是改造成你自己的框架。我先说最小改动把config.py里的参数改成可扫描的网格用循环跑一遍不同参数组合观察哪个组合在你的数据上回撤最小。海龟原版的20日入场、10日离场是经验值不同市场环境下最优窗口差别很大这套源码的参数集中管理让这种扫描写起来特别顺手。再往前走一步就是改成多品种组合回测。核心思路是把每个品种独立跑一遍generate_position然后用权重把各品种的每日盈亏叠加成组合净值。之前说过position列是策略层和回测层之间的接口契约多品种版本不需要改策略层只需要在回测层按品种分组处理最后加权求和。这也是我一直觉得State型接口比闭包传参更适合回测框架的原因——数据是内存里的DataFrame加品种只是多一次循环复杂度不会爆炸。风控增强方面我建议从上文提到的单位数动态化入手。当前版本unit_size固定在循环外只用第一天ATR计算如果行情波动加大实际风险敞口会超过1%预算。改造方案是每笔新交易开仓时用当日ATR重新计算单位数这样高波动时期自动缩小仓位、低波动时期自动放大相当于把「固定分数风险模型」变成了「动态波动率预算模型」这种做法在程序化交易里更抗极端行情。我自己的习惯是每次改完参数或逻辑强制走一遍同样的验证流程先跑通再看最大回撤有没有变大最后对比交易次数和胜率变化。有一回我把入场窗口从20改成15跑出来年化收益涨了兴冲冲准备用结果发现最大回撤从18%涨到了31%一查是因为15日通道信号更频繁止损次数变多导致连续亏损段变长。从那以后我就记住了改策略先看回撤再谈收益回撤失控的策略收益再高也拿不住。希望帮到你。本文还有配套的精品资源点击获取