
简介一份以pandas库实现海龟交易法则的资源包面向量化交易入门者与Python数据分析爱好者完整演示了趋势跟踪策略从数据获取、清洗、SMA/EMA指标计算到突破信号生成、ATR止损和仓位管理的全流程并包含数据频率转换、缺失值处理等细节。压缩包共3个文件含1个Python脚本和2个CSV数据文件总体积41KB脚本提供可运行的核心策略逻辑CSV文件包含股票历史行情数据便于本地复现和参数测试。已有1299人学习下载。通过研读代码可以掌握20日与10日趋势突破、2倍ATR止损、头寸规模计算等海龟法则核心要点并学会用pandas高效处理金融时间序列数据文件也支持替换其他标的进行二次验证便于开展对比实验。适合有一定Python基础、希望从零搭建量化策略回测框架并深入理解趋势跟踪策略原理的读者动手实践。1. 海龟交易法则核心思想与策略拆解1.1 海龟交易法则的前世今生为什么这套30年前的策略依然能打海龟交易法则诞生于1983年当时著名交易员理查德·丹尼斯提出了一个赌局他可以把普通人训练成优秀的交易员就像新加坡养海龟一样批量培养。实验结果是这批“海龟”确实跑出了惊人业绩于是这套完整的规则被记录下来成为量化交易史上的经典。这套策略的底层逻辑并不复杂核心就是一个词趋势跟踪。它只做一件事——在价格突破关键位置时进场在趋势延续时加仓在趋势反转时退出完全放弃预测行情靠“截断亏损、让利润奔跑”来赚取不对称的收益。它在期货、股票、加密货币上都适用尤其是那些经常走出大单边行情的品种效果最明显。那为什么要用Python的pandas库来实现因为海龟法则的每一条规则本质上都是对历史数据的时间序列计算。唐奇安通道需要滚动窗口求最高价和最低价ATR需要递归计算平均真实波幅信号持仓需要逻辑移位对齐——这些操作pandas都提供了矢量化接口不需要写一层层for循环几十行代码就能把完整策略跑起来回测效率远超手动Excel也比直接写原生Python循环快两个数量级。如果读者是刚接触pandas这段代码也是极好的练手项目因为它把数据清洗、滚动计算、分组聚合、信号对齐全串起来了。1.2 策略完整规则一览进场、加仓、止损、退出海龟法则不是单一策略而是一个规则体系。我做回测时会把规则拆成四层每一层对应一个独立函数方便后续调参和排查。第一层是进场规则标准版用的是20日唐奇安通道突破也就是当收盘价突破过去20个交易日的最高价时做多进场跌破20日最低价时做空进场。原版还有一套55日突破的“系统二”用于捕捉更长周期的趋势两个系统同时运行一个仓位轻、一个仓位重。第二层是加仓规则进场后价格每朝着有利方向移动0.5个ATR平均真实波幅下文会细讲就加一次仓单笔交易最多加仓到4个单位。第三层是止损规则用2ATR作为初始止损距离也就是进场价一旦反向波动超过2个ATR无条件离场。这是整套策略的灵魂它保证任何一笔交易的亏损都在可控范围内。第四层是退出规则系统一用10日反向突破退出系统二用20日反向突破退出。有经验的交易者还可以加一个移动止损把止损线随着价格抬高而抬高锁定更多利润。2. 用pandas实现唐奇安通道与信号生成2.1 数据准备先解决“喂给策略的原料从哪里来”写策略之前要先搞到干净的行情数据。我常用yfinance拉美股和部分A股数据接口免费返回的就是标准的DataFrame这是pandas最舒服的输入格式。A股如果数据拉不全也可以换akshare或者tushare只要包含open、high、low、close、volume这五列就行。import pandas as pd import numpy as np import yfinance as yf # 拉取日线数据这里以沪深300ETF为例 df yf.download(510300.SS, start2018-01-01, end2024-12-31, auto_adjustTrue) df df[[Open, High, Low, Close]].copy() df.columns [open, high, low, close] # 删除缺失值避免后续rolling计算出NaN df.dropna(inplaceTrue)这里有个小坑yfinance拉的数据默认包含股息调整如果做回测我建议使用auto_adjustTrue它对复权价格处理得更合理避免除权除息造成的假突破信号。数据拿到之后必须先检查有没有空行和缺失值否则后面rolling的计算结果会莫名其妙出现一整片NaN排查起来很浪费时间。2.2 唐奇安通道的矢量化计算三行代码搞定唐奇安通道的定义很朴素通道上轨是过去N日最高价的最大值下轨是过去N日最低价的最小值。用pandas实现这个方法就是rolling窗口加max/min聚合天然比循环写法快一个数量级。# 系统一20日通道 df[upper_20] df[high].rolling(window20).max() df[lower_20] df[low].rolling(window20).min() # 系统二55日通道 df[upper_55] df[high].rolling(window55).max() df[lower_55] df[low].rolling(window55).min()写到这里我要提醒一个问题信号必须用shift(1)做一天滞后。因为“突破”这个动作要等到当天收盘之后才确认如果你在当天盘中用当天的最高价去判断是否突破等于用了未来数据回测结果会虚高得离谱。正确做法是拿今天的收盘价去和“昨天算出来的通道上轨”比较。# 用shift(1)避免未来函数 df[entry_long] df[close] df[upper_20].shift(1) df[entry_short] df[close] df[lower_20].shift(1) df[exit_long] df[close] df[lower_10].shift(1)很多初学者会在这里犯错。我第一次写的时候也偷懒没加shift结果回测年化收益率翻了一倍当时还挺高兴后来逐笔核对交易记录才发现是未来函数在作弊。这种错误在实盘里根本不存在盈利可能所以回测前一定要养成检查“数据是否泄漏”的习惯。2.3 从布尔信号到持仓状态用fill与逻辑运算避免重复进出场信号生成之后下一个核心问题是怎么把“某一瞬间的触发事件”转换为“持仓状态”。因为连续几天价格都可能在上轨上方如果不做处理程序会每天发一次买入指令仓位就会失控。我的处理方法是先记录原始的进场意向和离场意向再通过状态机逻辑决定当前持仓。对趋势跟踪策略来说最简单的方式是一旦进场信号出现就保持持仓状态直到退出信号出现才平仓。用pandas可以这样实现df[position] 0 in_long False for i in range(len(df)): if not in_long and df[entry_long].iloc[i]: in_long True elif in_long and df[exit_long].iloc[i]: in_long False df[position].iloc[i] 1 if in_long else 0如果数据量很大纯Python循环确实慢。优化技巧是先把布尔序列转成整数再用diff判断状态变化的关键点只在关键点做循环或者直接用numba加速几千行数据几毫秒就跑完了。但为了可读性我一般在策略原型阶段保留这个循环等确认逻辑正确后再优化性能。3. ATR与仓位管理海龟法则最值钱的环节3.1 为什么用ATR而不是百分比止损波动率自适应的秘密海龟法则里的仓位管理和止损全部围绕一个指标展开——ATR即平均真实波幅Average True Range。它衡量的是最近一段时间内价格平均每天波动多少个点。波动大的品种ATR就大仓位就轻波动小的品种ATR就小仓位就可以重一些。为什么要用ATR而不是固定百分比因为不同品种的波动特征完全不同。茅台一天的振幅可能是2%螺纹钢可能是1.5%比特币可能是5%如果都用2%止损有些品种一天就打掉止损有些品种可能要扛半个月。ATR相当于一把“自适应标尺”把每个品种的波动拉到一个可比较的量纲上。真实波幅TR的定义是三者中的最大值当天最高价减最低价、当天最高价减昨收的绝对值、当天最低价减昨收的绝对值。用pandas可以完美矢量化def calc_atr(df, period20): prev_close df[close].shift(1) tr np.maximum( df[high] - df[low], np.maximum( (df[high] - prev_close).abs(), (df[low] - prev_close).abs() ) ) df[fatr_{period}] tr.rolling(windowperiod).mean() return df df calc_atr(df, 20)注释里补充一下原版海龟用20日ATR做加减仓和止损计算也就是所谓“N值”。有的实现版本里用的是Wilder平滑即ewm(alpha1/period)两种差别不大我习惯用简单移动平均代码更直观调参时也更容易理解。3.2 单位仓位计算1%风险原则海龟法则里一个非常重要的概念是“单位”Unit。一个单位对应的仓位大小要让“价格反向波动1个ATR”也就是N造成的资金亏损约等于账户总资金的1%。公式写出来非常简洁单位仓位 (账户资金 * 1%) / (ATR * 合约乘数)对应pandas代码假设账户资金100万每个单位风险1%交易品种是沪深300ETF合约乘数为1account_capital 1_000_000 risk_per_unit account_capital * 0.01 # 1万元 unit_size risk_per_unit / df[atr_20] # 每个单位能买多少份这样算出来的unit_size就是当日开盘可以买入的目标份数。注意仓位大小是每天都在动态变化的因为ATR每天都在变所以如果你严格执行原版海龟法则应该每天都根据最新ATR调整仓位而不是进场时算一次就一劳永逸。3.3 加仓与止损逻辑的实现0.5N间隔和2N硬止损原版海龟的加仓规则是进场后价格每朝有利方向变动0.5N就加仓一次最多加到4个单位。止损规则是进场价反向波动2N立刻止损。用pandas实现这套规则核心在于记录每一笔持仓的开仓均价。我在回测里用一个简单的方法维护一个entry_price序列当进场信号触发时记录当前收盘价之后每次加仓时更新持仓均价。df[units] 0 df[entry_price] np.nan for i in range(len(df)): if df[entry_long].iloc[i] and df[units].iloc[i-1] 0: df.loc[df.index[i], units] 1 df.loc[df.index[i], entry_price] df[close].iloc[i] elif df[units].iloc[i-1] 0 and df[units].iloc[i-1] 4: # 加仓条件价格比上次加仓价高0.5N if df[close].iloc[i] df[entry_price].iloc[i-1] 0.5 * df[atr_20].iloc[i]: df.loc[df.index[i], units] df[units].iloc[i-1] 1 df.loc[df.index[i], entry_price] df[close].iloc[i] else: df.loc[df.index[i], units] df[units].iloc[i-1] df.loc[df.index[i], entry_price] df[entry_price].iloc[i-1] else: df.loc[df.index[i], units] df[units].iloc[i-1] df.loc[df.index[i], entry_price] df[entry_price].iloc[i-1]止损实现更简单持仓期间一旦收盘价跌破entry_price - 2 * atr_20就在次日平仓这也是用shift(1)避免用到当日收盘时才知道的信息。实盘交易中这个止损单一般直接挂在交易所价格碰到就触发回测里则用收盘价判断两者会有滑点差异但长期统计下来影响不大。4. 完整回测框架从信号到净值曲线的落地实现4.1 回测主循环与持仓收益计算把前面所有零件组合起来就是一个完整的回测框架。思路是每天先检查是否触发止损或退出信号决定是否平仓再检查是否触发进场或加仓信号决定是否买入。def run_backtest(df, initial_capital1_000_000): df df.copy() df calc_atr(df, 20) df[upper_20] df[high].rolling(20).max() df[lower_20] df[low].rolling(20).min() df[upper_55] df[high].rolling(55).max() df[entry_long] df[close] df[upper_20].shift(1) df[exit_long] df[close] df[lower_20].shift(1) position 0 cash initial_capital entry_price 0 equity_curve [] for i in range(len(df)): price df[close].iloc[i] atr df[atr_20].iloc[i] # 先处理退出 if position 0 and df[exit_long].iloc[i]: cash position * price position 0 entry_price 0 # 再处理进场 if position 0 and df[entry_long].iloc[i]: unit int((cash * 0.01) / atr) position unit entry_price price cash - unit * price # 记录每日总资产 equity cash position * price if position 0 else cash equity_curve.append(equity) df[equity] equity_curve return df这段代码做了很多简化比如没有处理手续费和滑点也没有实现空头方向。但如果只是验证策略逻辑能不能跑通这个框架已经足够。我在实际项目里会把交易记录单独存成一个列表每条记录包括开仓日期、开仓价、平仓日期、平仓价、仓位、盈亏方便逐笔复盘。4.2 绩效指标年化收益、最大回撤、夏普比率回测不能只看总收益率必须看风险调整后的收益。我用pandas算三个核心指标年化收益率、最大回撤、夏普比率。def calc_performance(df, risk_free_rate0.03): df[daily_return] df[equity].pct_change() total_return df[equity].iloc[-1] / df[equity].iloc[0] - 1 years len(df) / 252 annual_return (1 total_return) ** (1 / years) - 1 # 最大回撤滚动累计净值的最低点 cummax df[equity].cummax() drawdown df[equity] / cummax - 1 max_drawdown drawdown.min() # 夏普比率 excess_return df[daily_return] - risk_free_rate / 252 sharpe excess_return.mean() / excess_return.std() * np.sqrt(252) return { total_return: total_return, annual_return: annual_return, max_drawdown: max_drawdown, sharpe: sharpe }夏普比率的含义是“每承受一单位风险能换来多少超额收益”一般在1以上就算不错海龟策略在趋势明显的年份能做到1.5到2但在震荡市里会很难看。最大回撤则是心理承受能力的试金石海龟策略的最大回撤经常达到20%到30%如果没做好这个心理准备很容易中途放弃这是趋势策略的固有代价。4.3 可视化净值曲线和回撤图绩效指标解释不了全部问题一定要画图看资金曲线的形态。用matplotlib画净值曲线和回撤区间能直观判断策略是“稳步上涨”还是“靠一两波大行情翻身”。import matplotlib.pyplot as plt fig, ax plt.subplots(2, 1, figsize(12, 8), sharexTrue) ax[0].plot(df.index, df[equity], labelEquity) ax[0].set_title(Equity Curve) ax[0].legend() drawdown df[equity] / df[equity].cummax() - 1 ax[1].fill_between(df.index, drawdown * 100, 0, colorred, alpha0.3) ax[1].set_title(Drawdown (%)) plt.tight_layout() plt.show()画完图之后要注意一个常见现象资金曲线往往长期横盘甚至阴跌然后在某几个月突然拉升。这是趋势跟踪策略的正常表现收益分布极不均匀可能80%的利润来自20%的时间段。看回测结果的时候不要只盯着年化收益要重点问一个问题这波利润主要来自哪一年、哪一段行情如果这段行情消失策略还剩多少竞争力5. 常见问题与实战排查技巧5.1 pandas版本升级后rolling行为变化pandas 2.0之后rolling的默认行为有过细微调整早期版本中min_periods默认为窗口大小现在部分版本会默认接受NaN。如果发现自己回测结果和别人对不上优先检查rolling函数的min_periods参数建议显式设置df[upper_20] df[high].rolling(window20, min_periods20).max()这样代码更明确不会因为pandas版本升级导致结果漂移。5.2 数据对齐问题时区、停牌日与非交易日跨市场数据最坑的是时区和对齐。A股和美股节假日不同直接合并两个DataFrame会出现大量NaN。我的处理办法是把索引统一转成pd.Timestamp再用reindex对齐到同一个交易历上缺失数据用ffill填充但要注意如果停牌超过一定天数前向填充的价格会失真应该直接删除这些日期。5.3 未来函数自检清单这是回测中最隐蔽也最致命的坑。我每次写完回测都强制自查三个地方信号是否有shift(1)滞后确保用的是“昨日收盘时已知道的数据”。止损判断是否用到当日最高价或最低价如果用当日价格触发止损意味着你假设自己能精确买在最低点这在实盘不可能实现。仓位计算是否用到了当日的收盘价决定当日的买入量正确做法是用昨日收盘后的ATR决定今日的操作计划。这三条如果有一条没守住回测结果就是“理论上可行、实盘必亏”的幻觉。我自己吃过大亏有段时间策略回测年化翻倍一上实盘连续亏损最后发现是止损逻辑里多用了当日最低价相当于每次止损都假设自己精准卖在了最高点。5.4 参数敏感性与过度优化海龟法则最优秀的地方在于参数鲁棒性强。20日突破、55日突破、2ATR止损、0.5N加仓这些参数在不同品种、不同时间段表现差异不大。如果你在回测里把参数调成“15日突破、37日突破、1.7ATR止损”之后年化突然暴涨这大概率不是发现了圣杯而是过拟合。我做参数敏感性分析时会把参数附近一个区间都跑一遍看结果是否平滑。比如20日通道我会同时测15、18、20、23、26日如果收益起伏剧烈说明策略不稳定这种结果宁可不用。海龟法则的原版参数已经经过长期市场检验我建议新手不需要过度优化先把规则完整实现一遍理解每一条规则的作用之后再做调整。6. 实盘落地中的补充思考回测跑通只是第一步。海龟法则在实盘落地时有几个容易被忽略的细节值得单独拿出来说。第一个是滑点和交易成本。回测里如果完全不扣手续费和滑点结果会虚高不少。A股ETF双边手续费大约万三到万五期货品种要看合约乘数和点差。我的习惯是在回测里保守一点每笔交易扣掉万五手续费再加一个固定滑点比例宁可结果难看一点实盘也别太意外。第二个是资金管理。海龟原版是满仓干多个品种、多个市场分散风险本质上是靠“多个不相关趋势同时出现”来平滑净值曲线。如果只做一个品种资金曲线会非常颠簸连续几次止损之后很容易失去信心。所以如果条件允许最好同时跑5个以上品种比如不同的ETF或者不同板块的品种相关性越低越好。第三个是心态问题。趋势跟踪策略天然是“低胜率、高盈亏比”海龟法则的胜率可能只有35%到40%但赚一次能覆盖亏三次。这和执行层面完全是两回事——连续止损五六次之后很少有人能坚持按规则继续开仓。这也是为什么很多量化团队把执行交给程序自动完成就是为了去掉人性和情绪干扰。如果手动操作建议先把规则写死在脚本里程序只负责提醒信号交易员只负责按下确认键。7. 本次实现的项目源码结构最后把完整项目的模块结构列出来方便读者直接照着搭。我平时会在项目根目录下建data/、strategy/、backtest/、results/四个文件夹各司其职回测结果统一导出成CSV存到results/里方便后续分析。turtle_trading_pandas/ ├── data/ │ └── download_data.py # 数据下载与预处理 ├── strategy/ │ ├── donchian.py # 唐奇安通道计算 │ ├── atr.py # ATR计算 │ └── turtle.py # 海龟策略主逻辑 ├── backtest/ │ └── engine.py # 回测引擎与绩效计算 ├── results/ │ └── (输出净值曲线、交易记录等) └── main.py # 主入口main.py只需要几十行负责串联数据、策略、回测最后打印绩效表格。这样拆分的意义在于后续想换数据源、调整参数、换品种只需要改对应模块不需要动主逻辑。我在实际写这个项目时最有成就感的时刻不是回测年化跑出多少而是看着一套三十年前用纸笔执行的交易规则被几十行pandas代码完整复现出来逻辑清晰、运行快速、结果可复现。这套代码给了我很大的底气去验证各种关于策略的假设——比如去掉加仓会怎样、把ATR换成波动率会怎样、在熊市里用55日通道会不会更好——而这些验证过程正是把交易从“感觉”变成“工程”的关键一步。本文还有配套的精品资源点击获取