ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用Python与pandas实现海龟交易法则:从ATR到完整回测

用Python与pandas实现海龟交易法则:从ATR到完整回测 简介海龟交易法则的Python语言与pandas数据分析库实现示例包面向量化交易、程序化交易及股票数据分析初学者提供一套结构清晰、易于运行的趋势跟踪策略范例。压缩包内共3个文件包含一个Python脚本和两个CSV数据文件脚本承担策略逻辑实现CSV文件提供用于回测演示的历史行情数据整体大小约41KB轻量易用。目前已有1299人学习下载。脚本基于经典海龟策略框架完成突破检测、移动平均线计算、买卖信号生成与模拟交易流程并在pandas环境中实现了时间序列处理、缺失值填充和交易结果统计读者借助这两个数据文件可以直观掌握金融数据的载入与清洗、技术指标的计算和简单回测方法甚至调整参数比较不同行情下的策略表现是入门量化策略开发的实用参考。1. 海龟交易法则的本质与实现思路海龟交易法则在我刚接触量化那阵子简直是绕不开的一座山。别人嘴里“趋势跟踪”三个字说出来特别轻巧但真到落地的时候你会发现自己面对的是一个体量相当完整的规则系统20日唐奇安通道开仓2倍ATR止损按0.5N间隔逐步加仓最后用反向突破离场。这套系统最反直觉的地方在于——它根本不预测行情它只做一件事在趋势走出来的时候果断上车在趋势反转的时候及时跳车然后靠盈亏比去赚钱。用Python和pandas把这套规则写出来既是对交易逻辑的再一次审视也是磨练数据处理和回测功底非常合适的练手项目。我决定用pandas而不是Excel或者纯Python列表来实现这套策略核心原因有三个。第一pandas天生就是为时间序列设计的交易数据天然带日期索引滚动窗口、移位对齐、缺失值处理这些操作都有现成接口不用自己造轮子。第二pandas的向量化计算在处理ATR、唐奇安通道这种指标的时候一秒钟能同时算完上千只股票速度比逐行循环高好几个量级。第三后边做数据分析、画资金曲线、统计交易指标pandas和matplotlib这套组合几乎是标配。当然要提前给大家打个预防针海龟交易法则是一个带有大量状态记忆的系统比如当前持了多少仓、已经加了几次仓、止损价在哪这些信息无法靠单纯的向量化一次性算出来所以整体实现思路会分成两层——前期用pandas做“指标计算”后期用一个逐K线的主循环做“状态机模拟”。如果你期待一个纯向量化的炫技版本那可能要先调整一下预期完整、可验证比代码好看重要得多。这套系统的适用对象也很明确已经会Python基础语法、能看懂DataFrame操作但还没完整写过一遍策略回测的量化入门者。如果你是老手这篇文章里关于未来函数、止损优先级的讨论也大概率能帮你抓到之前没注意过的回测漏洞。1.1 核心规则拆解在动键盘之前一定要先把规则背下来代码只是规则的翻译器。经典海龟系统分两套实际中经常是两套同时运行谁先出信号谁生效入场System 1用20日突破入场System 2用55日突破入场加仓每上涨0.5N加一次仓最多加到4个单位止损任一单位头寸亏损达2N时全部止损退出退出System 1用10日反向突破退出System 2用20日反向突破退出这里的N指代的是当前市场的波动水平也就是20日ATR。把仓位的大小、止损的宽窄、加仓的间隙全部跟N挂钩本质上是做一个自适应调节行情波动大的时候单次仓位小一点、止损宽一点行情波动小的时候单次仓位大一点、止损窄一点。这样做的好处是任何一笔交易的亏损占账户的比例被牢牢锁住海龟们赌的是“我可能连续亏十次但一次成功就全部扳回来还带拐弯”。1.2 为什么用pandas而不是其他方案我见过不少人用Excel表格来实现唐奇安通道突破拉几个公式也能跑但一旦要加进“加仓后止损价要跟着最新加仓价移动”这种逻辑公式就会变得像蜘蛛网一样复杂。pandas虽然也要写不少代码但它胜在逻辑清晰指标用列来算状态存成变量每一行代表一根K线一行一行地读下去就像是自己在复盘一样。另外pandas在后处理阶段优势太大了。回测跑完之后交易记录是一个二维表资金曲线是一段时间序列收益率、最大回撤、夏普比率、胜率、盈亏比这些小指标全是pandas几个统计方法直接出来的结果不需要再导到别的工具里二次加工。2. 数据准备与核心指标计算2.1 数据从哪里来、怎么清洗写回测最重要的不是策略有多复杂而是数据干不干净。这里给两个方案没有现成行情数据的话先用随机游走模拟数据把整个流程跑通真实数据我倾向于推荐akshare这个开源库一句pip install akshare就能装好然后可以这样拉A股日线数据import akshare as ak df ak.stock_zh_a_hist( symbol600519, perioddaily, start_date20200101, end_date20241231, adjustqfq ) df.head()如果你以前一直用Excel炒股第一次看到df.head()输出的结果时应该会很兴奋日期、开盘、收盘、最高、最低、成交量整整齐齐地躺在DataFrame里后面所有计算都是从这个地方开始的。注意三点第一一定要用前复权数据否则除权除息那几天会出现价格“跳崖”把突破信号搞得乱七八糟第二列名统一改成open、high、low、close的小写格式方便后面代码里引用第三索引改成日期并确保是时间类型否则rolling和shift的行为会变得不可预测。如果你没有akshare或者不想装额外环境可以用一段简单模拟数据先跑逻辑import numpy as np import pandas as pd np.random.seed(42) dates pd.date_range(2023-01-01, periods500, freqD) close 100 np.cumsum(np.random.randn(500) * 1.5) np.linspace(0, 30, 500) high close np.random.rand(500) * 2 low close - np.random.rand(500) * 2 df pd.DataFrame({ open: close np.random.randn(500) * 0.5, high: high, low: low, close: close, volume: np.random.randint(10000, 50000, 500) }, indexdates)我想强调一点回测流程本身能不能跑通跟行情数据长什么样没太大关系。先跑通再换真实数据这是我从debug地狱里爬出来以后总结出的最实用方法论。2.2 计算ATR海龟法则的“尺子”ATR的中文名是真实波幅均值它衡量的是市场每天真实的跳动范围。计算步骤分两步先算TR再算N。TR的计算公式看起来有三组比较逻辑其实很直白今天的真实波幅是“今天最高到最低的距离”“今天最高到昨天收盘的距离”“昨天收盘到今天最低的距离”这三者中最大的那个。为什么要取最大因为如果今天跳空高开或者低开那么从昨天收盘到今天的最高或最低才是市场真正扔出来的波动范围光看今天的K线柱子会漏掉跳空信息。pandas写出来非常简洁df[prev_close] df[close].shift(1) df[TR] np.maximum( df[high] - df[low], np.maximum( abs(df[high] - df[prev_close]), abs(df[low] - df[prev_close]) ) ) df[N] df[TR].ewm(span20, adjustFalse).mean()这里关于N的计算我要多说一句原版海龟规则手册里明确写的是20日指数移动平均EMA而不是简单移动平均SMA。两者的差异在于EMA对最近几天的波动变化反应更快一旦市场波动突然放大仓位会被更及时地调小。用ewm(span20, adjustFalse)就实现了一个标准的20日EMA。当然市面上也有大量复现版本用df[TR].rolling(20).mean()我觉得在日线级别上差异不大但如果你追求跟原版规则更贴近建议用EMA。2.3 唐奇安通道最容易写错的地方唐奇安通道的本质就四个字——创N日新高新低。但它有一个特别容易踩的坑必须排除当天。如果你直接写df[high].rolling(20).max()那么今天的最高价会进入今天的通道上轨这时候今天的最高价天然大于或等于通道上轨突破信号就变成了“今天创新高就触发”永远不可能会错这其实是把未来函数直接写进去了。正确的写法是entry_n 20 exit_n 10 df[entry_up] df[high].rolling(entry_n).max().shift(1) df[entry_down] df[low].rolling(entry_n).min().shift(1) df[exit_up] df[high].rolling(exit_n).max().shift(1) df[exit_down] df[low].rolling(exit_n).min().shift(1)shift(1)才是整个指标计算中最重要的一个方法它把通道信号全部滞后一天保证我们使用的数据只包含今天之前的行情。换句话说今天的通道上轨是“过去20天不含今天的最高价”今天的价格必须真正意义上突破这个位置才算突破成功。顺便提一个细节如果做A股可以顺手过滤掉上市时间不足60个交易日的次新股以及ST类标的这批股票要么波动异常要么存在交易规则限制放进海龟系统里会严重干扰回测结果。3. 交易规则与回测引擎实现指标算完接下来就是真正的“下棋”逻辑。为什么说海龟系统像一个下棋状态机因为在任何一根K线上程序都必须知道四件事现在手里有没有仓位仓位方向是多是空已经加了几次仓当前的止损价在哪。这四个状态全部走完才能决定这一根K线该干什么。3.1 入场信号如何判定多头入场的条件本质上是“收盘价突破20日通道上轨”。这里有一个细节值得展开实际交易中盘中价格一旦突破通道上轨海龟就会立即市价入场但日线回测里我们只能看到每天的最高价、最低价、收盘价没法追踪盘中的每一笔成交。所以常见的两种处理方式是用high entry_up作为突破条件入场价记为entry_up或者当天收盘价用close entry_up作为突破条件入场价记为次日开盘价我倾向于用第一种偏激进的判断但同时在成交价上统一加一个滑点成本用这种方式来抵消“想得美”的误差。毕竟真实成交的时候你以为你能在通道位买到实际上可能多付出几个跳动点。long_signal df[close] df[entry_up] short_signal df[close] df[entry_down]注意这里如果用close entry_up就代表收盘价确认突破信号不会在当天盘中触发但为了简单很多回测框架也是这么干的差别不大。如果是做空国内A股普通账户没法直接做空但如果你在期货市场或者模拟盘这一套逻辑是通用的方向反过来就行。3.2 仓位管理和加仓逻辑仓位大小是整个海龟系统最有特色的部分。它不按“买几手”来算而是按“买几个单位”来算。一个单位头寸的仓位公式是单位数量 账户总资金的1% / N × 每点价值在股票场景下可以把“每点价值”简化为1直接理解为账户资金的1%除以当前波动幅度N得到你愿意承受的仓位数量。举个例子账户100万元N2那么一个单位的仓位就是1000000 × 0.01 / 2等于5000股。然后你实际的止损距离是2N也就是4块钱所以这一个单位真正亏损的最大金额是5000 × 4 20000元正好是账户的2%。这个设计最大程度地保证了不管市场多震荡、止损被反复打掉单笔亏损始终在可控范围内。加仓规则是价格每朝有利方向移动0.5N就增加一个单位但最多加到4个单位。为什么是0.5N因为海龟体系的核心逻辑是“浮盈加仓”只有品种走势证明了自己才值得投入更多资金。对应到代码里加仓间隔和最大加仓次数都做成变量position 0 # 当前持仓单位数正数表示多头 units 0 # 已行使的加仓次数 last_entry_price 0 # 最近一次建仓/加仓价格 atr 0 # 当前N值 position_value 0 # 当前持仓股数/手数每次加仓之后止损价要跟着最新建仓价移动。这个移动逻辑极其关键下一小节会讲到。3.3 止损与退出优先级止损的规则说起来只有一句任意单位头寸亏损达到2N当场平仓。但代码层面最麻烦的是“优先级”问题。一根K线上可能开盘价先击穿止损价盘中又拉升触发0.5N加仓条件如果先判断加仓你的止损价已经上移这笔单子可能反而被救回来了但如果实际盘面是止损先被打掉那就是真亏了。出于保守估计回测里应该先处理止损再判断加仓。退出规则用的是10日反向突破也就是多头持仓时如果价格跌破过去10日不含当日的最低点无条件离场。这一个退出逻辑和2N止损构成了双保险行情温和反向时10日突破帮你及时撤退行情剧烈反向时止损单替你兜底。写出来大概是# 多头出场判定 exit_signal low df[exit_down] # 跌破10日低点 # 止损判定 stop_loss_hit low (last_entry_price - 2 * atr)一旦触发退出当根K线剩余的任何动作都不再执行这需要在整个主循环里用continue或者状态标记来保证逻辑唯一性。4. 完整回测实战4.1 逐K线主循环基础数据准备好以后用逐K线循环来模拟整个交易过程。这个循环不复杂但状态变量多建议写成类或者函数封装起来。一个可以直接运行的简化版本如下import numpy as np import pandas as pd def run_turtle_backtest(df, entry_n20, exit_n10, atr_n20, risk_pct0.01, initial_cash1000000): data df.copy() # 指标计算前文已实现这里简写 data[prev_close] data[close].shift(1) data[TR] np.maximum( data[high] - data[low], np.maximum(abs(data[high] - data[prev_close]), abs(data[low] - data[prev_close])) ) data[N] data[TR].ewm(spanatr_n, adjustFalse).mean() data[entry_up] data[high].rolling(entry_n).max().shift(1) data[entry_down] data[low].rolling(entry_n).min().shift(1) data[exit_down] data[low].rolling(exit_n).min().shift(1) data[exit_up] data[high].rolling(exit_n).max().shift(1) cash initial_cash position 0 # 正数多头负数空头0空仓 units 0 last_entry_price 0.0 max_units 4 trades [] equity_curve [] for i, idx in enumerate(data.index): row data.iloc[i] atr row[N] if np.isnan(atr) or np.isnan(row[entry_up]): cash position * row[close] # 处理开头数据未就绪空仓不动 equity_curve.append(cash position * row[close]) continue # 1) 优先处理多头持仓的止损和退出 if position 0: stop_price last_entry_price - 2 * atr if row[low] stop_price: exit_price min(row[open], stop_price) cash position * exit_price trades.append((exit_long_stop, idx, exit_price, position)) position 0 units 0 elif row[low] row[exit_down]: exit_price row[open] if row[open] row[close] else row[close] cash position * exit_price trades.append((exit_long_channel, idx, exit_price, position)) position 0 units 0 # 2) 空头持仓处理逻辑与多头对称略方向反着来即可 # 3) 没有持仓则判断入场 if position 0: if row[close] row[entry_up]: unit_vol (cash * risk_pct) / max(atr, 1e-9) cost unit_vol * row[close] if cost cash: unit_vol cash / row[close] cash - unit_vol * row[close] position unit_vol units 1 last_entry_price row[close] trades.append((entry_long, idx, row[close], unit_vol)) elif row[close] row[entry_down]: # 做空逻辑类似这里不展开 pass # 4) 多头持仓加仓判断 elif position 0 and units max_units: if row[high] last_entry_price 0.5 * atr: unit_vol (cash * risk_pct) / max(atr, 1e-9) cash - unit_vol * row[close] position unit_vol units 1 last_entry_price row[close] trades.append((add_long, idx, row[close], unit_vol)) equity_curve.append(cash position * row[close]) data[equity] equity_curve return data, trades这段代码简化掉了空头、手续费和滑点但把多头完整的加仓和离场逻辑串起来了。写回测代码最忌讳的是一口气把全部逻辑写完再去跑那样报错的时候你都不知道是行情数据问题、指标算错还是状态变量没更新。我强烈建议先跑通“入场→止损退出”的最小闭环再加“10日反向退出”再加“加仓逻辑”一步一步来。4.2 回测结果指标怎么看跑完之后用pandas提取交易记录和资金曲线然后算最能说明问题的几个指标。资金曲线是每一根K线末尾的cash position * close组成的列表转成Series以后算收益率、最大回撤都极其方便equity pd.Series(equity_curve, indexdata.index) returns equity.pct_change().dropna() total_return equity.iloc[-1] / equity.iloc[0] - 1 drawdown equity / equity.cummax() - 1 max_drawdown drawdown.min() print(f总收益率: {total_return:.2%}) print(f最大回撤: {max_drawdown:.2%})胜率和盈亏比可以从trades里拆把入场交易和出场交易配对分别统计盈利次数、亏损次数、平均盈利金额、平均亏损金额。海龟策略的特征是胜率通常不超过40%甚至有时只有30%左右但盈亏比能做到2倍以上账户靠少数几次大趋势赚钱。如果你跑出来的结果胜率奇高先别高兴八成是逻辑写漏了什么比如止损没生效或者退出条件变成了“只赚不亏”。4.3 模拟数据实测效果我用上面的代码配合前面那段500根K线的随机游走模拟数据跑了一遍。由于行情是带趋势漂移的np.linspace(0, 30, 500)叠加随机数趋势跟踪系统天然能吃到这部分趋势收益。跑完之后交易记录大概十来笔其中有几笔是连续小亏然后一两笔大赚把前面的亏损全部覆盖。这种“低胜率、高盈亏比”的结果恰恰是海龟系统该有的样子。如果你想验证系统在纯震荡行情中的表现可以把close改成不叠加线性趋势、只保留随机波动的序列你会发现止损会频繁被触发资金曲线一路阴跌。这不是bug而是趋势跟踪系统的天然短板——震荡行情里它就是会被反复打脸。做策略不能只拿一段顺风数据给自己看顺风和逆风场景都得测一遍心里才有底。5. 常见问题与踩坑实录5.1 未来函数防不胜防这是新手回测翻车第一大原因。未来函数说白了就是你的策略在计算时偷偷使用了当天甚至未来的数据导致回测结果虚高。常见的雷点有三个唐奇安通道没有shift(1)导致当天突破天际地出现用当天的close去计算指标后再判断当天的入场而实际收盘前你根本不知道收盘价止损判断时用了当天的low和当天的entry_up入场和出场在同一天逻辑上互相打架我的习惯是写完指标计算之后单独打印出最近五天的几列关键数据肉眼检查一下“今天的通道上轨是不是昨天之前就存在”。这比任何测试代码都管用。5.2 同一天先止损还是先加仓刚才提过这个优先级问题再往深里说一层。假设你原来建仓价100元N是5元止损位在90元加仓价在102.5元。今天的K线最低到88元、最高到105元按规则止损和加仓同时被触发。如果程序先判断加仓就会在102.5元加上一单位然后止损位被推到97.5元最后变成你多亏了一笔如果先判断止损按90元清仓之后涨幅跟你没有关系。真实交易里盘口是动态的不一定谁先出现但回测要得到偏保守、偏安全的结果应该先处理止损再处理加仓。这一点确认了代码的可信度才高。还有一个细节止损价格如果真的低于当天的开盘价那开盘就应该直接成交成交价记开盘价如果开盘价高于止损价盘中才触发的理论上成交价在止损价附近。简化的处理方式就是min(row[open], stop_price)偏保守一些宁可多记一点亏损。5.3 手续费、滑点与极端情况很多人写第一个回测时不加手续费和滑点跑出来年化收益率30%一兴奋就准备实盘结果被真实市场摩擦成本磨到亏损。尤其是海龟这种突破入场、破位离场的高频交易系统交易次数不算少滑点和手续费的影响非常显著。哪怕在回测里统一按单边万三手续费、每次成交多滑1个跳动点去处理结果都可能差出好几个百分点。别嫌麻烦这一步不能省。极端情况也值得单独提一下涨跌停的时候你想买买不进、想卖卖不出回测里如果照样按价格成交等于给你开了超能力。严谨的回测做法是先判断当天是否一字板如果是当天所有的入场出场信号都视为无法成交。5.4 环境与pandas版本问题关于环境问题我收到最多的问题就是“pandas装不上怎么办”。说实话如果你还在用最原始的方式挨个pip install强烈建议直接装Anaconda或者Miniconda打开以后创建独立环境顺手把pandas、numpy、matplotlib、akshare一次性装齐。pandas的API在不同版本里有局部变动比如DataFrame.append在2.0以后被彻底移除了如果你参考的博客代码用到这个方法记得改成pd.concat。回测代码跑之前先print(pd.__version__)确认一下版本版本不一致导致的报错是最容易让人怀疑人生的。最后再聊两句海龟交易法则用pandas实现前前后后我改了三版第一版是纯循环硬写速度慢得离谱第二版想着全部向量化结果状态逻辑绕成一团乱麻第三版才是现在这个“指标向量化主循环状态机”的结构速度和清晰度都满意了。我个人最大的体会是写回测比自己以为的要难不是难在语法而是难在把自己真正放回“当时不知道未来”那个位置去思考问题。你把这套系统完整地跑通一遍对趋势交易的敬畏、对风险控制的理解会远比单纯背下来“2N止损”这四个字深刻得多。后续想扩展的话有几个方向可以继续做把System 2的55日/20日参数加进来双轨运行加入50日均线的趋势过滤器避免震荡期频繁开仓或者跑一批不同品种的历史数据统计系统在股票、期货上的适应性差异。任何一步玩熟了你对pandas和交易系统的理解都会再上一个台阶。本文还有配套的精品资源点击获取
返回列表