海龟汤和海龟汤升级版的改进·综合运用
(3/3)·把康纳斯的反向假突破逻辑落成 MQL5 结构式 EA,并用近年 tick 历史检验它的真实边界
新开仓位的初始止损怎么摆
Turtle Soup 类策略在仓位刚建立、SL 还为零时,需要先用近期极值算一个初始止损,再校验它离现价是否够远。够远才下,不够远就退到 broker 允许的最小止损距离上,避免挂单直接被拒。 下面这个函数就是「距离校验」的核心:取现价与候选水平的绝对差,必须严格大于止损底线与当前点差中的较大者。gd_Stop_Level 一般是经纪商要求的止损最小点数折算价,go_Tick.ask - go_Tick.bid 则是实时点差,两者取大才保险。 [CODE]bool fb_Is_Acceptable_Distance(double d_Level_To_Check, double d_Current_Price) { return( fabs(d_Current_Price - d_Level_To_Check) > fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid) ); }[/CODE] 买入仓位的初始止损取近期 Low 数组的最小值再减 gd_Exit_Offset;若 Turtle_Soup_Type 是 PLIS_1 变体,则多取一根 K 线(数组长度 1+1)。算完立刻用上面的函数测距,不达标就改成 bid - max(止损底线, 点差)。 卖出仓位对称处理:用 CopyHigh 取近期 High 的最大值加偏移,距离校验对着 ask 做,不达标则 ask + max(止损底线, 点差)。最后 CTrade.PositionModify 只改 SL、TP 原样传回。外汇与贵金属杠杆高,最小止损距离随品种和经纪商浮动,实盘前务必在 MT5 策略测试器里把 gd_Stop_Level 打印出来核对。
class="type">bool fb_Is_Acceptable_Distance(class="type">class="kw">double d_Level_To_Check, class="type">class="kw">double d_Current_Price) { class="kw">return( fabs(d_Current_Price - d_Level_To_Check) > fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid) ); } if(PositionSelect(_Symbol)) { class=class="str">"cmt">// 有建立的仓位 if(PositionGetDouble(POSITION_SL) == class="num">0.) { class=class="str">"cmt">// 新的仓位 class="type">class="kw">double d_SL = WRONG_VALUE, class=class="str">"cmt">// 止损水平 da_Price_Array[] class=class="str">"cmt">// 辅助数组 ; class=class="str">"cmt">// 计算止损水平: if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { class=class="str">"cmt">// 对于买入仓位 if(WRONG_VALUE == CopyLow(_Symbol, PERIOD_CURRENT, class="num">0, class="num">1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) { class=class="str">"cmt">// 处理 CopyLow 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyLow: 错误 #%u", __FUNCTION__, _LastError); class="kw">return; } d_SL = da_Price_Array[ArrayMinimum(da_Price_Array)] - gd_Exit_Offset; class=class="str">"cmt">// 距离当前价格是否已经足够? if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.bid)) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("计算所得的止损水平 %s 由允许的最小值 %s 代替", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.bid + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits)); d_SL = go_Tick.bid - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid); } } else { class=class="str">"cmt">// For a class="type">short position if(WRONG_VALUE == CopyHigh(_Symbol, PERIOD_CURRENT, class="num">0, class="num">1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1), da_Price_Array)) { class=class="str">"cmt">// 处理 CopyHigh 函数的错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyHigh: 错误 #%u", __FUNCTION__, _LastError); class="kw">return; } d_SL = da_Price_Array[ArrayMaximum(da_Price_Array)] + gd_Exit_Offset; class=class="str">"cmt">// 距离当前价格是否已经足够? if(!fb_Is_Acceptable_Distance(d_SL, go_Tick.ask)) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("计算所得的止损水平 %s 由允许的最小值 %s 代替", DoubleToString(d_SL, _Digits), DoubleToString(go_Tick.ask - fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid), _Digits)); d_SL = go_Tick.ask + fmax(gd_Stop_Level, go_Tick.ask - go_Tick.bid); } } CTrade Trade; Trade.LogLevel(LOG_LEVEL_ERRORS); class=class="str">"cmt">// 设置止损 Trade.PositionModify(_Symbol, d_SL, PositionGetDouble(POSITION_TP)); class="kw">return; } class=class="str">"cmt">// 跟踪止损
「隔夜挂单清理与通道刷新逻辑」
EA 在每根新柱(按 PeriodSeconds() 折算的日期边界)生成时,先比对静态变量 si_Last_Tick_Bar_Num 与 floor(go_Tick.time / PeriodSeconds()),若前者更小说明跨入了新交易日,随即把 si_Last_Tick_Bar_Num 更新为当前值。这一步是日更类策略的节拍器,漏掉就会在 MT5 上重复处理同一根柱。 跨日之后先查当前品种是否残留 STOP 类挂单。fi_Get_Pending_Type 会遍历 OrdersTotal() 返回的订单数,用 OrderGetString(ORDER_SYMBOL) 与 _Symbol 做不区分大小写比对,只认 BUY_LIMIT / BUY_STOP / SELL_LIMIT / SELL_STOP 四种挂单,命中即返回类型并带出订单 ticket。若类型是 SELL_STOP 或 BUY_STOP,就进入删除分支。 gi_Try_To_Trade 控制删除重试次数,每次失败 Sleep(gi_Connect_Wait) 再试,成功则把 i_Try 置为 -10 跳出。若最终 i_Try 仍等于 WRONG_VALUE,说明删单失败,直接 return 等下一根柱。外汇与贵金属隔夜跳空频繁,这种旧 STOP 单不清理,次日可能以不利价格成交,属典型高风险点。 删单完成后调用 go_Channel.f_Set 按 Turtle_Soup_Period_Length 重算通道,第二参数随 Turtle_Soup_Type 是否为 TS_TURTLE_SOUP_PLIS_1 在 1 与 2 间切换。随后 fe_Get_Entry_Signal 取信号状态,整段构成「日界→清场→重算→取信号」的闭环。
fb_Trailing_Stop(gd_Trail_Trigger, gd_Trail_Step, gd_Trail_Distance); class="kw">return; } class="type">int i_Order_Ticket = WRONG_VALUE, class=class="str">"cmt">// 挂单的订单编号 i_Try = gi_Try_To_Trade, class=class="str">"cmt">// 进行操作的尝试次数 i_Pending_Type = -class="num">10 class=class="str">"cmt">// 已存在挂单的类型 ; class="kw">static class="type">int si_Last_Tick_Bar_Num = class="num">0; class=class="str">"cmt">// 前一个订单时刻柱的编号 (class="num">0 = MQL中的计算起点) class=class="str">"cmt">// 处理与新的一天(柱)开始的事件: if(si_Last_Tick_Bar_Num < class="type">int(floor(go_Tick.time / PeriodSeconds()))) { class=class="str">"cmt">// 向新的一天打个招呼 :) si_Last_Tick_Bar_Num = class="type">int(floor(go_Tick.time / PeriodSeconds())); class=class="str">"cmt">// 是否有过时的挂单? i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket); if(i_Pending_Type == ORDER_TYPE_SELL_STOP || i_Pending_Type == ORDER_TYPE_BUY_STOP) { class=class="str">"cmt">// 删除旧的订单: if(Log_Level > LOG_LEVEL_ERR) Print("删除昨天的挂单"); CTrade o_Trade; o_Trade.LogLevel(LOG_LEVEL_ERRORS); class="kw">while(i_Try-- > class="num">0) { class=class="str">"cmt">// 尝试删除 if(o_Trade.OrderDelete(i_Order_Ticket)) { class=class="str">"cmt">// 尝试成功 i_Try = -class="num">10; class=class="str">"cmt">// 成功操作的标志 class="kw">break; } class=class="str">"cmt">// 尝试失败 Sleep(gi_Connect_Wait); class=class="str">"cmt">// 在下一次尝试前暂停 } if(i_Try == WRONG_VALUE) { class=class="str">"cmt">// 删除挂单失败 if(Log_Level > LOG_LEVEL_NONE) Print("删除挂单错误"); class="kw">return; class=class="str">"cmt">// 等待下一个订单时刻 } } class=class="str">"cmt">// 更新通道参数: go_Channel.f_Set(Turtle_Soup_Period_Length, class="num">1 + (Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1)); } class="type">int fi_Get_Pending_Type( class=class="str">"cmt">// 侦测当前交易品种是否有挂单 class="type">int& i_Order_Ticket class=class="str">"cmt">// 所选挂单单号的引用 ) { class="type">int i_Order = OrdersTotal(), class=class="str">"cmt">// 订单总数 i_Order_Type = WRONG_VALUE class=class="str">"cmt">// 订单类型的变量 ; i_Order_Ticket = WRONG_VALUE; class=class="str">"cmt">// 默认返回的订单编号 if(i_Order < class="num">1) class="kw">return(i_Order_Ticket); class=class="str">"cmt">// 没有订单 class="kw">while(i_Order-- > class="num">0) { class=class="str">"cmt">// 检查已有的订单 i_Order_Ticket = class="type">int(OrderGetTicket(i_Order)); class=class="str">"cmt">// 读取订单编号 if(i_Order_Ticket > class="num">0) if(StringCompare(OrderGetString(ORDER_SYMBOL), _Symbol, class="kw">false) == class="num">0) { i_Order_Type = class="type">int(OrderGetInteger(ORDER_TYPE)); class=class="str">"cmt">// 我们只需要挂单: if(i_Order_Type == ORDER_TYPE_BUY_LIMIT || i_Order_Type == ORDER_TYPE_BUY_STOP || i_Order_Type == ORDER_TYPE_SELL_LIMIT || i_Order_Type == ORDER_TYPE_SELL_STOP) class="kw">break; class=class="str">"cmt">// 找到了一个挂单 } i_Order_Ticket = WRONG_VALUE; class=class="str">"cmt">// 还没有找到 } class="kw">return(i_Order_Type); } class=class="str">"cmt">// 取得信号状态: ENUM_ENTRY_SIGNAL e_Signal = fe_Get_Entry_Signal(Turtle_Soup_Type == TS_TURTLE_SOUP_PLIS_1, Turtle_Soup_Extremum_Offset);
◍ 挂单方向冲突时的删除与重设逻辑
EA 在每次订单时刻先判断信号强度,若 e_Signal 大于 1 直接 return,视作无信号不动作。随后通过 fi_Get_Pending_Type 补查挂单类型,若已有与信号同向的 BUY_STOP 或 SELL_STOP 则跳过,避免重复挂单。 当检测到挂单方向与信号相反(例如信号 ENTRY_SELL 却存在 BUY_STOP),代码进入删除分支:用 gi_Try_To_Trade 次循环调用 OrderDelete,每次失败 Sleep(gi_Connect_Wait) 再试;若 i_Try 最终等于 WRONG_VALUE 说明删除失败,打印日志并退出本次处理。 删除成功后计算入场位:买入用通道下轨 go_Channel.d_Low 加 gd_Entry_Offset,卖出用通道上轨 go_Channel.d_High 减 gd_Entry_Offset。fb_Is_Acceptable_Distance 会拿该价位与 go_Tick.ask/bid 比对,距离不足就 PrintFormat 输出 Bid/Ask/StopLevel 并 return,等价格走动。 手数经 fd_Normalize_Lot 按服务器要求规范化,最后对 ENTRY_BUY 走 o_Trade.BuyStop 分支尝试挂单,同样套用 i_Try 重试机制。外汇与贵金属杠杆高,挂单被拒或滑点可能引发非预期持仓,实盘前建议在 MT5 策略测试器用历史数据验证 StopLevel 与通道偏移的兼容性。
if(e_Signal > class="num">1) class="kw">return; class=class="str">"cmt">// No signal class=class="str">"cmt">// 寻找挂单的类型和它的订单编号: if(i_Pending_Type == -class="num">10) i_Pending_Type = fi_Get_Pending_Type(i_Order_Ticket); class=class="str">"cmt">// 我们是否需要一个新的挂单? if( (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_SELL_STOP) || (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_BUY_STOP) ) class="kw">return; class=class="str">"cmt">// 在信号方向上有一个挂单 class=class="str">"cmt">// 我们是否需要删除挂单? if( (e_Signal == ENTRY_SELL && i_Pending_Type == ORDER_TYPE_BUY_STOP) || (e_Signal == ENTRY_BUY && i_Pending_Type == ORDER_TYPE_SELL_STOP) ) { class=class="str">"cmt">// 挂单的方向与信号方向不匹配 if(Log_Level > LOG_LEVEL_ERR) Print("挂单的方向和信号的方向不符合"); i_Try = gi_Try_To_Trade; class="kw">while(i_Try-- > class="num">0) { class=class="str">"cmt">// 尝试删除 if(o_Trade.OrderDelete(i_Order_Ticket)) { class=class="str">"cmt">// 尝试成功 i_Try = -class="num">10; class=class="str">"cmt">// 成功操作的标志 class="kw">break; } class=class="str">"cmt">// 尝试失败 Sleep(gi_Connect_Wait); class=class="str">"cmt">// 在下一次尝试前暂停 } if(i_Try == WRONG_VALUE) { class=class="str">"cmt">// 删除挂单失败 if(Log_Level > LOG_LEVEL_NONE) Print("删除挂单错误"); class="kw">return; class=class="str">"cmt">// 等待下一个订单时刻 } } class="type">class="kw">double d_Entry_Level = WRONG_VALUE; class=class="str">"cmt">// 设置挂单的水平 if(e_Signal == ENTRY_BUY) { class=class="str">"cmt">// 对于买入挂单 class=class="str">"cmt">// 检查是否可以设置订单: d_Entry_Level = go_Channel.d_Low + gd_Entry_Offset; class=class="str">"cmt">// 订单设置水平 if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.ask)) { class=class="str">"cmt">// 与当前价格的距离不够 if(Log_Level > LOG_LEVEL_ERR) PrintFormat("无法在 %s 水平设置止损买入挂单. Bid: %s Ask: %s StopLevel: %s", DoubleToString(d_Entry_Level, _Digits), DoubleToString(go_Tick.bid, _Digits), DoubleToString(go_Tick.ask, _Digits), DoubleToString(gd_Stop_Level, _Digits) ); class="kw">return; class=class="str">"cmt">// 等待当前价格改变 } } else { class=class="str">"cmt">// 检查是否可以设置订单: d_Entry_Level = go_Channel.d_High - gd_Entry_Offset; class=class="str">"cmt">// 订单设置水平 if(!fb_Is_Acceptable_Distance(d_Entry_Level, go_Tick.bid)) { class=class="str">"cmt">// 与当前价格的距离不够 if(Log_Level > LOG_LEVEL_ERR) PrintFormat("无法在 %s 水平设置止损卖出挂单,Bid: %s Ask: %s StopLevel: %s", DoubleToString(d_Entry_Level, _Digits), DoubleToString(go_Tick.bid, _Digits), DoubleToString(go_Tick.ask, _Digits), DoubleToString(gd_Stop_Level, _Digits) ); class="kw">return; class=class="str">"cmt">// 等待当前价格改变 } } class=class="str">"cmt">// 符合服务器要求的手数: class="type">class="kw">double d_Volume = fd_Normalize_Lot(Trade_Volume); class=class="str">"cmt">// 设置挂单: i_Try = gi_Try_To_Trade; if(e_Signal == ENTRY_BUY) { class="kw">while(i_Try-- > class="num">0) { class=class="str">"cmt">// 尝试设置止损买入挂单 if(o_Trade.BuyStop( d_Volume, d_Entry_Level, _Symbol
挂单失败重试与状态标记
这段逻辑处理的是买入止损与卖出止损两类挂单的提交。若方向变量走 else 分支,就循环调用 SellStop 尝试在 d_Entry_Level 挂卖单,每次失败后用 Sleep(gi_Connect_Wait) 暂停再试,最多重试 i_Try 次。 成功时 Alert 弹窗提示,并把 i_Try 写成 -10 作为脱离循环的成功标志,随后 break 退出。这个 -10 不是重试计数,只是脱离 while 的硬标记,读代码时容易误判。 循环结束若 i_Try 仍等于 WRONG_VALUE,说明所有尝试均失败,按 Log_Level 设置输出“设置挂单错误”。外汇与贵金属挂单受点差和报价冻结影响,这类重试机制在实盘 MT5 上能明显降低因瞬时连接抖动导致的丢单概率。
)) { class=class="str">"cmt">// 尝试成功
Alert("已经设置了买入挂单!");
i_Try = -class="num">10; class=class="str">"cmt">// 成功操作的标志
class="kw">break;
}
class=class="str">"cmt">// 失败
Sleep(gi_Connect_Wait); class=class="str">"cmt">// 在下一次尝试前暂停
}
} else {
class="kw">while(i_Try-- > class="num">0) { class=class="str">"cmt">// 尝试设置止损卖出挂单
if(o_Trade.SellStop(
d_Volume,
d_Entry_Level,
_Symbol
)) { class=class="str">"cmt">// 尝试成功
Alert("已经设置了卖出挂单!");
i_Try = -class="num">10; class=class="str">"cmt">// 成功操作的标志
class="kw">break;
}
class=class="str">"cmt">// 失败
Sleep(gi_Connect_Wait); class=class="str">"cmt">// 在下一次尝试前暂停
}
}
if(i_Try == WRONG_VALUE) class=class="str">"cmt">// 设置挂单失败
if(Log_Level > LOG_LEVEL_NONE) Print("设置挂单错误");「用近五年数据重跑海龟汤」
康纳斯和瑞斯克当年用超过20年图表演示策略,本测试改用更近年份数据验证效能,参数与每日时段均按作者原指定值。20年前主流是4位数报价,而 MetaQuotes 模拟服务器跑的是5位数,所以原文里的1点和10点要换算成10点和100点。原策略没给跟踪止损参数,我按每日时段挑了组最顺手的补上。 近5年 USDJPY 上,海龟汤原版与升级版同参数同历史对照测试已出图;黄金近5年、原油近4年也都分别跑了原版和升级版。完整数值在附件,不在这里列。 两位作者明确警告过:别纯机械跟书里的信号。他们强调得看价格怎么逼近通道边界、以及触边界时的测试动作,只是没写细。外汇和贵金属杠杆高、滑点跳空频繁,回测好看不等于实盘能复制。 想自己动手的话,开 MT5 用每日周期加载 USDJPY,把止损参数从原书点数乘10,先跑一遍原版再调升级版参数,比对最近5年权益曲线。
◍ 最后一句大实话
海龟汤与海龟汤升级版这两套策略已经按书里规则写进EA和信号库,但作者顺带提的交易间隙、每日仅一次进场或仅一次盈利进场、挂单保留过日这些细节,代码层并没全包进去。 想让回测更贴近实盘,得自己补这几条限制:比如给OrderSend加时间窗判断,或用一个bool变量锁死当日已盈利仓位。MT5里打开附带的MQL5.zip改两行,跑出来的曲线和原文报告就会有肉眼可见的偏差。 外汇和贵金属杠杆高、滑点凶,这类短线策略在历史数据上盈亏比飘忽,真上线前请用策略测试器多品种验证,别直接丢实盘。