做量化这两年,我最大的感触是:策略逻辑写得再漂亮,落不到代码里、跑不出实盘效果,一切都是纸上谈兵。最近把经典的海龟策略在AIQT框架上完整实现了一遍,从回测到模拟盘都跑通了,过程里踩了不少坑,也把很多以前想当然的细节重新捋了一遍。这篇文章就把整个实现过程原原本本分享出来,包括策略拆解、代码结构、回测参数设置,以及几个特别容易翻车的细节。想用 Python 框架落地趋势跟踪策略的朋友,不管你是刚接触量化还是写过几个策略的老手,这篇文章都值得花几分钟看完。
1. AIQT 框架初体验:为什么我选它来落地海龟策略
很多人在"用什么框架写策略"这件事上纠结很久。backtrader 老牌但 API 有点绕,vn.py 功能全但重,聚宽、掘金这类在线平台又受限于数据源和运行环境。AIQT 是这两年兴起的一个偏 AI 风格的量化框架,我第一次用的时候最直观的感受就是:回调函数写得少,配置式的东西多,启动回测只要几行代码,数据源封装得也不错。对个人做策略验证来说,这个上手成本很友好。
1.1 AIQT 到底是干什么的
AIQT 底层是基于 Python 的量化交易开发框架,它把行情数据获取、技术指标计算、回测引擎、交易信号生成这些环节都封装成了现成的接口。你不需要自己维护 K 线数据,不需要手写均线、最高价最低价追踪这类逻辑,只需要在策略文件里专注于"什么时候开仓、什么时候止损、什么时候加仓"这些核心规则。
我自己比较喜欢的两个特性:一是它的行情接口可以直接拉实盘级别的历史数据,不用去东拼西凑;二是它内置了像ai.ma()、ai.hhv()、ai.atr()这类封装好的指标函数,代码写出来很接近交易思路本身,可读性很强。
1.2 和 backtrader、vn.py 这类框架相比,AIQT 的差异在哪
框架选型没有绝对好坏,只看匹不匹配你的使用场景。我简单列一个对比,都是基于我个人实际用下来的感受:
| 维度 | backtrader | vn.py | AIQT |
|---|---|---|---|
| 上手难度 | 中等,需要理解 Data Feed、Strategy、Analyzer 等抽象概念 | 较高,适合组合管理和实盘交易系统 | 较低,API 风格接近 OpenAI 接口,开箱即用 |
| 数据获取 | 需要自己接数据源或写 feed | 自带较多数据服务,但配置复杂 | 内置行情接口,几行代码拉数据 |
| 策略表达 | Cerebro 加 Strategy 类,编码量偏多 | 事件驱动,适合复杂系统 | 装饰器加回调,聚焦信号逻辑 |
| 适合场景 | 传统回测研究 | 机构级、多市场实盘系统 | 个人/小团队快速验证与轻量实盘 |
我用 AIQT 落地海龟策略,主要是因为海龟策略本身逻辑并不复杂,没必要上重型框架。AIQT 这种"把复杂留给自己,把简单留给用户"的设计,很契合这个需求层级。
1.3 AIQT 的基本工作模式
AIQT 的策略运行方式是on_bar回调机制,每一根 K 线收盘后触发一次,你的策略代码在这个回调里判断要不要下单。回测通过生成器(generator)逐根 bar 推进,模拟真实行情演变。数据获取上,HistoricalData这个类负责拉取 K 线,支持股票、期货、数字货币等多种标的。
我这次用的是期货数据,日线级别的rb0000(螺纹钢指数),这和海龟策略最经典的"日线突破"场景完全匹配。
2. 海龟策略核心逻辑拆解:从交易思想到参数体系
海龟策略的原始体系来自 Richard Dennis 和 William Eckhardt 的著名实验。它最核心的思想不是某个神奇的指标,而是一整套完整的交易系统:趋势识别、入场时机、仓位管理、止损退出、加仓规则,每一个环节都是明确定义的。
很多人提到海龟就只想到"20 日突破入场",这其实只看到了冰山一角。海龟策略真正的厉害之处在于风险和仓位管理,那套基于 ATR 的动态止损和加仓规则,才是它在趋势行情里能持续获利的关键。
2.1 趋势跟踪的本质:截断亏损,让利润奔跑
趋势跟踪策略不预测行情方向,只负责"跟随"。它假设市场在某个时间段内会走出持续性的趋势,策略要做的就是尽早发现趋势启动,然后一路持有到趋势结束,并且在趋势没来的时候用小亏损试错。
这个逻辑用大白话说就是:错了亏小钱,对了赚大钱。海龟策略的单笔止损大约在 2N(下文会细讲),单次亏损被严格限制在账户的一小部分,而趋势一旦展开,盈利空间是开放的。所以从期望值的角度看,即使胜率不到 50%,整套系统也能在多次交易之后获得正收益。
2.2 入场信号:唐奇安通道突破
海龟策略使用唐奇安通道来确定入场点。本质上就是追踪过去一段时间内的最高价和最低价:
- 系统 1:入场采用 20 日唐奇安通道。价格突破过去 20 日最高价,开多单;跌破过去 20 日最低价,开空单。
- 系统 2:入场采用 55 日唐奇安通道,适用于更长周期的趋势跟踪。
这里有一个容易被忽略的细节:海龟法则要求"突破"必须用真实价格击穿,而不是触及。在你的代码里这表现为high > 前20日最高值用严格大于,而不是>=。这个细节虽然对回测结果影响不大,但它体现了原始策略对信号质量的严格态度。
2.3 仓位管理:ATR 与 N 的神奇配合
这是海龟策略里最值得反复琢磨的部分。N 就是 ATR(Average True Range,20 日平均真实波幅),它衡量的是市场当前的平均波动幅度。
海龟法则用 N 来做仓位控制,核心公式如下:
单位头寸量 = 账户权益的 1% ÷ (N × 每一点对应的合约价值)用一个具体例子说明:假设账户权益是 100 万,N 值为 30(也就是当前市场平均每天波动 30 个点),每点合约价值是 10 元。那么:
1% 权益 = 10000 元 N × 每点价值 = 30 × 10 = 300 元 单位头寸 = 10000 ÷ 300 ≈ 33 手这个计算的意义在于:无论市场波动大小,每买入一个单位头寸,单日不利波动对账户的影响都在 1% 左右。市场波动大的时候自动少买,波动小的时候自动多买。这种风险预算思维比固定手数交易科学得多。
2.4 止损、加仓与退出:完整交易闭环
海龟策略的止损线设置为 2N。以做多为例,入场价一旦确定,止损价 = 入场价 - 2 × N。这意味着如果行情反向波动超过两日的平均真实波幅,系统无条件离场,风险被锁定在可接受范围内。
更精彩的是加仓机制。海龟不是一次性建满仓位,而是以 0.5N 为间隔逐步加仓:
- 初始开仓:价格突破唐奇安通道,买入 1 个单位;
- 第一次加仓:价格向有利方向移动 0.5N,加仓 1 个单位;
- 第二次加仓:价格继续移动 0.5N,再加 1 个单位;
- 以此类推,最多加仓到 4 个单位(算上初始仓)。
这里要特别注意,加仓后的止损线也要随之下移。原始海龟规则里,最后一次加仓后,所有持仓的统一止损线移动到最后一次加仓价下方 2N 处。也就是说,止损并不总是从初始入场价算起,而是随着加仓动态调整。
退出规则同样用唐奇安通道,但周期不同。入场用 20 日突破,离场则采用 10 日反向突破:持有多单时,价格跌破过去 10 日最低价,平仓离场。
3. 用 AIQT 实现海龟策略:代码详解
理论部分说清楚了,现在进入正题:在 AIQT 里把海龟策略完整实现出来。我按环境准备、数据获取、策略核心逻辑、回测启动四个步骤走,每一步都给出可以直接参考的代码和解释。
3.1 环境准备与数据获取
首先是安装 AIQT,这一步和其他 Python 库一样,直接用 pip 安装即可:
pip install aiqt安装完成后,在项目里创建策略文件,比如turtle_strategy.py,然后通过 API Key 初始化连接:
from aiqt import AIQuant, HistoricalData import pandas as pd # 初始化,api_key需要替换成你自己的 api_key = "your_api_key" ai = AIQuant(api_key) hist_data = HistoricalData(ai) # 拉取螺纹钢指数日线数据,最近1000根K线 df = hist_data.get_kline_data("rb0000", "1d", 1000) print(df.tail())这一步运行成功后,你会得到一个包含日期、开盘价、最高价、最低价、收盘价、成交量的 DataFrame。这里我建议先拉 1000 根日线,足够覆盖海龟策略的 20 日和 55 日通道计算需求,同时回测也有足够样本。
3.2 策略框架搭建:理解 on_bar 回调
AIQT 的策略核心是一个使用@ai.on_bar()装饰器修饰的函数。这个函数会在每一根 K 线结束后被调用,接收两个参数:bar(当前这根K线的行情数据)和context(一个上下文对象,用来维护策略状态,比如当前持仓)。
@ai.on_bar() def turtle_strategy(bar, context): # 在这里实现海龟策略逻辑 pass有人会把 on_bar 理解成"每个时间点运行一次",这个理解方向对,但少了一个关键细节:bar数据结构里除了 open、high、low、close、volume 之外,还有time(这根 K 线的时间戳)。策略在做信号判断时,要用的是这根 bar已经完成的数据,而不是正在进行的实时数据。这也是我在调试中花时间最多的地方,后面详细讲。
3.3 核心代码:海龟策略的完整实现
下面是我在 AIQT 中实现的完整海龟策略代码。为了保持代码可读性,我以系统 1(20 日突破入场、10 日离场)为基础,并加入了基于 ATR 的动态仓位管理、2N 止损和 0.5N 加仓逻辑。
from aiqt import AIQuant, HistoricalData import pandas as pd import numpy as np api_key = "your_api_key" ai = AIQuant(api_key) hist_data = HistoricalData(ai) # 拉取数据 df = hist_data.get_kline_data("rb0000", "1d", 1000) # 计算指标 df['don_20_high'] = ai.hhv(df['high'], 20) # 20日最高价 df['don_20_low'] = ai.llv(df['low'], 20) # 20日最低价 df['don_10_low'] = ai.llv(df['low'], 10) # 10日最低价(用于退出) df['atr_20'] = ai.atr(df, 20) # 20日ATR @ai.on_bar() def turtle_strategy(bar, context): # 初始化状态 if not hasattr(context, 'position'): context.position = 0 # 当前持仓单位数,正数为多单 context.entry_price = 0 # 入场价(最近一次加仓的价格) context.stop_price = 0 # 当前止损价 context.last_high = 0 # 用于跟踪加仓的参考价(做多) context.last_low = 0 # 用于跟踪加仓的参考价(做空) # 从bar中取出当前时间点的数据 close = bar.close high = bar.high low = bar.low cur_time = bar.time # 找到当前bar对应的指标值 cur_row = df[df['time'] == cur_time] if cur_row.empty: return don_20_high = cur_row['don_20_high'].values[0] don_20_low = cur_row['don_20_low'].values[0] don_10_low = cur_row['don_10_low'].values[0] atr_20 = cur_row['atr_20'].values[0] # 账户权益 equity = ai.account_equity(context) # 当前账户总权益 # 计算单位头寸:1%账户权益 / (ATR * 每点价值) # 螺纹钢每点合约价值约10元,这里以10为例 unit_size = int((equity * 0.01) / (atr_20 * 10)) # 无持仓时,判断是否入场 if context.position == 0: if close > don_20_high: # 突破20日最高价,开多单 context.position = 1 context.entry_price = close context.stop_price = close - 2 * atr_20 context.last_high = close ai.buy(unit_size, close) print(f"{cur_time}: 开多,价格={close},数量={unit_size},止损={context.stop_price}") elif close < don_20_low: # 跌破20日最低价,开空单 context.position = -1 context.entry_price = close context.stop_price = close + 2 * atr_20 context.last_low = close ai.sell(unit_size, close) print(f"{cur_time}: 开空,价格={close},数量={unit_size},止损={context.stop_price}") return # 持仓时的跟踪逻辑 if context.position > 0: # 多单加仓:价格每上涨0.5N加仓一次,最多4个单位 if context.position < 4 and high >= context.last_high + 0.5 * atr_20: add_price = context.last_high + 0.5 * atr_20 context.position += 1 context.entry_price = add_price context.last_high = add_price # 每次加仓后,止损上调至最新加仓价下方 2N context.stop_price = add_price - 2 * atr_20 ai.buy(unit_size, add_price) print(f"{cur_time}: 加多,价格={add_price},仓位={context.position}/{4}") # 多单止损:价格跌破止损价,全部清仓 if low <= context.stop_price: ai.close_all() print(f"{cur_time}: 多单止损,价格={low}") context.position = 0 return # 多单离场:价格跌破10日最低价,平仓 if close < don_10_low: ai.close_all() print(f"{cur_time}: 多单离场,价格={close}") context.position = 0 return elif context.position < 0: # 空单加仓:价格每下跌0.5N加仓一次,最多4个单位 if context.position > -4 and low <= context.last_low - 0.5 * atr_20: add_price = context.last_low - 0.5 * atr_20 context.position -= 1 context.entry_price = add_price context.last_low = add_price context.stop_price = add_price + 2 * atr_20 ai.sell(unit_size, add_price) print(f"{cur_time}: 加空,价格={add_price},仓位={-context.position}/{4}") # 空单止损:价格突破止损价,全部清仓 if high >= context.stop_price: ai.close_all() print(f"{cur_time}: 空单止损,价格={high}") context.position = 0 return # 空单离场:价格突破10日最高价,平仓 don_10_high = cur_row['don_10_high'].values[0] if close > don_10_high: ai.close_all() print(f"{cur_time}: 空单离场,价格={close}") context.position = 0 return这里有一个我在实现时特别注意的点:df[df['time'] == cur_time]这种查找方式在实际运行中效率不高,但胜在直观。如果你的回测数据量大,可以把 DataFrame 转成字典或者按时间戳索引,速度会快很多。
3.4 关键细节:信号确认、K线时间戳与订单执行
代码看似简单,但有三个细节决定它能不能真实还原海龟策略:
第一,信号确认用收盘价还是用最高价/最低价。入场我用的是收盘价突破通道,这是相对稳健的一种处理方法。原始海龟策略更激进,只要盘中价格突破就立即触发,但回测系统基于 K 线只能做近似处理。收盘价确认会少掉一些做突破的入场机会,但也避免了盘中假突破的噪音。这是个有意为之的取舍,不是疏忽。
第二,K 线时间戳会带来的错位问题。AIQT 的bar.time通常是 K 线的时间标签,但不同的数据源定义不一样。有的时间戳表示 K 线开始时间,有的表示结束时间。如果你拿这个时间戳去 DataFrame 里找指标值,一定要先确认数据源的time列和bar.time的语义一致。我一开始没对齐,导致信号总是延迟一根 bar 才触发,回测曲线完全变了样。
第三,止损单的执行价。代码里我用low <= context.stop_price来判断止损触发,但真实成交价不一定是stop_price这个精确价位。在极端行情下,价格可能直接跳空穿过止损位。回测引擎如果默认按止损价成交,会高估实际收益。AIQT 回测支持滑点设置,后面回测部分我会展开说。
4. 回测配置与结果解读:怎样才算跑通了海龟策略
策略代码写完,下一步就是放到回测环境里跑。AIQT 的回测启动方式和传统框架差异比较大,用生成器模式,每次迭代推进一步。同时 AIQT 还内置了实盘模拟模式,接口参数和回测高度一致,我用的感觉是从验证到实盘丝滑切换。
4.1 回测参数设置与启动
以下是我在 AIQT 中启动海龟策略回测的方式:
from aiqt import AIQuant, HistoricalData ai = AIQuant(api_key) hist_data = HistoricalData(ai) # 准备数据 df = hist_data.get_kline_data("rb0000", "1d", 1000) # 启动回测,初始资金100万,手续费和滑点按实际水平设置 generator = ai.backtest_start( start_date="2020-01-01", end_date="2023-12-31", init_cash=1000000, strategy_path="turtle_strategy.py", data=df, commission=0.0002, slippage=1, ) for result in generator: # 生成器每推进一步,就返回当前状态 pass # 获取完整回测报告 report = ai.backtest_result(generator) print(report)回测配置里有两个参数值得专门说明一下。
第一个是commission,手续费。海龟策略是典型的高频趋势跟踪系统,一年可能交易几十次,每次还有加仓操作,手续费累计起来相当可观。我按万分之二收取,基本符合期货市场的平均水平。如果有人告诉你"海龟策略随便写写就翻倍",你先问问他有没有算手续费。
第二个是slippage,滑点。我用的是 1 跳,默认按合约最小变动价位设置。滑点对海龟策略的影响在于:突破入场本身就是在价格快速移动时触发的,如果滑点设置太小,回测结果会比实盘乐观。
4.2 从回测报告里看什么
AIQT 的回测报告包含收益曲线、最大回撤、夏普比率、胜率、盈亏比等核心指标。对海龟策略来说,我最关注三个维度:
一是收益曲线是否平滑稳健。海龟策略在震荡行情里会连续小亏,收益曲线会出现一段平台期甚至小幅下滑,这很正常。关键是进入趋势行情后,曲线要能看到陡峭的上升段。如果整个回测期曲线都在稳步上涨,反而可疑,说明策略参数可能被优化过度了。
二是最大回撤是否在心理承受范围。趋势跟踪策略的最大回撤往往发生在长期震荡的市场环境里。我回测这三年螺纹钢的数据,海龟策略回撤大约在 15% 到 25% 之间,这个体量心里要有数。如果这一版回撤超过 30%,我一般会怀疑是不是止损逻辑或者仓位管理出了问题。
三是单笔交易的亏损分布。海龟策略的亏损单应该呈现出"小亏聚集"的特征,每一笔亏损都集中在 2N 左右。如果回测结果里出现单笔大额亏损,往往说明止损没有严格执行,或者滑点设置过大了。
4.3 参数敏感性:一个必须做的小实验
海龟策略的参数看起来是"钦定"的:20 日突破、55 日突破、2N 止损、0.5N 加仓。但不同市场、不同周期,这些参数的适配度并不一样。
我在写完基础版之后,做了一组简单的参数敏感性测试,把入场通道从 10 日到 40 日逐个跑了一遍,对比收益和回撤。结果很有意思:参数在 20 日附近的表现都还不错,但偏离太远(比如 10 日)时交易次数暴增,手续费和假突破把收益吃掉一大块;参数太大(比如 40 日)时会错过很多行情的启动阶段。这说明海龟策略的参数体系有其内在的稳健性逻辑,也和这个策略"持仓周期在数周级别"的定位一致。
这种敏感性测试用 AIQT 做起来成本很低,只需要在配置里改参数重跑,几分钟就能出结果。我强烈建议任何策略在正式使用前都做一遍,它比研究曲线拟合的那些花活实在得多。
5. 常见问题与排查实录:我踩过的那些坑
最后这部分,我把自己在 AIQT 里实现海龟策略时遇到过的典型问题整理成清单,包括问题表现、原因分析和解决办法。这些问题在文档和教程里通常不会写,但对实际跑通策略非常重要。
5.1 突破信号总慢一根K线
这是我在调试时遇到的第一个大坑。策略代码逻辑完全按照海龟规则写了,但回测结果显示,所有入场点的位置都比理论信号晚了一根 K 线。
排查下来发现,问题出在on_bar的触发机制上。AIQT 的on_bar是在当前 K 线数据生成后触发的,意味着bar.close已经是当前 K 线的收盘价。但我的指标 DataFrame 里,don_20_high等值对应的时间戳也是当前 K 线的。如果我在当前 bar 里把当前 bar 的高点包含进通道计算,实际上引入了未来数据——用今天的数据判断今天的信号,这在实盘里是不可能做到的。
正确做法:计算 20 日最高价时,应该用当前 bar 之前 20 根 K 线的数据,不包含当前 bar。AIQT 的hhv函数默认包含当前行,所以需要显式处理,比如把信号和数据错开一根,或者在计算时用shift(1)。我用了一句简单的代码解决:
df['don_20_high'] = df['high'].rolling(20).max().shift(1)这句话的意思是:前 20 根 K 线的最高价,且不包括当前根。用这个值做突破判断,才能保证策略逻辑不会用到未来数据。
注意:未来函数(look-ahead bias)是回测里最隐蔽的问题,不会报错,但会让回测曲线异常漂亮。任何基于历史窗口的指标,都要检查窗口计算是否包含了当前 K 线。
5.2 加仓后止损线为什么总是"打早了"
跑完回测后我统计了一下止损触发的次数,发现多单在加仓之后,止损单被触发的频率明显偏高,而且往往是刚加仓不久就被打掉。
复盘发现,问题出在止损线的更新逻辑上。我在代码里每次加仓后就把止损线上移到加仓价 - 2N,这看起来符合海龟规则,但忽略了入场初始仓的止损。如果第一次入场价是 4000,第一次加仓价是 4200(中间隔了 0.5N 约等于 50 个点),止损线上移到 4200 - 2N。如果 N 值是 40,止损线就是 4120,反而高于初始入场价,导致初始仓的止损被提前拉大,整体风险暴露超出设计。
原始的规则其实是:加仓后,所有持仓统一使用最新的止损线。但很多人在代码里把"最新止损线"误解为"只对最新加仓的单子有效",从而在持仓结构上出现了止损漏洞。我的处理办法是:统一用一个context.stop_price变量,每次加仓后重新计算,并且强制校验一下,新的止损线不能比原来的止损线更不利于持仓(做多时不能降低止损)。
这个问题想明白了,对海龟策略的风险逻辑才算是真正理解透了。
5.3 收盘价突破入场,导致止损单在盘中就被打掉
海龟策略经典的入场信号用的是盘中价格突破,但我代码里用的是收盘价突破确认后,在收盘价买入。这种简化带来的副作用是:入场之后,止损单直接挂在收盘价下方 2N 处。如果入场当天盘中波动很大(比如拉了一根长上影线),止损价可能在 K 线范围内就被击穿,系统只能按止损价成交。
这导致的一个直接后果就是:策略的持仓时间变短,很多本应持有的趋势行情,因为入场位置不好,在第二天就被震出来了。
我的解决方案是给入场加一个筛选条件:只有突破当日的 K 线实体足够大的时候才入场。完整的代码实现里需要加一个条件,比如bar.close - bar.open > 0.5 * atr_20(做多时),这能在一定程度上过滤掉假突破。当然,如果你要做严格按照原版海龟规则的实盘,这种方法需要更精细的触发机制,回测系统里的 K 线粒度限制让这个问题没办法完全规避,只能取舍。
5.4 手续费和滑点吃掉了一半利润
这是所有趋势跟踪策略共同的问题,但在海龟策略上尤为突出。因为它每次入场后还有加仓,交易次数比一般的长线策略多得多,每次加仓都是一笔新的手续费支出。
我在最初回测时把手续费设置为 0,滑点设置为 0,回测结果年化收益看起来很诱人。后来把手续费调到万分之二、滑点调到 1 跳,年化收益直接缩水了一半以上。这不是 AIQT 的问题,而是任何趋势策略的固有成本。所以我的建议是:从第一版回测开始就设置真实的手续费和滑点参数,否则你会被虚高的收益数字误导,投入大量时间去优化一个根本没有实盘可行性的参数组合。
5.5 K线数据复权与合约换月的影响
我用的是股票和期货数据做回测,如果是期货,还要注意主力合约换月和复权问题。rb0000这种指数连续合约,数据源一般已经处理好了换月的价格跳跃,但你仍然要留意:指数合约和真实可交易的合约价格之间存在基差,回测里算出的收益和实盘会有差异。如果是股票数据,前复权和后复权的选择会影响突破通道的计算,务必保持一致。
我在第一次用股票数据跑海龟策略时,因为没注意复权方式,实盘和回测的入场点差了很大一截。这个坑虽然不算 AIQT 特有的,但在任何框架里都值得提前确认。
写在最后的一个实操体会
海龟策略这套逻辑写起来不难,难的是把每一个细节都落到实处。从通道计算要不要包含当前 K 线,到加仓后止损线怎么更新,再到手续费滑点对最终收益的影响,任何一个环节处理不当,回测结果和实盘表现都会天差地别。我自己用 AIQT 把这套流程完整跑通之后,最大的收获不是"我复现了一个经典策略",而是对趋势跟踪系统的完整闭环有了更清晰的认知。如果你也想把这套代码跑起来,我建议先用小资金、模拟盘跑一段时间,重点观察实盘成交价和回测预估价的偏差,再决定要不要放大资金。量化交易的道路上,能稳定把损耗控制住,比找到一个神奇策略重要得多。