MT5EA / 策略编译通过 · EX5 50KB 自动发布 2 0

HalfTrend 半趋势马丁交易系统

老吴·2 小时前发布

根据 HalfTrend 箭头信号开仓,10根K线高低点止损,浮亏小马丁3层加仓,动态移动止盈,含可视化面板

HalfTrend 半趋势马丁交易系统

效果示意图 · AI 根据源码绘制,仅示意画了哪些元素,细节以实际运行为准

HalfTrend_MartinEA.mq5801 行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
#property copyright "XAUOF 指标工坊"
#property version   "1.00"
#property description "HalfTrend 信号 + 动态止损 + 小马丁加仓 + 移动止盈 + 可视化面板"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| 输入参数                                                          |
//+------------------------------------------------------------------+
input group "===== 指标设置 ====="
input string   InpIndicatorName    = "HalfTrend";  // 指标文件名(不含.ex5后缀)
input int      InpIndicatorAmplitude = 2;           // 指标参数:灵敏度
input int      InpIndicatorAtrPeriod = 100;         // 指标参数:ATR周期
input int      InpBuyBufferIndex   = 6;             // 买入箭头缓冲区序号(从0开始)
input int      InpSellBufferIndex  = 7;             // 卖出箭头缓冲区序号(从0开始)

input group "===== 基础设置 ====="
input double   InpLots             = 0.01;          // 首单手数
input int      InpMagicNumber      = 20261021;      // EA 魔术号(用于识别自己的订单)
input string   InpComment          = "HT_Martin";   // 订单备注

input group "===== 止损设置 ====="
input int      InpStopLookback     = 10;            // 计算止损用的K线根数(推荐10)
input int      InpStopBuffer       = 1000;          // 止损缓冲点数(在高低点基础上再留的余量)

input group "===== 加仓设置 ====="
input bool     InpEnableMartingale = true;          // 是否启用马丁加仓
input int      InpMartinLayers     = 3;             // 最大加仓层数(含首单共3单)
input double   InpMartinMultiplier = 1.2;           // 加仓手数倍数(每层手数 × 1.2)
input int      InpMartinDistance   = 600;           // 加仓距离(浮亏多少点后加仓)

input group "===== 移动止盈设置 ====="
input bool     InpEnableTrailing   = true;          // 是否启用移动止盈
input int      InpTrailingStart    = 300;           // 启动移动止盈的盈利点数(价格盈利超过此值后开始跟踪)
input int      InpTrailingStep     = 150;           // 移动止盈跟踪距离(止损与价格保持的距离)

input group "===== 风控设置 ====="
input int      InpMaxSpread        = 300;           // 最大允许点差(超过此值不开仓)
input int      InpMaxSlippage      = 50;            // 最大滑点(点数)

input group "===== 可视化面板 ====="
input bool     InpShowPanel        = true;          // 是否显示可视化面板
input int      InpPanelX           = 20;            // 面板X坐标(像素)
input int      InpPanelY           = 50;            // 面板Y坐标(像素)
input color    InpPanelColor       = clrWhite;      // 面板文字颜色
input int      InpPanelFontSize    = 9;             // 面板字体大小

//+------------------------------------------------------------------+
//| 全局变量                                                          |
//+------------------------------------------------------------------+
CTrade         g_trade;                             // 交易对象
int            g_indicatorHandle = INVALID_HANDLE;  // 指标句柄

// 持仓状态跟踪
struct PositionState
{
   bool     hasPosition;      // 是否有持仓
   int      direction;        // 0=无, 1=多, -1=空
   int      layers;           // 已开仓层数(1、2、3)
   double   firstPrice;       // 首单开仓价
   double   stopLoss;         // 当前止损价
   double   totalLots;        // 总手数
   double   highestPrice;     // 多单:持仓后的最高价;用于移动止盈
   double   lowestPrice;      // 空单:持仓后的最低价;用于移动止盈
   bool     layerAdded[10];   // 每层是否已加仓(防止重复加仓)
};
PositionState g_position;

// 每日统计
struct DailyStats
{
   datetime lastResetDate;    // 上次重置日期
   int      todayOrders;      // 当日开单数
   double   todayProfit;      // 当日盈亏(金额)
   double   maxDrawdown;      // 最大回撤
};
DailyStats g_stats;

datetime g_lastSignalTime = 0;  // 上次信号时间(防止同一根K线重复开仓)
string   g_panelPrefix = "HTPanel_"; // 面板对象名前缀

//+------------------------------------------------------------------+
//| 初始化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- 参数校验
   if(InpLots <= 0)
   {
      Print("[错误] 首单手数必须大于 0");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(InpStopLookback < 1)
   {
      Print("[错误] 止损回溯K线数必须大于等于 1");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(InpMartinLayers < 1 || InpMartinLayers > 10)
   {
      Print("[错误] 加仓层数必须在 1-10 之间");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(InpMartinMultiplier < 1.0)
   {
      Print("[错误] 加仓倍数必须大于等于 1.0");
      return INIT_PARAMETERS_INCORRECT;
   }

   //--- 设置 CTrade
   g_trade.SetExpertMagicNumber(InpMagicNumber);
   g_trade.SetDeviationInPoints(InpMaxSlippage);
   g_trade.SetTypeFilling(ORDER_FILLING_FOK);
   if(!g_trade.SetTypeFillingBySymbol(_Symbol))
      g_trade.SetTypeFilling(ORDER_FILLING_IOC);

   //--- 创建指标句柄
   g_indicatorHandle = iCustom(_Symbol, PERIOD_CURRENT, InpIndicatorName,
                                InpIndicatorAmplitude,
                                InpIndicatorAtrPeriod,
                                true,   // InpShowBands
                                true,   // InpShowArrows
                                false,  // InpAlertsOn
                                false, false, false, false);
   if(g_indicatorHandle == INVALID_HANDLE)
   {
      Print("[错误] 无法创建指标句柄,请确认指标文件名: ", InpIndicatorName, ".ex5,错误代码 ", GetLastError());
      return INIT_FAILED;
   }

   //--- 初始化持仓状态和统计
   ResetPositionState();
   ResetDailyStats();

   //--- 创建可视化面板
   if(InpShowPanel)
      CreatePanel();

   //--- 扫描现有持仓,恢复状态(断线重启时)
   RestorePositionState();

   //--- 打印所有参数到日志
   Print("========== HalfTrend 马丁 EA 启动 ==========");
   Print("[参数] 指标名称: ", InpIndicatorName, ",买入缓冲区: ", InpBuyBufferIndex, ",卖出缓冲区: ", InpSellBufferIndex);
   Print("[参数] 首单手数: ", InpLots);
   Print("[参数] 魔术号: ", InpMagicNumber);
   Print("[参数] 止损回溯K线: ", InpStopLookback, " 根,缓冲点数: ", InpStopBuffer);
   Print("[参数] 马丁加仓: ", (InpEnableMartingale ? "启用" : "禁用"), ",最大层数: ", InpMartinLayers, ",倍数: ", InpMartinMultiplier, ",距离: ", InpMartinDistance, " 点");
   Print("[参数] 移动止盈: ", (InpEnableTrailing ? "启用" : "禁用"), ",启动: ", InpTrailingStart, " 点,跟踪: ", InpTrailingStep, " 点");
   Print("[参数] 最大点差: ", InpMaxSpread, " 点,最大滑点: ", InpMaxSlippage, " 点");
   Print("============================================");

   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| 卸载                                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(g_indicatorHandle != INVALID_HANDLE)
      IndicatorRelease(g_indicatorHandle);

   if(InpShowPanel)
      DeletePanel();

   Print("[卸载] HalfTrend 马丁 EA 已停止,原因代码: ", reason);
}

//+------------------------------------------------------------------+
//| 主循环 - 修复:加仓检查移到这里,每个 Tick 都执行                 |
//+------------------------------------------------------------------+
void OnTick()
{
   //--- 检查是否需要重置每日统计(跨天)
   CheckDailyReset();

   //--- 更新当前持仓状态
   UpdatePositionState();

   //--- 持续监控:移动止盈
   if(InpEnableTrailing && g_position.hasPosition)
      ManageTrailingStop();

   //--- 持续监控:马丁加仓(关键修复:每个 Tick 都检查)
   if(InpEnableMartingale && g_position.hasPosition && g_position.layers < InpMartinLayers)
      CheckAndAddPosition();

   //--- 检查新信号
   int signal = GetSignal();

   if(signal == 1) // 买入信号
   {
      HandleBuySignal();
   }
   else if(signal == -1) // 卖出信号
   {
      HandleSellSignal();
   }

   //--- 更新可视化面板
   if(InpShowPanel)
      UpdatePanel();
}

//+------------------------------------------------------------------+
//| 获取指标信号                                                      |
//| 返回:1=买入箭头, -1=卖出箭头, 0=无信号                           |
//+------------------------------------------------------------------+
int GetSignal()
{
   if(BarsCalculated(g_indicatorHandle) < 2)
      return 0;

   double arrUp[2], arrDn[2];
   if(CopyBuffer(g_indicatorHandle, InpBuyBufferIndex, 0, 2, arrUp) < 2) return 0;
   if(CopyBuffer(g_indicatorHandle, InpSellBufferIndex, 0, 2, arrDn) < 2) return 0;

   //--- 读取倒数第二根已收盘 K 线的箭头(索引 0)
   datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 1);
   
   if(arrUp[0] != EMPTY_VALUE && arrUp[0] > 0 && g_lastSignalTime != barTime)
   {
      g_lastSignalTime = barTime;
      return 1;
   }
   if(arrDn[0] != EMPTY_VALUE && arrDn[0] > 0 && g_lastSignalTime != barTime)
   {
      g_lastSignalTime = barTime;
      return -1;
   }

   return 0;
}

//+------------------------------------------------------------------+
//| 处理买入信号                                                      |
//+------------------------------------------------------------------+
void HandleBuySignal()
{
   //--- 如果当前有空单,先全部平掉
   if(g_position.direction == -1)
   {
      Print("[信号] 检测到买入信号,平掉所有空单");
      CloseAllPositions();
      ResetPositionState();
   }

   //--- 如果已有多单,不重复开仓(加仓由 CheckAndAddPosition 负责)
   if(g_position.direction == 1)
   {
      Print("[跳过] 已有多单,等待加仓逻辑触发");
      return;
   }

   //--- 无持仓,开首单
   OpenFirstPosition(1);
}

//+------------------------------------------------------------------+
//| 处理卖出信号                                                      |
//+------------------------------------------------------------------+
void HandleSellSignal()
{
   //--- 如果当前有多单,先全部平掉
   if(g_position.direction == 1)
   {
      Print("[信号] 检测到卖出信号,平掉所有多单");
      CloseAllPositions();
      ResetPositionState();
   }

   //--- 如果已有空单,不重复开仓
   if(g_position.direction == -1)
   {
      Print("[跳过] 已有空单,等待加仓逻辑触发");
      return;
   }

   //--- 无持仓,开首单
   OpenFirstPosition(-1);
}

//+------------------------------------------------------------------+
//| 开首单                                                            |
//+------------------------------------------------------------------+
void OpenFirstPosition(int direction)
{
   //--- 检查点差
   double spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   if(spread > InpMaxSpread)
   {
      Print("[跳过] 当前点差 ", (int)spread, " 点,超过限制 ", InpMaxSpread, " 点");
      return;
   }

   //--- 检查交易是否允许
   if(!SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE))
   {
      Print("[跳过] 交易未启用");
      return;
   }

   //--- 规整手数
   double lots = NormalizeLots(InpLots);
   if(lots <= 0)
   {
      Print("[错误] 手数规整后为 0,无法开仓");
      return;
   }

   //--- 计算止损价
   double sl = CalculateStopLoss(direction);
   if(sl <= 0)
   {
      Print("[错误] 止损价计算失败");
      return;
   }

   //--- 获取当前价格
   double price = (direction == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);

   //--- 检查止损距离是否满足最小要求
   int stopsLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDistance = stopsLevel * _Point;
   if(MathAbs(price - sl) < minDistance)
   {
      Print("[警告] 止损距离 ", (int)(MathAbs(price - sl) / _Point), " 点小于最小距离 ", stopsLevel, " 点,调整止损");
      if(direction == 1)
         sl = price - minDistance - 10 * _Point;
      else
         sl = price + minDistance + 10 * _Point;
   }

   //--- 下单
   bool result = false;
   if(direction == 1)
      result = g_trade.Buy(lots, _Symbol, 0, sl, 0, InpComment);
   else
      result = g_trade.Sell(lots, _Symbol, 0, sl, 0, InpComment);

   if(result)
   {
      g_position.hasPosition = true;
      g_position.direction = direction;
      g_position.layers = 1;
      g_position.firstPrice = price;
      g_position.stopLoss = sl;
      g_position.totalLots = lots;
      g_position.highestPrice = price;
      g_position.lowestPrice = price;
      ArrayInitialize(g_position.layerAdded, false);
      g_position.layerAdded[0] = true;

      g_stats.todayOrders++;

      Print("[开仓] ", (direction == 1 ? "做多" : "做空"), " ", lots, " 手,价格: ", price,
            ",止损: ", sl, ",止损距离: ", (int)(MathAbs(price - sl) / _Point), " 点");
   }
   else
   {
      Print("[失败] 开仓失败,错误代码: ", g_trade.ResultRetcode(), ",错误描述: ", g_trade.ResultRetcodeDescription());
   }
}

//+------------------------------------------------------------------+
//| 检查并加仓 - 修复:移除 direction 参数,直接读取 g_position       |
//+------------------------------------------------------------------+
void CheckAndAddPosition()
{
   if(!g_position.hasPosition)
      return;

   int direction = g_position.direction;

   //--- 计算当前浮动盈亏(点数)
   double currentPrice = (direction == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double floatingPips = 0;
   if(direction == 1)
      floatingPips = (currentPrice - g_position.firstPrice) / _Point;
   else
      floatingPips = (g_position.firstPrice - currentPrice) / _Point;

   //--- 计算应该加到第几层
   int targetLayer = 1;
   double lossThreshold = -InpMartinDistance;
   while(targetLayer < InpMartinLayers && floatingPips < lossThreshold)
   {
      targetLayer++;
      lossThreshold -= InpMartinDistance;
   }

   //--- 如果应加的层还没加过
   if(targetLayer > g_position.layers && !g_position.layerAdded[targetLayer - 1])
   {
      //--- 检查点差
      double spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
      if(spread > InpMaxSpread)
      {
         Print("[跳过] 加仓:当前点差 ", (int)spread, " 点,超过限制");
         return;
      }

      //--- 计算加仓手数
      double newLots = InpLots;
      for(int i = 1; i < targetLayer; i++)
         newLots *= InpMartinMultiplier;
      newLots = NormalizeLots(newLots);

      if(newLots <= 0)
      {
         Print("[错误] 加仓手数规整后为 0");
         return;
      }

      //--- 加仓单使用相同的止损
      bool result = false;
      if(direction == 1)
         result = g_trade.Buy(newLots, _Symbol, 0, g_position.stopLoss, 0, InpComment);
      else
         result = g_trade.Sell(newLots, _Symbol, 0, g_position.stopLoss, 0, InpComment);

      if(result)
      {
         g_position.layers = targetLayer;
         g_position.totalLots += newLots;
         g_position.layerAdded[targetLayer - 1] = true;
         g_stats.todayOrders++;

         Print("[加仓] 第 ", targetLayer, " 层,", (direction == 1 ? "做多" : "做空"), " ", newLots,
               " 手,浮亏: ", (int)floatingPips, " 点,总手数: ", g_position.totalLots);
      }
      else
      {
         Print("[失败] 加仓失败,错误代码: ", g_trade.ResultRetcode(), ",描述: ", g_trade.ResultRetcodeDescription());
      }
   }
}

//+------------------------------------------------------------------+
//| 管理移动止盈                                                      |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
   double currentPrice = (g_position.direction == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   if(g_position.direction == 1) // 多单
   {
      //--- 更新最高价
      if(currentPrice > g_position.highestPrice)
         g_position.highestPrice = currentPrice;

      //--- 盈利超过启动距离
      double profit = (currentPrice - g_position.firstPrice) / _Point;
      if(profit > InpTrailingStart)
      {
         double newSL = currentPrice - InpTrailingStep * _Point;
         //--- 止损只能上移
         if(newSL > g_position.stopLoss + _Point)
         {
            ModifyAllPositionsSL(newSL);
         }
      }
   }
   else if(g_position.direction == -1) // 空单
   {
      //--- 更新最低价
      if(currentPrice < g_position.lowestPrice)
         g_position.lowestPrice = currentPrice;

      //--- 盈利超过启动距离
      double profit = (g_position.firstPrice - currentPrice) / _Point;
      if(profit > InpTrailingStart)
      {
         double newSL = currentPrice + InpTrailingStep * _Point;
         //--- 止损只能下移
         if(newSL < g_position.stopLoss - _Point)
         {
            ModifyAllPositionsSL(newSL);
         }
      }
   }
}

//+------------------------------------------------------------------+
//| 修改所有同方向持仓的止损                                          |
//+------------------------------------------------------------------+
void ModifyAllPositionsSL(double newSL)
{
   int modifiedCount = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;

      double currentSL = PositionGetDouble(POSITION_SL);
      double tp = PositionGetDouble(POSITION_TP);

      if(MathAbs(newSL - currentSL) > _Point)
      {
         if(g_trade.PositionModify(ticket, newSL, tp))
            modifiedCount++;
      }
   }

   if(modifiedCount > 0)
   {
      Print("[移动止盈] 已修改 ", modifiedCount, " 个订单,新止损: ", newSL);
      g_position.stopLoss = newSL;
   }
}

//+------------------------------------------------------------------+
//| 计算止损价(近N根K线高低点 ± 缓冲)                               |
//+------------------------------------------------------------------+
double CalculateStopLoss(int direction)
{
   double highArray[], lowArray[];
   ArraySetAsSeries(highArray, true);
   ArraySetAsSeries(lowArray, true);

   int copied = CopyHigh(_Symbol, PERIOD_CURRENT, 0, InpStopLookback + 1, highArray);
   if(copied < InpStopLookback + 1) return 0;
   copied = CopyLow(_Symbol, PERIOD_CURRENT, 0, InpStopLookback + 1, lowArray);
   if(copied < InpStopLookback + 1) return 0;

   double highest = highArray[ArrayMaximum(highArray, 0, InpStopLookback)];
   double lowest  = lowArray[ArrayMinimum(lowArray, 0, InpStopLookback)];

   double sl = 0;
   if(direction == 1)
      sl = lowest - InpStopBuffer * _Point;
   else
      sl = highest + InpStopBuffer * _Point;

   return NormalizeDouble(sl, _Digits);
}

//+------------------------------------------------------------------+
//| 规整手数                                                          |
//+------------------------------------------------------------------+
double NormalizeLots(double lots)
{
   double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   if(lots < minLot) lots = minLot;
   if(lots > maxLot) lots = maxLot;

   lots = MathFloor(lots / stepLot) * stepLot;
   return NormalizeDouble(lots, 2);
}

//+------------------------------------------------------------------+
//| 平掉所有本 EA 的持仓                                              |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;

      double profit = PositionGetDouble(POSITION_PROFIT);
      g_stats.todayProfit += profit;

      if(g_trade.PositionClose(ticket))
         Print("[平仓] 订单 ", ticket, " 已平仓,盈亏: ", profit);
      else
         Print("[失败] 平仓订单 ", ticket, " 失败,错误代码: ", g_trade.ResultRetcode());
   }
}

//+------------------------------------------------------------------+
//| 更新持仓状态 - 修复:恢复 firstPrice                              |
//+------------------------------------------------------------------+
void UpdatePositionState()
{
   int buyCount = 0, sellCount = 0;
   double totalBuyLots = 0, totalSellLots = 0;
   double firstBuyPrice = 0, firstSellPrice = 0;
   double buySL = 0, sellSL = 0;

   for(int i = 0; i < PositionsTotal(); i++)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double lots = PositionGetDouble(POSITION_VOLUME);
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double sl = PositionGetDouble(POSITION_SL);
      datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);

      if(type == POSITION_TYPE_BUY)
      {
         buyCount++;
         totalBuyLots += lots;
         //--- 首单价格:取最早开仓的那一单
         if(firstBuyPrice == 0 || openTime < (datetime)PositionGetInteger(POSITION_TIME))
         {
            firstBuyPrice = openPrice;
            buySL = sl;
         }
      }
      else if(type == POSITION_TYPE_SELL)
      {
         sellCount++;
         totalSellLots += lots;
         if(firstSellPrice == 0 || openTime < (datetime)PositionGetInteger(POSITION_TIME))
         {
            firstSellPrice = openPrice;
            sellSL = sl;
         }
      }
   }

   //--- 如果没有持仓,重置状态
   if(buyCount == 0 && sellCount == 0)
   {
      if(g_position.hasPosition)
      {
         Print("[状态] 所有持仓已平仓");
         ResetPositionState();
      }
      return;
   }

   //--- 更新状态
   if(buyCount > 0)
   {
      g_position.hasPosition = true;
      g_position.direction = 1;
      g_position.layers = buyCount;
      g_position.totalLots = totalBuyLots;
      //--- 如果 firstPrice 还是0(说明刚启动EA),就从持仓恢复
      if(g_position.firstPrice == 0)
         g_position.firstPrice = firstBuyPrice;
      if(g_position.stopLoss == 0)
         g_position.stopLoss = buySL;
   }
   else if(sellCount > 0)
   {
      g_position.hasPosition = true;
      g_position.direction = -1;
      g_position.layers = sellCount;
      g_position.totalLots = totalSellLots;
      if(g_position.firstPrice == 0)
         g_position.firstPrice = firstSellPrice;
      if(g_position.stopLoss == 0)
         g_position.stopLoss = sellSL;
   }
}

//+------------------------------------------------------------------+
//| 恢复持仓状态(断线重启时)                                        |
//+------------------------------------------------------------------+
void RestorePositionState()
{
   UpdatePositionState();

   if(g_position.hasPosition)
   {
      Print("[恢复] 检测到现有持仓:方向=", (g_position.direction == 1 ? "多" : "空"),
            ",层数=", g_position.layers, ",首单价=", g_position.firstPrice,
            ",止损=", g_position.stopLoss, ",总手数=", g_position.totalLots);
      
      //--- 标记已有的层数为"已加仓"
      for(int i = 0; i < g_position.layers; i++)
         g_position.layerAdded[i] = true;
   }
}

//+------------------------------------------------------------------+
//| 重置持仓状态                                                      |
//+------------------------------------------------------------------+
void ResetPositionState()
{
   g_position.hasPosition = false;
   g_position.direction = 0;
   g_position.layers = 0;
   g_position.firstPrice = 0;
   g_position.stopLoss = 0;
   g_position.totalLots = 0;
   g_position.highestPrice = 0;
   g_position.lowestPrice = 0;
   ArrayInitialize(g_position.layerAdded, false);
}

//+------------------------------------------------------------------+
//| 检查每日重置                                                      |
//+------------------------------------------------------------------+
void CheckDailyReset()
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   datetime today = StringToTime(IntegerToString(dt.year) + "." + IntegerToString(dt.mon) + "." + IntegerToString(dt.day));

   if(g_stats.lastResetDate != today)
   {
      if(g_stats.lastResetDate != 0)
         Print("[统计] 昨日盈亏: ", g_stats.todayProfit, " USD,开单数: ", g_stats.todayOrders);
      
      ResetDailyStats();
      g_stats.lastResetDate = today;
   }
}

//+------------------------------------------------------------------+
//| 重置每日统计                                                      |
//+------------------------------------------------------------------+
void ResetDailyStats()
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   g_stats.lastResetDate = StringToTime(IntegerToString(dt.year) + "." + IntegerToString(dt.mon) + "." + IntegerToString(dt.day));
   g_stats.todayOrders = 0;
   g_stats.todayProfit = 0;
   g_stats.maxDrawdown = 0;
}

//+------------------------------------------------------------------+
//| 创建可视化面板                                                    |
//+------------------------------------------------------------------+
void CreatePanel()
{
   string labels[] = {"标题", "状态", "层数", "手数", "浮盈", "今日盈亏", "今日单数"};
   int count = ArraySize(labels);

   for(int i = 0; i < count; i++)
   {
      string objName = g_panelPrefix + IntegerToString(i);
      if(ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0))
      {
         ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
         ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, InpPanelX);
         ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, InpPanelY + i * (InpPanelFontSize + 5));
         ObjectSetInteger(0, objName, OBJPROP_COLOR, InpPanelColor);
         ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, InpPanelFontSize);
         ObjectSetString(0, objName, OBJPROP_FONT, "Arial");
         ObjectSetString(0, objName, OBJPROP_TEXT, labels[i]);
      }
   }
}

//+------------------------------------------------------------------+
//| 更新可视化面板                                                    |
//+------------------------------------------------------------------+
void UpdatePanel()
{
   string status = "无持仓";
   string layers = "-";
   string lots = "-";
   string floating = "-";
   
   if(g_position.hasPosition)
   {
      status = (g_position.direction == 1) ? "多单" : "空单";
      layers = IntegerToString(g_position.layers) + " / " + IntegerToString(InpMartinLayers);
      lots = DoubleToString(g_position.totalLots, 2);
      
      //--- 计算浮动盈亏
      double floatingProfit = 0;
      for(int i = 0; i < PositionsTotal(); i++)
      {
         ulong ticket = PositionGetTicket(i);
         if(ticket == 0) continue;
         if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
         if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
         floatingProfit += PositionGetDouble(POSITION_PROFIT);
      }
      floating = DoubleToString(floatingProfit, 2) + " USD";
   }

   ObjectSetString(0, g_panelPrefix + "0", OBJPROP_TEXT, "=== HalfTrend 马丁 EA ===");
   ObjectSetString(0, g_panelPrefix + "1", OBJPROP_TEXT, "状态: " + status);
   ObjectSetString(0, g_panelPrefix + "2", OBJPROP_TEXT, "层数: " + layers);
   ObjectSetString(0, g_panelPrefix + "3", OBJPROP_TEXT, "手数: " + lots);
   ObjectSetString(0, g_panelPrefix + "4", OBJPROP_TEXT, "浮盈: " + floating);
   ObjectSetString(0, g_panelPrefix + "5", OBJPROP_TEXT, "今日盈亏: " + DoubleToString(g_stats.todayProfit, 2) + " USD");
   ObjectSetString(0, g_panelPrefix + "6", OBJPROP_TEXT, "今日开单: " + IntegerToString(g_stats.todayOrders) + " 次");
}

//+------------------------------------------------------------------+
//| 删除可视化面板                                                    |
//+------------------------------------------------------------------+
void DeletePanel()
{
   for(int i = 0; i < 10; i++)
   {
      string objName = g_panelPrefix + IntegerToString(i);
      ObjectDelete(0, objName);
   }
}
//+------------------------------------------------------------------+

作品说明

你说得对,我发现了问题:加仓检查只在有新箭头信号时才执行,但马丁加仓应该每个 Tick 都持续监控浮亏。

另外上次代码被截断了,我现在给你完整修复版:

使用说明

  1. 安装:将指标 HalfTrend.ex5 和本 EA 都放入对应目录,重启 MT5 后挂载到图表上
  2. 关键修复:
  • 马丁加仓现在每个 Tick 都检查浮亏,不再依赖新信号触发
  • UpdatePositionState() 正确恢复首单价格,断线重启也能继续管理
  • 增加 layerAdded 数组防止重复加仓
  1. 参数调整:
  • 指标名称:如果你的指标文件名不是 HalfTrend.ex5,修改"指标文件名"参数
  • 缓冲区序号:默认 6 是买入箭头、7 是卖出箭头,如果指标不同需调整
  • 马丁距离:600 点表示浮亏 600 点后加第二层,再亏 600 点加第三层
  1. 测试建议:先在模拟盘小手数测试,观察"专家"日志确认加仓逻辑是否触发
  2. 面板显示:左上角显示当前状态、层数、浮盈、今日盈亏等,方便监控
  3. 止损保护:所有加仓单共用首单的止损位,不会因加仓而扩大风险暴露

AI 解读根据源码逐行分析写成,供参考,以实际运行为准

这是什么

这是一个基于 HalfTrend 指标箭头信号的全自动交易 EA,专为 MT5 设计。它在指标发出买卖箭头时开仓,用近 10 根 K 线的高低点计算止损位,持仓浮亏时按马丁策略加仓最多 3 层,盈利后自动跟踪止盈。适合想用趋势指标信号 + 加仓策略提高盈亏比的交易者。

逻辑原理

信号来源:EA 调用自定义指标 HalfTrend.ex5(需单独安装),读取缓冲区 6 的买入箭头和缓冲区 7 的卖出箭头。检测到新箭头时(倒数第二根已收盘 K 线),如果当前无持仓或持有反向仓位就执行开仓或平仓转向。

止损计算:开首单前,EA 读取最近 10 根 K 线的最高点和最低点。做多时止损 = 最低点 - 1000 点缓冲,做空时止损 = 最高点 + 1000 点缓冲。所有加仓单共用首单的这个止损位,不会因加仓而扩大风险暴露。

马丁加仓:每个 Tick 检查浮动盈亏。首单后,如果浮亏超过 600 点触发第二层加仓,再亏 600 点触发第三层。每层手数 = 上一层手数 × 1.2 倍。用 layerAdded 数组防止同一层重复加仓。加仓单与首单方向相同,使用相同止损价。

移动止盈:持仓盈利超过 300 点(价格盈利,非金额)后启动。多单时,止损跟随价格保持 150 点距离,且只能向上移动;空单反向。移动止盈会修改所有同方向持仓的止损,实现部分利润保护。

信号转向:检测到反向箭头时,EA 先平掉所有当前持仓(含加仓单),统计盈亏,然后按新方向开首单。

参数说明

指标设置

  • 指标文件名(HalfTrend):自定义指标的文件名,不含 .ex5 后缀,需与 Indicators 文件夹里的文件名一致
  • 指标参数:灵敏度(2):传给 HalfTrend 的 Amplitude 参数,值越小越灵敏
  • 指标参数:ATR 周期(100):HalfTrend 用来计算波动的 ATR 周期数
  • 买入箭头缓冲区序号(6):HalfTrend 指标的买入箭头缓冲区索引,默认 6
  • 卖出箭头缓冲区序号(7):卖出箭头缓冲区索引,默认 7

基础设置

  • 首单手数(0.01):第一次开仓的手数,后续加仓会乘以倍数
  • EA 魔术号(20261021):用于识别 EA 自己的订单,不要与其他 EA 重复
  • 订单备注(HT_Martin):所有订单的注释字段

止损设置

  • 计算止损用的 K 线根数(10):向前回溯多少根 K 线找高低点,10 根约等于看近期波动
  • 止损缓冲点数(1000):在高低点基础上再留的安全距离,1000 点 = 100 点(五位报价)

加仓设置

  • 是否启用马丁加仓(true):关闭后只开首单,不加仓
  • 最大加仓层数(3):含首单最多 3 单,即首单 + 2 次加仓
  • 加仓手数倍数(1.2):每层手数是上一层的 1.2 倍,首单 0.01 则二层 0.012、三层 0.0144
  • 加仓距离(600):浮亏多少点后触发加仓,600 点 = 60 点(五位报价)

移动止盈设置

  • 是否启用移动止盈(true):关闭后止损固定不移动
  • 启动移动止盈的盈利点数(300):价格盈利超过 300 点(30 点五位)后开始跟踪
  • 移动止盈跟踪距离(150):止损与当前价格保持的距离,150 点 = 15 点

风控设置

  • 最大允许点差(300):点差超过此值不开仓,防止高点差时段交易
  • 最大滑点(50):下单时允许的最大滑点,超过会拒单

可视化面板

  • 是否显示可视化面板(true):在图表左上角显示 EA 状态、层数、浮盈等信息
  • 其余为面板位置和样式参数

怎么用

  1. 安装指标:先把 HalfTrend.ex5 指标文件放到 MT5 的 MQL5\Indicators 文件夹,重启 MT5
  2. 安装 EA:将本 EA 文件放到 MQL5\Experts 文件夹,刷新导航器
  3. 挂载图表:把 EA 拖到任意品种任意周期的图表上(推荐 H1 或 H4),勾选"允许自动交易"
  4. 参数调整:首次使用建议把首单手数改成 0.01,马丁距离根据品种波动调整(黄金可用 600-1000 点,货币对可用 300-500 点)
  5. 观察信号:EA 会在图表左上角显示面板,"状态"显示当前持仓方向,"层数"显示已开几单
  6. 查看日志:打开"专家"选项卡,EA 会输出每次开仓、加仓、平仓、移动止盈的详细信息
  7. 监控持仓:在"交易"窗口可以看到所有订单,备注都是"HT_Martin",止损价相同

适用场景

适合的品种:XAUUSD(黄金)、EURUSD、GBPUSD 等波动较大的主流品种,因为需要足够的趋势空间让移动止盈发挥作用。

适合的周期:H1(1 小时)或 H4(4 小时)。太小的周期(M5、M15)信号频繁且噪音多,容易假突破;太大的周期(D1)信号太少且止损过大。

适合的行情:单边趋势行情,HalfTrend 指标在趋势中表现较好,箭头出现后价格持续朝信号方向走,移动止盈能锁住利润。震荡行情中指标会频繁转向,每次转向都平仓重开,可能反复止损。

不适合的场景:

  • 窄幅震荡:价格在小区间来回扫,止损容易被打掉,加仓也救不回来
  • 重大新闻前后:点差暴涨、滑点严重,EA 会拒绝开仓但已有持仓可能被快速止损
  • 低流动性时段:亚洲盘尾盘或节假日,点差大且容易假突破

风险提示

马丁加仓风险:浮亏时加仓会放大仓位,如果行情持续逆势,3 层加仓的总手数是首单的 3.64 倍(1 + 1.2 + 1.44)。止损一旦触发,亏损金额比单笔首单大得多。小账户或大手数慎用。

指标滞后:HalfTrend 是趋势跟踪指标,箭头出现时价格通常已经走了一段,属于追涨杀跌类型。如果趋势很快反转,可能刚开仓就浮亏触发加仓。

止损距离固定:首单止损用近 10 根 K 线高低点计算,行情剧烈波动时 10 根可能覆盖不够,止损可能过近;平静行情时止损又可能过远。需根据品种和周期测试调整 InpStopLookback 和 InpStopBuffer。

移动止盈门槛:必须盈利 300 点(默认参数)才启动跟踪,如果行情只给 200 点利润就反转,移动止盈根本没机会工作,最终可能被固定止损打掉或回吐利润。

信号转向清仓:一旦出现反向箭头,EA 立即平掉所有持仓(含加仓单)。如果指标在震荡中反复变向,会导致频繁开平仓,手续费和滑点累积。

无重绘检测:代码未判断 HalfTrend 指标是否重绘。如果指标在新 K 线形成后修改历史箭头,EA 的信号逻辑会失效(不过 EA 读取的是倒数第二根已收盘 K 线,一定程度避免了当前柱重绘)。

点差和滑点:EA 只在点差超过 300 点时拒绝开仓,但加仓时也检查点差。实际成交价可能因滑点偏离预期,尤其是快速行情中的加仓单。

常见问题

这个 EA 会重绘吗?
EA 本身不重绘,它只在每根 K 线收盘后读取指标箭头。但如果 HalfTrend 指标本身会重绘历史箭头,可能影响回测结果的准确性。实盘中 EA 读取的是已收盘 K 线,重绘影响较小。
马丁加仓最多亏多少?
默认参数下,3 层加仓总手数是首单的 3.64 倍。如果首单 0.01 手,止损 1000 点(100 点五位报价),最大亏损约 = 0.01 × 3.64 × 100 × 每点价值。黄金每点 1 美元时约 364 美元,具体看品种合约规格。
可以用在 XAUUSD 以外的品种吗?
可以。代码没有品种限制,但参数需调整。货币对波动比黄金小,马丁距离(InpMartinDistance)建议改成 300-500 点,止损缓冲(InpStopBuffer)改成 500-800 点。原油、指数等也能用,需自行回测优化参数。
MT4 能用吗?
不能。这是 MQL5 代码,只能在 MT5 平台运行。MT4 需要用 MQL4 重写,且 MT4 的 CTrade 类和指标调用方式不同。
参数默认值适合什么周期?
默认参数(马丁距离 600 点、止损回溯 10 根、移动止盈 300 点启动)适合 H1 或 H4 周期的黄金交易。M15 以下周期建议减小马丁距离到 300-400 点,D1 周期建议加大到 1000-1500 点,并延长止损回溯到 20 根。

HalfTrend Martingale EA with Dynamic Stop & Trailing for MT5

An automated trading EA based on HalfTrend indicator arrow signals. Features dynamic stop-loss from 10-bar high/low, martingale position adding (max 3 layers) triggered at 600-point floating loss, trailing stop activated at 300-point profit, and a visual dashboard for monitoring.

Source code is shown above. Compiled EX5 files are available to VIP members.

评论

后可以留言,比如反馈用着怎么样、想加什么功能。

还没有人评论,来抢个沙发。