海龟汤和海龟汤升级版的改进(基础篇)
◍ 海龟汤与升级版的改造起点
这套思路脱胎于经典海龟汤与海龟汤升级版,核心是把原系统的通道识别逻辑做参数化重构,让策略能直接跑在 MT5 上验证。原文作者 Alexander Puzanov 在 2016 年 12 月 15 日发布该示例,截至统计节点获得 20 114 次查看与 16 条讨论,说明这类通道型价格行为玩法在外汇与贵金属交易者里一直有刚需。 改造的第一步是重新定义通道参数:不再写死 N 日高低点,而是把通道周期、突破缓冲、回调容差全部外置为输入变量。这样你可以在 EURUSD 或 XAUUSD 的 M15 上,分别试不同的通道长度,观察假突破频率的变化。 外汇与贵金属杠杆高、滑点跳空频繁,通道参数过紧容易在重大数据前被扫损,调参时务必用历史回测先压一遍极端行情。
康纳斯策略的MQL5落地前提
劳伦斯·康纳斯与琳达·瑞斯克合计有34年实盘与机构经验,他们认为单一策略亦可支撑稳定交易,但原书仍给出约20种策略,按市场周期分四组。原书策略基于15至20年市况样本开发,不能直接当作当代行情的圣杯。 本文只取书中第一组里的首个策略,用MQL5实现,并丢进MT5策略测试器,以MetaQuotes模拟服务器近年的价格历史跑效能。读者可自行开MT5照此复现,外汇与贵金属品种波动剧烈,回测顺滑不等于实盘低风险。 代码层面假定你是略高阶的MQL5初学者:不解释标准库函数、不掰扯变量类型选型,这些应在写EA前自补。也不照顾老手——他们自有成熟开发库。 为帮过程式背景的人过渡到面向对象,这里不用class,只用struct。结构能把异类数据和操作函数捆在一起,支持继承,却不必先背类语法,改几处写法就能上手。
「海龟汤与升级版:靠假突破反吃通道」
海龟汤属于‘用价格测试区间边界或支撑阻力’这一类策略,核心假设是:价格不会不经一次反弹就直接突破 20 天区间,任务是从临时假突破里获利。仓位方向永远朝通道内侧,本质是一种反弹策略,和经典海龟都盯 20 天范围但反向操作。 原版买入进场可拆为几条硬规则:距离前一个 20 天低点至少过去 3 天;等价格跌破 20 天低点;在向下突破那根 K 线低点上方 5~10 点挂 buy stop;触发后止损放当日最低价下方 1 点;有浮盈后接跟踪止损;若头一两天被止损,可在原水平重进。卖出对称,看 20 天高点。 原文没说挂单留多久。实操逻辑:价格测试边界时会出新极值,突破后第二天‘距前低已过 3 天’这个条件通常就不成立了,那天无信号,前一天挂单应撤掉。MT5 代码库有现成指标能在历史 K 上画通道边界,手动交易直接加载看边界就行。 升级版只改两点:不突破就挂单,而是等确认——当天 K 线收在 20 天区间外(收在测算的通道边界外也可);初始止损用两根 K 的极值(最高或最低)定位。外汇和贵金属波动大、假突破频繁,这套方法可能提高胜率但仍有连续止损风险,开 MT5 加载指标回测 20 天通道就知真假。
◍ 通道参数怎么塞进结构里
虚拟通道由四个基础变量锁定:范围最高价、最低价,以及它们各自对应的时间边界。再补上最高价、最低价距离当前柱的偏移天数(柱数),这六个量就能完整描述一个通道状态,适合打包成结构体供策略调用。 f_Set 函数负责按指定起点(i_Newest_Bar_Shift)和历史深度(i_Bars_Limit)刷新通道。原始版本仅 13 行,直接用 CopyHigh、CopyLow、CopyTime 拉序列,但 MT5 时间序列首次访问时可能没准备好,返回条数少于请求值是常态,不处理就会读到脏数据。 加错误分支后,发现 WRONG_VALUE 或复制条数不足就打印并 return,让终端先补齐数据,下次 tick 再试。为防止半截数据被别的函數拿去下单,结构里要挂 b_Ready 标志(true 才可用);另加 b_Updated 和 s_Signature,用来识别四个参数是否变动、避免无谓重算。 日志别写死,用枚举 ENUM_LOG_LEVEL 让用户选 NONE/ERR/INFO/DEBUG,主程序开头声明 Log_Level 全局变量,各函数按级别打印,调试时能还原下单当时的通道细节。下面两段代码分别是初版结构和带错误处理的 f_Set 重写。
class="kw">struct CHANNEL { class="type">class="kw">double d_High; class=class="str">"cmt">// 范围上方边界的价格 class="type">class="kw">double d_Low; class=class="str">"cmt">// 范围下方边界的价格 class="type">class="kw">datetime t_From; class=class="str">"cmt">// 通道中第一个(最早的)柱的日期/时间 class="type">class="kw">datetime t_To; class=class="str">"cmt">// 通道中最后一个柱的日期/时间 class="type">int i_Highest_Offset;class=class="str">"cmt">// 最高价右边的柱数 class="type">int i_Lowest_Offset; class=class="str">"cmt">// 最低价右边的柱数 }; class="type">void f_Set(class="type">int i_Bars_Limit, class="type">int i_Newest_Bar_Shift = class="num">1) { class="type">class="kw">double da_Price_Array[]; class=class="str">"cmt">// 用于保存通道中所有柱的最高/最低价格的辅助数组 class=class="str">"cmt">// 确定通道的上方边界: class="type">int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); class="type">int i_Bar = ArrayMaximum(da_Price_Array); d_High = da_Price_Array[i_Bar]; class=class="str">"cmt">// 确定通道的上方边界 i_Highest_Offset = i_Price_Bars - i_Bar; class=class="str">"cmt">// 距离最高价的时间(柱数) class=class="str">"cmt">// 确定范围的最低边界: i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); i_Bar = ArrayMinimum(da_Price_Array); d_Low = da_Price_Array[i_Bar]; class=class="str">"cmt">// 确定通道的最低边界 i_Lowest_Offset = i_Price_Bars - i_Bar; class=class="str">"cmt">// 距离最低点的时间(柱数) class="type">class="kw">datetime ta_Time_Array[]; i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array); t_From = ta_Time_Array[class="num">0]; t_To = ta_Time_Array[i_Price_Bars - class="num">1]; } enum ENUM_LOG_LEVEL { class=class="str">"cmt">// 记录级别 LOG_LEVEL_NONE, class=class="str">"cmt">// 禁用记录 LOG_LEVEL_ERR, class=class="str">"cmt">// 只含有错误信息 LOG_LEVEL_INFO, class=class="str">"cmt">// 错误 + EA的注释 LOG_LEVEL_DEBUG class=class="str">"cmt">// 所有内容 }; class="type">void f_Set(class="type">int i_Bars_Limit, class="type">int i_Newest_Bar_Shift = class="num">1) { class="type">class="kw">double da_Price_Array[]; class=class="str">"cmt">// 用于保存通道中所有柱的最高/最低价格的辅助数组 class=class="str">"cmt">// 确定通道的上方边界: class="type">int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { class=class="str">"cmt">// 处理 CopyHigh 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: 错误 #%u", __FUNCSIG__, _LastError); class="kw">return; } if(i_Price_Bars < i_Bars_Limit) { class=class="str">"cmt">// CopyHigh 函数未能接收到请求数量的数据 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: 已经复制了 %u 个柱,共需 %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit);
通道高低边界与结构体的落地写法
找通道上沿别自己写循环比大小,直接让 ArrayMaximum 吐出最高价所在下标,再用这个下标回数组取价格。下方边界同理用 ArrayMinimum,两者都返回 WRONG_VALUE 时必须立刻 return,否则后续拿到的 d_High / d_Low 可能是脏值。 CopyLow 拿不到足够柱数也要拦住:若 i_Price_Bars < i_Bars_Limit,说明历史数据没加载全,此时硬算通道会失真。外汇和贵金属市场跳空频繁,这种数据缺失在周一并盘时尤其常见,属于高风险场景下的必检项。 时间边界用 CopyTime 填,取 ta_Time_Array[0] 为起始、ta_Time_Array[i_Price_Bars-1] 为结束;若返回值小于 1 则把 t_From、t_To 都置 0,避免调用方误用无效时间。 把上述字段收进一个 CHANNEL 结构体最省事:d_High、d_Low 存边界,i_Highest_Offset / i_Lowest_Offset 记最高最低距当前的柱数,b_Ready 标是否算完,s_Signature 留作去重比对。构造函数里先全部清零,b_Ready 和 b_Updated 初始 false,签名给个 '-' 占位。
CHANNEL() { d_High = d_Low = class="num">0; t_From = t_To = class="num">0; b_Ready = b_Updated = class="kw">false; s_Signature = "-";
「通道边界的刷新与容错」
把价格通道的上沿和下沿算准,核心在 f_Set 这个函数:先拉一段 High 数组找最大值,再拉一段 Low 数组找最小值,两者共同界定当前观察窗口的波动边界。 进场前先把 b_Ready 置 false,相当于给后续计算加了个‘暂停服务’标志,避免旧通道值在被覆盖前就被别的模块误用。 CopyHigh / CopyLow 返回 WRONG_VALUE 或实际复制柱数少于请求值,都必须直接 return 并打印错误——历史数据不足时硬算通道,得到的 d_High / d_Low 会严重失真。 ArrayMaximum 拿到的是数组下标,转成距当前柱的偏移要用 i_Price_Bars - i_Bar;比如窗口取 100 根、最高价落在第 30 根(从旧到新排),i_Highest_Offset 就是 70,意味着 70 根前见到上沿。 CopyTime 若连 1 根时间都拿不到,t_From 和 t_To 直接清零,下游若依赖时间区间做绘制就得自行判断空值,别拿 0 时间去算跨度。
i_Highest_Offset = i_Lowest_Offset = WRONG_VALUE; } class="type">void f_Set(class="type">int i_Bars_Limit, class="type">int i_Newest_Bar_Shift = class="num">1) { b_Ready = class="kw">false; class=class="str">"cmt">// Pitstop: 设置一个服务标志 class="type">class="kw">double da_Price_Array[]; class=class="str">"cmt">// 用于保存通道中所有柱的最高/最低价格的辅助数组 class=class="str">"cmt">// 确定通道的上方边界: class="type">int i_Price_Bars = CopyHigh(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { class=class="str">"cmt">// 处理 CopyHigh 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: 错误 #%u", __FUNCSIG__, _LastError); class="kw">return; } if(i_Price_Bars < i_Bars_Limit) { class=class="str">"cmt">// CopyHigh 函数未能接收到请求数量的数据 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: 已经复制了 %u 个柱,共需 %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit); class="kw">return; } class="type">int i_Bar = ArrayMaximum(da_Price_Array); if(i_Bar == WRONG_VALUE) { class=class="str">"cmt">// 处理 ArrayMaximum 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMaximum: 错误 #%u", __FUNCSIG__, _LastError); class="kw">return; } d_High = da_Price_Array[i_Bar]; class=class="str">"cmt">// 确定通道的上方边界 i_Highest_Offset = i_Price_Bars - i_Bar; class=class="str">"cmt">// 距离最高价的时间(柱数) class=class="str">"cmt">// 确定范围的最低边界: i_Price_Bars = CopyLow(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, da_Price_Array); if(i_Price_Bars == WRONG_VALUE) { class=class="str">"cmt">// 处理 CopyLow 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: 错误 #%u", __FUNCSIG__, _LastError); class="kw">return; } if(i_Price_Bars < i_Bars_Limit) { class=class="str">"cmt">// CopyLow 函数未能收到所请求数量的数据 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: 已复制 %u 个柱,共需 %u", __FUNCSIG__, i_Price_Bars, i_Bars_Limit); class="kw">return; } i_Bar = ArrayMinimum(da_Price_Array); if(i_Bar == WRONG_VALUE) { class=class="str">"cmt">// 处理 ArrayMinimum 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: ArrayMinimum: 错误 #%u", __FUNCSIG__, _LastError); class="kw">return; } d_Low = da_Price_Array[i_Bar]; class=class="str">"cmt">// 确定通道的最低边界 i_Lowest_Offset = i_Price_Bars - i_Bar; class=class="str">"cmt">// 距离最低点的时间(柱数) class="type">class="kw">datetime ta_Time_Array[]; i_Price_Bars = CopyTime(_Symbol, PERIOD_CURRENT, i_Newest_Bar_Shift, i_Bars_Limit, ta_Time_Array); if(i_Price_Bars < class="num">1) t_From = t_To = class="num">0;