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

持仓限制控制器 - 自动风控系统

taiyu1983·1 小时前发布

实时显示持仓统计,自动监控总手数,达到限制后禁止新开仓并自动平掉超限订单,提供一键平仓、移动止盈、固定止损、挂单价止盈功能

持仓限制控制器 - 自动风控系统

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

PositionLimitController.mq51068
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
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
#property copyright "XAUOF 指标工坊"
#property version   "1.40"
#property strict

#include <Trade\Trade.mqh>

//--- 可调参数
input double   MaxTotalLots = 10.0;        // 最大总手数限制(0=不限制)
input bool     AutoCloseLimitExcess = true; // 超限自动平仓(true=自动平,false=仅警告)
input int      TrailingStopPoints = 300;   // 移动止盈点数(0=不启用)
input int      FixedStopLossPoints = 500;  // 固定止损点数(用于一键设置)
input int      BreakEvenPoints = 200;      // 挂单价止盈点数(盈利超过此值移动到开仓价)
input int      InitPanelX = 20;            // 面板初始左边距(像素)
input int      InitPanelY = 30;            // 面板初始上边距(像素)
input int      PanelWidth = 340;           // 面板宽度(像素)
input int      PanelHeight = 360;          // 面板高度(像素)
input color    PanelBgColor = clrDarkSlateGray; // 面板背景色
input color    TitleBgColor = clrNavy;     // 标题栏背景色
input color    TextColor = clrWhite;       // 文字颜色
input color    BuyColor = clrLime;         // 多头数据颜色
input color    SellColor = clrRed;         // 空头数据颜色
input color    ProfitColor = clrGold;      // 盈亏数据颜色
input int      FontSize = 10;              // 字体大小
input int      RefreshMS = 500;            // 刷新间隔(毫秒)

//--- 全局变量
CTrade trade;
uint lastRefreshTime = 0;
datetime lastCheckTime = 0;
bool trailingStopEnabled = false;  // 移动止盈开关

// 面板位置和大小
int panelX, panelY;
int panelW, panelH;

// 拖动相关变量
bool isDragging = false;
bool isResizing = false;
int dragStartX, dragStartY;
int resizeStartX, resizeStartY;
int resizeStartW, resizeStartH;

// 界面对象名称
string panelBg         = "StatPanelBg";
string panelTitle      = "StatPanelTitle";
string resizeHandle    = "StatResizeHandle";
string labelBuyLots    = "LabelBuyLots";
string labelBuyCount   = "LabelBuyCount";
string labelSellLots   = "LabelSellLots";
string labelSellCount  = "LabelSellCount";
string labelTotalLots  = "LabelTotalLots";
string labelTotalProfit = "LabelTotalProfit";
string labelLimitWarning = "LabelLimitWarning";
string labelSeparator1 = "LabelSeparator1";
string labelSeparator2 = "LabelSeparator2";
string labelSeparator3 = "LabelSeparator3";
string btnCloseAll     = "BtnCloseAll";
string btnCloseBuy     = "BtnCloseBuy";
string btnCloseSell    = "BtnCloseSell";
string btnTrailing     = "BtnTrailing";
string btnFixedSL      = "BtnFixedSL";
string btnBreakEven    = "BtnBreakEven";

//+------------------------------------------------------------------+
//| 初始化                                                             |
//+------------------------------------------------------------------+
int OnInit()
{
   // 初始化面板位置和大小
   panelX = InitPanelX;
   panelY = InitPanelY;
   panelW = PanelWidth;
   panelH = PanelHeight;
   
   // 创建面板界面
   CreatePanel();
   
   // 启用图表事件
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   
   Print("======================================");
   Print("持仓限制控制器已启动");
   if(MaxTotalLots > 0)
   {
      Print("总手数限制: ", MaxTotalLots, " 手");
      Print("超限处理: ", AutoCloseLimitExcess ? "自动平仓" : "仅警告");
   }
   else
   {
      Print("总手数限制: 无限制");
   }
   Print("移动止盈: ", TrailingStopPoints, " 点");
   Print("固定止损: ", FixedStopLossPoints, " 点");
   Print("挂单价止盈触发: ", BreakEvenPoints, " 点");
   Print("提示:可拖动标题栏移动面板");
   Print("======================================");
   
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| 清理                                                               |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // 删除所有界面对象
   ObjectDelete(0, panelBg);
   ObjectDelete(0, panelTitle);
   ObjectDelete(0, resizeHandle);
   ObjectDelete(0, labelBuyLots);
   ObjectDelete(0, labelBuyCount);
   ObjectDelete(0, labelSellLots);
   ObjectDelete(0, labelSellCount);
   ObjectDelete(0, labelTotalLots);
   ObjectDelete(0, labelTotalProfit);
   ObjectDelete(0, labelLimitWarning);
   ObjectDelete(0, labelSeparator1);
   ObjectDelete(0, labelSeparator2);
   ObjectDelete(0, labelSeparator3);
   ObjectDelete(0, btnCloseAll);
   ObjectDelete(0, btnCloseBuy);
   ObjectDelete(0, btnCloseSell);
   ObjectDelete(0, btnTrailing);
   ObjectDelete(0, btnFixedSL);
   ObjectDelete(0, btnBreakEven);
   
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| 每个 Tick 更新                                                     |
//+------------------------------------------------------------------+
void OnTick()
{
   // 按时间间隔刷新面板
   if(GetTickCount() - lastRefreshTime > (uint)RefreshMS)
   {
      UpdatePanel();
      lastRefreshTime = GetTickCount();
   }
   
   // 每秒检查一次手数限制
   datetime currentTime = TimeCurrent();
   if(currentTime > lastCheckTime)
   {
      CheckPositionLimit();
      lastCheckTime = currentTime;
   }
   
   // 如果启用了移动止盈,执行移动止盈逻辑
   if(trailingStopEnabled && TrailingStopPoints > 0)
   {
      ProcessTrailingStop();
   }
}

//+------------------------------------------------------------------+
//| 交易事件监控(关键:在这里拦截新订单)                                |
//+------------------------------------------------------------------+
void OnTrade()
{
   // 立即检查手数限制
   CheckPositionLimit();
}

//+------------------------------------------------------------------+
//| 检查持仓手数限制                                                    |
//+------------------------------------------------------------------+
void CheckPositionLimit()
{
   // 如果没有设置限制,直接返回
   if(MaxTotalLots <= 0) return;
   
   // 统计当前总手数
   double totalLots = 0;
   int totalCount = 0;
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      totalLots += PositionGetDouble(POSITION_VOLUME);
      totalCount++;
   }
   
   // 判断是否超过限制
   if(totalLots > MaxTotalLots)
   {
      double excessLots = totalLots - MaxTotalLots;
      
      Print("⚠ 警告:总手数 ", DoubleToString(totalLots, 2), 
            " 超过限制 ", DoubleToString(MaxTotalLots, 2), 
            " 手,超出 ", DoubleToString(excessLots, 2), " 手");
      
      // 如果启用自动平仓
      if(AutoCloseLimitExcess)
      {
         Print("开始自动平掉超限订单...");
         CloseExcessPositions(excessLots);
      }
      else
      {
         Print("提示:请手动平仓或调整限制参数");
      }
   }
}

//+------------------------------------------------------------------+
//| 平掉超限订单(按开仓时间从新到旧)                                   |
//+------------------------------------------------------------------+
void CloseExcessPositions(double excessLots)
{
   // 创建订单列表(按开仓时间排序)
   struct PositionInfo
   {
      ulong ticket;
      datetime openTime;
      double lots;
   };
   
   PositionInfo positions[];
   int count = 0;
   
   // 收集所有持仓
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      count++;
      ArrayResize(positions, count);
      
      positions[count-1].ticket = ticket;
      positions[count-1].openTime = (datetime)PositionGetInteger(POSITION_TIME);
      positions[count-1].lots = PositionGetDouble(POSITION_VOLUME);
   }
   
   // 按开仓时间从新到旧排序(最新的订单先平)
   for(int i = 0; i < count - 1; i++)
   {
      for(int j = i + 1; j < count; j++)
      {
         if(positions[i].openTime < positions[j].openTime)
         {
            PositionInfo temp = positions[i];
            positions[i] = positions[j];
            positions[j] = temp;
         }
      }
   }
   
   // 从最新的订单开始平仓
   double closedLots = 0;
   int closedCount = 0;
   
   for(int i = 0; i < count; i++)
   {
      if(closedLots >= excessLots) break;
      
      ulong ticket = positions[i].ticket;
      double lots = positions[i].lots;
      
      Print("正在平仓订单 #", ticket, " (", DoubleToString(lots, 2), " 手)...");
      
      if(trade.PositionClose(ticket))
      {
         closedLots += lots;
         closedCount++;
         Print("✓ 订单 #", ticket, " 已平仓");
      }
      else
      {
         Print("✗ 订单 #", ticket, " 平仓失败: ", trade.ResultRetcodeDescription());
      }
   }
   
   Print("自动平仓完成:平掉 ", closedCount, " 单,共 ", DoubleToString(closedLots, 2), " 手");
}

//+------------------------------------------------------------------+
//| 处理移动止盈                                                        |
//+------------------------------------------------------------------+
void ProcessTrailingStop()
{
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double currentSL = PositionGetDouble(POSITION_SL);
      double currentTP = PositionGetDouble(POSITION_TP);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      
      double currentPrice = (type == POSITION_TYPE_BUY) ? 
                            SymbolInfoDouble(_Symbol, SYMBOL_BID) : 
                            SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      
      double newSL = 0;
      bool needUpdate = false;
      
      if(type == POSITION_TYPE_BUY)
      {
         // 多单:当前价 - 移动止盈点数
         newSL = currentPrice - TrailingStopPoints * point;
         
         // 新止损必须高于开仓价且高于当前止损
         if(newSL > openPrice && (currentSL == 0 || newSL > currentSL))
         {
            needUpdate = true;
         }
      }
      else if(type == POSITION_TYPE_SELL)
      {
         // 空单:当前价 + 移动止盈点数
         newSL = currentPrice + TrailingStopPoints * point;
         
         // 新止损必须低于开仓价且低于当前止损(或当前止损为0)
         if(newSL < openPrice && (currentSL == 0 || newSL < currentSL))
         {
            needUpdate = true;
         }
      }
      
      // 执行修改
      if(needUpdate)
      {
         newSL = NormalizeDouble(newSL, digits);
         if(trade.PositionModify(ticket, newSL, currentTP))
         {
            Print("移动止盈:订单 #", ticket, " 止损已更新至 ", DoubleToString(newSL, digits));
         }
      }
   }
}

//+------------------------------------------------------------------+
//| 设置固定止损                                                        |
//+------------------------------------------------------------------+
void SetFixedStopLoss()
{
   if(FixedStopLossPoints <= 0)
   {
      MessageBox("固定止损点数必须大于0", "参数错误", MB_OK | MB_ICONERROR);
      return;
   }
   
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   int updated = 0, failed = 0;
   
   Print("======================================");
   Print("开始设置固定止损 ", FixedStopLossPoints, " 点...");
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double currentTP = PositionGetDouble(POSITION_TP);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      
      double newSL = 0;
      
      if(type == POSITION_TYPE_BUY)
      {
         newSL = openPrice - FixedStopLossPoints * point;
      }
      else if(type == POSITION_TYPE_SELL)
      {
         newSL = openPrice + FixedStopLossPoints * point;
      }
      
      newSL = NormalizeDouble(newSL, digits);
      
      if(trade.PositionModify(ticket, newSL, currentTP))
      {
         updated++;
         Print("✓ 订单 #", ticket, " 止损已设置为 ", DoubleToString(newSL, digits));
      }
      else
      {
         failed++;
         Print("✗ 订单 #", ticket, " 设置失败: ", trade.ResultRetcodeDescription());
      }
   }
   
   string msg = "固定止损设置完成\n\n成功: " + IntegerToString(updated) + " 单";
   if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
   
   Print("固定止损设置完成:成功 ", updated, " 单,失败 ", failed, " 单");
   Print("======================================");
   
   MessageBox(msg, "固定止损结果", MB_OK | MB_ICONINFORMATION);
}

//+------------------------------------------------------------------+
//| 设置挂单价止盈(保本)                                              |
//+------------------------------------------------------------------+
void SetBreakEven()
{
   if(BreakEvenPoints <= 0)
   {
      MessageBox("挂单价止盈点数必须大于0", "参数错误", MB_OK | MB_ICONERROR);
      return;
   }
   
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   int updated = 0, skipped = 0, failed = 0;
   
   Print("======================================");
   Print("开始设置挂单价止盈,触发阈值 ", BreakEvenPoints, " 点...");
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double currentSL = PositionGetDouble(POSITION_SL);
      double currentTP = PositionGetDouble(POSITION_TP);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      
      double currentPrice = (type == POSITION_TYPE_BUY) ? 
                            SymbolInfoDouble(_Symbol, SYMBOL_BID) : 
                            SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      
      double profitPoints = 0;
      bool canSetBreakEven = false;
      
      if(type == POSITION_TYPE_BUY)
      {
         profitPoints = (currentPrice - openPrice) / point;
         // 盈利超过阈值,且当前止损低于开仓价(或没设止损)
         if(profitPoints >= BreakEvenPoints && (currentSL == 0 || currentSL < openPrice))
         {
            canSetBreakEven = true;
         }
      }
      else if(type == POSITION_TYPE_SELL)
      {
         profitPoints = (openPrice - currentPrice) / point;
         // 盈利超过阈值,且当前止损高于开仓价(或没设止损)
         if(profitPoints >= BreakEvenPoints && (currentSL == 0 || currentSL > openPrice))
         {
            canSetBreakEven = true;
         }
      }
      
      if(canSetBreakEven)
      {
         double newSL = NormalizeDouble(openPrice, digits);
         
         if(trade.PositionModify(ticket, newSL, currentTP))
         {
            updated++;
            Print("✓ 订单 #", ticket, " 止损已移至开仓价 ", DoubleToString(newSL, digits), 
                  " (盈利 ", DoubleToString(profitPoints, 1), " 点)");
         }
         else
         {
            failed++;
            Print("✗ 订单 #", ticket, " 设置失败: ", trade.ResultRetcodeDescription());
         }
      }
      else
      {
         skipped++;
         Print("跳过订单 #", ticket, " (盈利 ", DoubleToString(profitPoints, 1), " 点,未达阈值或已设置)");
      }
   }
   
   string msg = "挂单价止盈设置完成\n\n";
   msg += "成功: " + IntegerToString(updated) + " 单\n";
   msg += "跳过: " + IntegerToString(skipped) + " 单";
   if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
   
   Print("挂单价止盈完成:成功 ", updated, " 单,跳过 ", skipped, " 单,失败 ", failed, " 单");
   Print("======================================");
   
   MessageBox(msg, "挂单价止盈结果", MB_OK | MB_ICONINFORMATION);
}

//+------------------------------------------------------------------+
//| 图表事件处理                                                        |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   // 鼠标移动事件 - 处理拖动
   if(id == CHARTEVENT_MOUSE_MOVE)
   {
      int x = (int)lparam;
      int y = (int)dparam;
      int mouseState = (int)sparam;  // 0=无按钮, 1=左键按下
      
      // 正在拖动面板
      if(isDragging && mouseState == 1)
      {
         panelX = x - dragStartX;
         panelY = y - dragStartY;
         UpdatePanelPosition();
      }
      else if(isDragging && mouseState == 0)
      {
         isDragging = false;
      }
      
      // 正在调整大小
      if(isResizing && mouseState == 1)
      {
         panelW = resizeStartW + (x - resizeStartX);
         panelH = resizeStartH + (y - resizeStartY);
         
         // 限制最小尺寸
         if(panelW < 280) panelW = 280;
         if(panelH < 320) panelH = 320;
         
         UpdatePanelSize();
      }
      else if(isResizing && mouseState == 0)
      {
         isResizing = false;
      }
   }
   
   // 对象点击事件
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      // 点击标题栏开始拖动
      if(sparam == panelTitle)
      {
         isDragging = true;
         dragStartX = (int)lparam - panelX;
         dragStartY = (int)dparam - panelY;
         ObjectSetInteger(0, panelTitle, OBJPROP_STATE, false);
         return;
      }
      
      // 点击调整大小手柄
      if(sparam == resizeHandle)
      {
         isResizing = true;
         resizeStartX = (int)lparam;
         resizeStartY = (int)dparam;
         resizeStartW = panelW;
         resizeStartH = panelH;
         ObjectSetInteger(0, resizeHandle, OBJPROP_STATE, false);
         return;
      }
      
      // 一键平仓按钮
      if(sparam == btnCloseAll)
      {
         if(MessageBox("确定要平掉当前品种所有持仓吗?", "一键平仓", MB_YESNO | MB_ICONWARNING) == IDYES)
         {
            CloseAllPositions();
         }
         ObjectSetInteger(0, btnCloseAll, OBJPROP_STATE, false);
      }
      
      // 一键平多按钮
      if(sparam == btnCloseBuy)
      {
         if(MessageBox("确定要平掉所有多头持仓吗?", "一键平多", MB_YESNO | MB_ICONQUESTION) == IDYES)
         {
            ClosePositionsByType(POSITION_TYPE_BUY);
         }
         ObjectSetInteger(0, btnCloseBuy, OBJPROP_STATE, false);
      }
      
      // 一键平空按钮
      if(sparam == btnCloseSell)
      {
         if(MessageBox("确定要平掉所有空头持仓吗?", "一键平空", MB_YESNO | MB_ICONQUESTION) == IDYES)
         {
            ClosePositionsByType(POSITION_TYPE_SELL);
         }
         ObjectSetInteger(0, btnCloseSell, OBJPROP_STATE, false);
      }
      
      // 移动止盈按钮
      if(sparam == btnTrailing)
      {
         trailingStopEnabled = !trailingStopEnabled;
         ObjectSetInteger(0, btnTrailing, OBJPROP_BGCOLOR, trailingStopEnabled ? clrLimeGreen : clrGreen);
         ObjectSetString(0, btnTrailing, OBJPROP_TEXT, trailingStopEnabled ? "移动止盈 ●" : "移动止盈");
         Print("移动止盈 ", trailingStopEnabled ? "已启用" : "已停用");
         ObjectSetInteger(0, btnTrailing, OBJPROP_STATE, false);
      }
      
      // 固定止损按钮
      if(sparam == btnFixedSL)
      {
         if(MessageBox("为所有持仓设置固定止损 " + IntegerToString(FixedStopLossPoints) + " 点?", "固定止损", MB_YESNO | MB_ICONQUESTION) == IDYES)
         {
            SetFixedStopLoss();
         }
         ObjectSetInteger(0, btnFixedSL, OBJPROP_STATE, false);
      }
      
      // 挂单价止盈按钮
      if(sparam == btnBreakEven)
      {
         if(MessageBox("将盈利订单的止损移动到开仓价(保本)?", "挂单价止盈", MB_YESNO | MB_ICONQUESTION) == IDYES)
         {
            SetBreakEven();
         }
         ObjectSetInteger(0, btnBreakEven, OBJPROP_STATE, false);
      }
      
      ChartRedraw();
   }
}

//+------------------------------------------------------------------+
//| 关闭所有持仓                                                        |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
   int closed = 0, failed = 0;
   
   Print("======================================");
   Print("开始执行一键平仓...");
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      double lots = PositionGetDouble(POSITION_VOLUME);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      string typeName = (type == POSITION_TYPE_BUY) ? "多" : "空";
      
      if(trade.PositionClose(ticket))
      {
         closed++;
         Print("✓ 已平仓 ", typeName, "单 #", ticket, " (", DoubleToString(lots, 2), " 手)");
      }
      else
      {
         failed++;
         Print("✗ 平仓失败 ", typeName, "单 #", ticket, " 错误: ", trade.ResultRetcodeDescription());
      }
   }
   
   string msg = "一键平仓完成\n\n成功: " + IntegerToString(closed) + " 单";
   if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
   
   Print("一键平仓完成:成功 ", closed, " 单,失败 ", failed, " 单");
   Print("======================================");
   
   MessageBox(msg, "一键平仓结果", MB_OK | MB_ICONINFORMATION);
   UpdatePanel();
}

//+------------------------------------------------------------------+
//| 按类型关闭持仓                                                      |
//+------------------------------------------------------------------+
void ClosePositionsByType(ENUM_POSITION_TYPE posType)
{
   int closed = 0, failed = 0;
   string typeName = (posType == POSITION_TYPE_BUY) ? "多头" : "空头";
   
   Print("======================================");
   Print("开始执行一键平", typeName, "...");
   
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(type != posType) continue;
      
      double lots = PositionGetDouble(POSITION_VOLUME);
      
      if(trade.PositionClose(ticket))
      {
         closed++;
         Print("✓ 已平", typeName, "订单 #", ticket, " (", DoubleToString(lots, 2), " 手)");
      }
      else
      {
         failed++;
         Print("✗ 平", typeName, "失败 #", ticket, " 错误: ", trade.ResultRetcodeDescription());
      }
   }
   
   string msg = "一键平" + typeName + "完成\n\n成功: " + IntegerToString(closed) + " 单";
   if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
   
   Print("一键平", typeName, "完成:成功 ", closed, " 单,失败 ", failed, " 单");
   Print("======================================");
   
   MessageBox(msg, "一键平" + (posType == POSITION_TYPE_BUY ? "多" : "空") + "结果", MB_OK | MB_ICONINFORMATION);
   UpdatePanel();
}

//+------------------------------------------------------------------+
//| 创建面板                                                            |
//+------------------------------------------------------------------+
void CreatePanel()
{
   // 创建背景矩形
   ObjectCreate(0, panelBg, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, panelBg, OBJPROP_XDISTANCE, panelX);
   ObjectSetInteger(0, panelBg, OBJPROP_YDISTANCE, panelY);
   ObjectSetInteger(0, panelBg, OBJPROP_XSIZE, panelW);
   ObjectSetInteger(0, panelBg, OBJPROP_YSIZE, panelH);
   ObjectSetInteger(0, panelBg, OBJPROP_BGCOLOR, PanelBgColor);
   ObjectSetInteger(0, panelBg, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, panelBg, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, panelBg, OBJPROP_BACK, false);
   ObjectSetInteger(0, panelBg, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, panelBg, OBJPROP_HIDDEN, true);
   
   // 创建标题栏(可点击拖动)
   ObjectCreate(0, panelTitle, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, panelTitle, OBJPROP_XDISTANCE, panelX);
   ObjectSetInteger(0, panelTitle, OBJPROP_YDISTANCE, panelY);
   ObjectSetInteger(0, panelTitle, OBJPROP_XSIZE, panelW);
   ObjectSetInteger(0, panelTitle, OBJPROP_YSIZE, 28);
   ObjectSetString(0, panelTitle, OBJPROP_TEXT, "【" + _Symbol + " 持仓限制控制器】");
   ObjectSetInteger(0, panelTitle, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, panelTitle, OBJPROP_BGCOLOR, TitleBgColor);
   ObjectSetInteger(0, panelTitle, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, panelTitle, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, panelTitle, OBJPROP_FONTSIZE, FontSize);
   ObjectSetString(0, panelTitle, OBJPROP_FONT, "Microsoft YaHei");
   
   // 调整大小手柄(右下角按钮)
   ObjectCreate(0, resizeHandle, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, resizeHandle, OBJPROP_XDISTANCE, panelX + panelW - 18);
   ObjectSetInteger(0, resizeHandle, OBJPROP_YDISTANCE, panelY + panelH - 18);
   ObjectSetInteger(0, resizeHandle, OBJPROP_XSIZE, 18);
   ObjectSetInteger(0, resizeHandle, OBJPROP_YSIZE, 18);
   ObjectSetString(0, resizeHandle, OBJPROP_TEXT, "⤡");
   ObjectSetInteger(0, resizeHandle, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, resizeHandle, OBJPROP_BGCOLOR, clrDimGray);
   ObjectSetInteger(0, resizeHandle, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, resizeHandle, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, resizeHandle, OBJPROP_FONTSIZE, 9);
   
   // 创建数据显示标签 - 每项单独一行
   int startY = panelY + 40;
   int lineHeight = 20;
   
   // 分隔线1
   CreateLabel(labelSeparator1, panelX + 10, startY, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", clrGray, FontSize - 2);
   
   // 多头手数
   CreateLabel(labelBuyLots, panelX + 15, startY + lineHeight, "多头手数: 0.00", BuyColor, FontSize);
   
   // 多头订单数
   CreateLabel(labelBuyCount, panelX + 15, startY + lineHeight * 2, "多头订单: 0 单", BuyColor, FontSize - 1);
   
   // 空头手数
   CreateLabel(labelSellLots, panelX + 15, startY + lineHeight * 3 + 5, "空头手数: 0.00", SellColor, FontSize);
   
   // 空头订单数
   CreateLabel(labelSellCount, panelX + 15, startY + lineHeight * 4 + 5, "空头订单: 0 单", SellColor, FontSize - 1);
   
   // 分隔线2
   CreateLabel(labelSeparator2, panelX + 10, startY + lineHeight * 5 + 10, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", clrGray, FontSize - 2);
   
   // 总手数
   CreateLabel(labelTotalLots, panelX + 15, startY + lineHeight * 6 + 10, "总手数: 0.00", TextColor, FontSize + 1);
   
   // 总盈亏
   CreateLabel(labelTotalProfit, panelX + 15, startY + lineHeight * 7 + 10, "总盈亏: 0.00 USD", ProfitColor, FontSize + 1);
   
   // 分隔线3
   CreateLabel(labelSeparator3, panelX + 10, startY + lineHeight * 8 + 15, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", clrGray, FontSize - 2);
   
   // 手数限制警告
   CreateLabel(labelLimitWarning, panelX + 15, startY + lineHeight * 9 + 15, "手数限制: 无限制", clrLightGray, FontSize);
   
   // 创建平仓按钮(第一行)
   int btnY1 = panelY + panelH - 75;
   int btnWidth = (panelW - 50) / 3;
   
   CreateButton(btnCloseAll, panelX + 10, btnY1, btnWidth, 30, "一键平仓", clrCrimson);
   CreateButton(btnCloseBuy, panelX + 20 + btnWidth, btnY1, btnWidth, 30, "一键平多", clrOrangeRed);
   CreateButton(btnCloseSell, panelX + 30 + btnWidth * 2, btnY1, btnWidth, 30, "一键平空", clrDodgerBlue);
   
   // 创建止盈止损按钮(第二行)
   int btnY2 = panelY + panelH - 38;
   
   CreateButton(btnTrailing, panelX + 10, btnY2, btnWidth, 30, "移动止盈", clrGreen);
   CreateButton(btnFixedSL, panelX + 20 + btnWidth, btnY2, btnWidth, 30, "固定止损", clrDarkOrange);
   CreateButton(btnBreakEven, panelX + 30 + btnWidth * 2, btnY2, btnWidth, 30, "挂单价止盈", clrMediumSeaGreen);
}

//+------------------------------------------------------------------+
//| 更新面板位置                                                        |
//+------------------------------------------------------------------+
void UpdatePanelPosition()
{
   // 更新背景
   ObjectSetInteger(0, panelBg, OBJPROP_XDISTANCE, panelX);
   ObjectSetInteger(0, panelBg, OBJPROP_YDISTANCE, panelY);
   
   // 更新标题栏
   ObjectSetInteger(0, panelTitle, OBJPROP_XDISTANCE, panelX);
   ObjectSetInteger(0, panelTitle, OBJPROP_YDISTANCE, panelY);
   
   // 更新调整大小手柄
   ObjectSetInteger(0, resizeHandle, OBJPROP_XDISTANCE, panelX + panelW - 18);
   ObjectSetInteger(0, resizeHandle, OBJPROP_YDISTANCE, panelY + panelH - 18);
   
   // 更新所有标签位置
   int startY = panelY + 40;
   int lineHeight = 20;
   
   ObjectSetInteger(0, labelSeparator1, OBJPROP_XDISTANCE, panelX + 10);
   ObjectSetInteger(0, labelSeparator1, OBJPROP_YDISTANCE, startY);
   
   ObjectSetInteger(0, labelBuyLots, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelBuyLots, OBJPROP_YDISTANCE, startY + lineHeight);
   
   ObjectSetInteger(0, labelBuyCount, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelBuyCount, OBJPROP_YDISTANCE, startY + lineHeight * 2);
   
   ObjectSetInteger(0, labelSellLots, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelSellLots, OBJPROP_YDISTANCE, startY + lineHeight * 3 + 5);
   
   ObjectSetInteger(0, labelSellCount, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelSellCount, OBJPROP_YDISTANCE, startY + lineHeight * 4 + 5);
   
   ObjectSetInteger(0, labelSeparator2, OBJPROP_XDISTANCE, panelX + 10);
   ObjectSetInteger(0, labelSeparator2, OBJPROP_YDISTANCE, startY + lineHeight * 5 + 10);
   
   ObjectSetInteger(0, labelTotalLots, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelTotalLots, OBJPROP_YDISTANCE, startY + lineHeight * 6 + 10);
   
   ObjectSetInteger(0, labelTotalProfit, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelTotalProfit, OBJPROP_YDISTANCE, startY + lineHeight * 7 + 10);
   
   ObjectSetInteger(0, labelSeparator3, OBJPROP_XDISTANCE, panelX + 10);
   ObjectSetInteger(0, labelSeparator3, OBJPROP_YDISTANCE, startY + lineHeight * 8 + 15);
   
   ObjectSetInteger(0, labelLimitWarning, OBJPROP_XDISTANCE, panelX + 15);
   ObjectSetInteger(0, labelLimitWarning, OBJPROP_YDISTANCE, startY + lineHeight * 9 + 15);
   
   // 更新按钮位置
   UpdateButtonPositions();
   
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| 更新面板大小                                                        |
//+------------------------------------------------------------------+
void UpdatePanelSize()
{
   // 更新背景大小
   ObjectSetInteger(0, panelBg, OBJPROP_XSIZE, panelW);
   ObjectSetInteger(0, panelBg, OBJPROP_YSIZE, panelH);
   
   // 更新标题栏宽度
   ObjectSetInteger(0, panelTitle, OBJPROP_XSIZE, panelW);
   
   // 更新调整大小手柄位置
   ObjectSetInteger(0, resizeHandle, OBJPROP_XDISTANCE, panelX + panelW - 18);
   ObjectSetInteger(0, resizeHandle, OBJPROP_YDISTANCE, panelY + panelH - 18);
   
   // 更新按钮位置和大小
   UpdateButtonPositions();
   
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| 更新按钮位置                                                        |
//+------------------------------------------------------------------+
void UpdateButtonPositions()
{
   int btnY1 = panelY + panelH - 75;
   int btnY2 = panelY + panelH - 38;
   int btnWidth = (panelW - 50) / 3;
   
   // 第一行平仓按钮
   ObjectSetInteger(0, btnCloseAll, OBJPROP_XDISTANCE, panelX + 10);
   ObjectSetInteger(0, btnCloseAll, OBJPROP_YDISTANCE, btnY1);
   ObjectSetInteger(0, btnCloseAll, OBJPROP_XSIZE, btnWidth);
   
   ObjectSetInteger(0, btnCloseBuy, OBJPROP_XDISTANCE, panelX + 20 + btnWidth);
   ObjectSetInteger(0, btnCloseBuy, OBJPROP_YDISTANCE, btnY1);
   ObjectSetInteger(0, btnCloseBuy, OBJPROP_XSIZE, btnWidth);
   
   ObjectSetInteger(0, btnCloseSell, OBJPROP_XDISTANCE, panelX + 30 + btnWidth * 2);
   ObjectSetInteger(0, btnCloseSell, OBJPROP_YDISTANCE, btnY1);
   ObjectSetInteger(0, btnCloseSell, OBJPROP_XSIZE, btnWidth);
   
   // 第二行止盈止损按钮
   ObjectSetInteger(0, btnTrailing, OBJPROP_XDISTANCE, panelX + 10);
   ObjectSetInteger(0, btnTrailing, OBJPROP_YDISTANCE, btnY2);
   ObjectSetInteger(0, btnTrailing, OBJPROP_XSIZE, btnWidth);
   
   ObjectSetInteger(0, btnFixedSL, OBJPROP_XDISTANCE, panelX + 20 + btnWidth);
   ObjectSetInteger(0, btnFixedSL, OBJPROP_YDISTANCE, btnY2);
   ObjectSetInteger(0, btnFixedSL, OBJPROP_XSIZE, btnWidth);
   
   ObjectSetInteger(0, btnBreakEven, OBJPROP_XDISTANCE, panelX + 30 + btnWidth * 2);
   ObjectSetInteger(0, btnBreakEven, OBJPROP_YDISTANCE, btnY2);
   ObjectSetInteger(0, btnBreakEven, OBJPROP_XSIZE, btnWidth);
}

//+------------------------------------------------------------------+
//| 更新面板显示数据                                                    |
//+------------------------------------------------------------------+
void UpdatePanel()
{
   double buyLots = 0, sellLots = 0, totalProfit = 0;
   int buyCount = 0, sellCount = 0;
   
   // 统计所有持仓
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      double lots = PositionGetDouble(POSITION_VOLUME);
      double profit = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      
      if(type == POSITION_TYPE_BUY)
      {
         buyLots += lots;
         buyCount++;
      }
      else if(type == POSITION_TYPE_SELL)
      {
         sellLots += lots;
         sellCount++;
      }
      
      totalProfit += profit;
   }
   
   double totalLots = buyLots + sellLots;
   string currency = AccountInfoString(ACCOUNT_CURRENCY);
   
   // 更新各项数据显示 - 每项单独一行
   ObjectSetString(0, labelBuyLots, OBJPROP_TEXT, "多头手数: " + DoubleToString(buyLots, 2));
   ObjectSetString(0, labelBuyCount, OBJPROP_TEXT, "多头订单: " + IntegerToString(buyCount) + " 单");
   
   ObjectSetString(0, labelSellLots, OBJPROP_TEXT, "空头手数: " + DoubleToString(sellLots, 2));
   ObjectSetString(0, labelSellCount, OBJPROP_TEXT, "空头订单: " + IntegerToString(sellCount) + " 单");
   
   ObjectSetString(0, labelTotalLots, OBJPROP_TEXT, "总手数: " + DoubleToString(totalLots, 2));
   
   // 总盈亏根据正负显示不同颜色
   string profitText = "总盈亏: " + DoubleToString(totalProfit, 2) + " " + currency;
   ObjectSetString(0, labelTotalProfit, OBJPROP_TEXT, profitText);
   
   if(totalProfit > 0)
      ObjectSetInteger(0, labelTotalProfit, OBJPROP_COLOR, clrLime);
   else if(totalProfit < 0)
      ObjectSetInteger(0, labelTotalProfit, OBJPROP_COLOR, clrRed);
   else
      ObjectSetInteger(0, labelTotalProfit, OBJPROP_COLOR, ProfitColor);
   
   // 更新手数限制警告
   if(MaxTotalLots > 0)
   {
      double remainingLots = MaxTotalLots - totalLots;
      
      if(totalLots > MaxTotalLots)
      {
         // 超过限制 - 红色闪烁警告
         string warningText = "⚠⚠ 超限 " + DoubleToString(totalLots - MaxTotalLots, 2) + " 手!";
         if(AutoCloseLimitExcess)
            warningText += " [自动平仓中]";
         ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, warningText);
         ObjectSetInteger(0, labelLimitWarning, OBJPROP_COLOR, clrRed);
      }
      else if(totalLots >= MaxTotalLots)
      {
         // 刚好达到限制 - 红色警告
         string warningText = "⚠ 已达限制 " + DoubleToString(MaxTotalLots, 2) + " 手 - 禁止开仓";
         ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, warningText);
         ObjectSetInteger(0, labelLimitWarning, OBJPROP_COLOR, clrRed);
      }
      else if(totalLots >= MaxTotalLots * 0.8)
      {
         // 接近限制(80%以上)- 黄色警告
         string warningText = "提示: 剩余 " + DoubleToString(remainingLots, 2) + " 手可用";
         ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, warningText);
         ObjectSetInteger(0, labelLimitWarning, OBJPROP_COLOR, clrYellow);
      }
      else
      {
         // 正常状态 - 灰色
         string warningText = "手数限制: " + DoubleToString(MaxTotalLots, 2) + " 手";
         ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, warningText);
         ObjectSetInteger(0, labelLimitWarning, OBJPROP_COLOR, clrLightGray);
      }
   }
   else
   {
      // 无限制
      ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, "手数限制: 无限制");
      ObjectSetInteger(0, labelLimitWarning, OBJPROP_COLOR, clrLightGray);
   }
   
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| 创建文本标签                                                        |
//+------------------------------------------------------------------+
void CreateLabel(string name, int x, int y, string text, color clr, int fontSize)
{
   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
   ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}

//+------------------------------------------------------------------+
//| 创建按钮                                                            |
//+------------------------------------------------------------------+
void CreateButton(string name, int x, int y, int width, int height, string text, color bgColor)
{
   ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bgColor);
   ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, FontSize - 1);
   ObjectSetString(0, name, OBJPROP_FONT, "Microsoft YaHei");
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}

作品说明


使用说明

1. 安装方法

  • 将代码保存为 PositionLimitController.mq5,放入 MQL5/Experts 文件夹
  • 在 MetaEditor 中编译(F7)
  • 在 MT5 图表上拖入该 EA,勾选"允许自动交易"

2. 持仓统计显示(每项单独一行)

面板显示以下信息:

  • 多头手数:所有多头持仓的总手数(绿色)
  • 多头订单:多头订单数量(绿色)
  • 空头手数:所有空头持仓的总手数(红色)
  • 空头订单:空头订单数量(红色)
  • 总手数:多空合计的总手数(白色)
  • 总盈亏:所有持仓的盈亏总和,含隔夜利息(盈利绿色,亏损红色)
  • 手数限制警告:实时显示手数限制状态

3. 总手数限制功能

  • MaxTotalLots:设置最大总手数(例如 10.0 手)
  • AutoCloseLimitExcess:超限是否自动平仓
  • true:自动平掉最新的订单直到符合限制
  • false:仅警告提示
  • 三种状态显示
  • 灰色:正常状态
  • 黄色:已使用 80% 以上手数
  • 红色:已达到或超过限制

4. 平仓按钮(第一行)

  • 一键平仓:关闭当前品种所有持仓(多空都平)
  • 一键平多:只关闭所有多头持仓
  • 一键平空:只关闭所有空头持仓

所有平仓操作都需要二次确认,防止误操作。

5. 止盈止损按钮(第二行)

  • 移动止盈
  • 点击启用/停用移动止盈功能
  • 启用后按钮变亮绿色并显示 ●
  • 自动追踪价格,持续将止损向有利方向移动
  • 移动距离由 TrailingStopPoints 参数控制(默认 300 点)
  • 只会向保护浮盈的方向移动,不会放宽止损
  • 固定止损
  • 一键为所有持仓设置固定止损
  • 距离开仓价 FixedStopLossPoints 点(默认 500 点)
  • 多单:开仓价 - 500 点
  • 空单:开仓价 + 500 点
  • 挂单价止盈
  • 将盈利订单的止损移至开仓价(保本)
  • 只处理盈利超过 BreakEvenPoints 点的订单(默认 200 点)
  • 锁定利润,避免盈利回吐为亏损

6. 参数设置

  • MaxTotalLots:最大总手数限制(0 = 不限制)
  • AutoCloseLimitExcess:超限是否自动平仓
  • TrailingStopPoints:移动止盈点数(0 = 不启用)
  • FixedStopLossPoints:固定止损点数
  • BreakEvenPoints:挂单价止盈触发点数
  • InitPanelX/Y:面板初始位置
  • PanelWidth/Height:面板初始大小
  • RefreshMS:刷新间隔(毫秒)

7. 操作提示

  • 面板可拖动标题栏移动位置
  • 右下角可拖动调整大小
  • 所有操作都有日志记录在 MT5 的"专家"选项卡
  • 移动止盈启用时会在每个 Tick 自动执行
  • 固定止损和挂单价止盈需手动点击执行

8. 注意事项

  • 只统计和操作当前图表品种的持仓
  • 手数限制功能在 OnTrade() 事件中监控,新订单触发时立即检查
  • 移动止盈只会向有利方向移动止损,不会放宽
  • 挂单价止盈只处理达到盈利阈值且未设置保本的订单
  • 所有修改操作都有二次确认对话框,防止误操作

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

这是什么

这是一个为 MT5 设计的持仓管理和风控工具,以面板形式显示当前品种的多空持仓统计(手数、订单数、总盈亏),并能设定总手数上限。当持仓手数超过限制时,EA 自动按开仓时间从新到旧平掉超限订单,防止过度持仓。同时提供一键平仓、移动止盈、固定止损、挂单价止盈等快捷操作,适合需要严格控制风险敞口或同时运行多个策略的交易者。

逻辑原理

持仓统计:每隔 500 毫秒(可调)扫描一次当前品种的所有持仓,分别统计多头手数、空头手数、订单数量和总盈亏(含隔夜利息),在面板上实时显示。

手数限制监控:在每次交易事件(OnTrade)和每秒 Tick 检查时,计算当前总手数。若超过 MaxTotalLots 设定值,根据 AutoCloseLimitExcess 参数决定是仅警告还是自动平仓。自动平仓时,EA 按开仓时间从新到旧排序所有订单,逐个平仓直到手数回到限制以下。

移动止盈:点击按钮启用后,每个 Tick 检查所有持仓,对多单将止损向上移至「当前买价 - TrailingStopPoints 点」,对空单向下移至「当前卖价 + TrailingStopPoints 点」。新止损必须比当前止损更有利且高于(多单)或低于(空单)开仓价,确保只锁定浮盈。

固定止损:点击后一次性为所有持仓设置距离开仓价 FixedStopLossPoints 点的止损(多单在下方,空单在上方)。

挂单价止盈(保本):扫描所有持仓,对盈利超过 BreakEvenPoints 点的订单,将止损移至开仓价,锁定保本。

参数说明

  • MaxTotalLots(默认 10.0):允许的最大总手数。设为 0 表示不限制。
  • AutoCloseLimitExcess(默认 true):超限时是否自动平仓。false 时只在日志中警告。
  • TrailingStopPoints(默认 300):移动止盈的跟踪点数。设为 0 则该功能不可用。
  • FixedStopLossPoints(默认 500):一键固定止损距离开仓价的点数。
  • BreakEvenPoints(默认 200):触发挂单价止盈的最小盈利点数。
  • InitPanelX / InitPanelY(默认 20 / 30):面板初始位置的左上角坐标(像素)。
  • PanelWidth / PanelHeight(默认 340 / 360):面板初始宽度和高度(像素),运行时可拖动右下角调整。
  • PanelBgColor / TitleBgColor / TextColor / BuyColor / SellColor / ProfitColor:面板和文字的颜色设置。
  • FontSize(默认 10):面板文字大小。
  • RefreshMS(默认 500):面板刷新间隔,单位毫秒。

怎么用

将代码保存为 .mq5 文件,放入 MT5 的 MQL5/Experts 文件夹,用 MetaEditor 编译后拖到任意图表上(只操作拖入图表对应的品种),勾选"允许自动交易"。面板会显示在图表左上角,拖动标题栏可移动,拖动右下角手柄可调整大小。

  • 查看统计:面板实时显示当前品种的多空手数、订单数、总盈亏和手数限制状态(灰色正常,黄色接近限制,红色超限)。
  • 一键平仓:点击「一键平仓」「一键平多」「一键平空」按钮,弹窗二次确认后执行。
  • 移动止盈:点击「移动止盈」按钮启用(按钮变亮绿色并显示 ●),EA 自动追踪价格调整止损;再次点击停用。
  • 固定止损:点击后弹窗确认,为所有持仓设置固定点数止损。
  • 挂单价止盈:点击后弹窗确认,对盈利订单将止损移至开仓价。

EA 只统计和操作挂载图表的品种,其他品种不受影响。

适用场景

  • 多策略叠加交易:同时运行多个 EA 或手动加仓时,用总手数限制防止仓位失控。
  • 日内短线或剥头皮:快速开平仓时,用移动止盈和一键平仓提高操作效率。
  • 品种不限:适用于任何 MT5 交易品种,但点数参数需根据品种和周期调整(如外汇、黄金、指数的点值差异大)。
  • 周期不限:EA 不依赖 K 线数据,任意周期图表均可使用。

不适合无法容忍自动平仓的场景(如长线持仓、网格策略希望保留所有订单)。若需手动控制每笔平仓,将 AutoCloseLimitExcess 设为 false。

风险提示

  1. 自动平仓不可逆:启用 AutoCloseLimitExcess 后,超限订单会立即被平掉,无法恢复。建议初次使用时先在模拟盘测试限制值是否合理。
  2. 按时间平仓可能不是最优:代码按开仓时间从新到旧平仓,不考虑盈亏或品种。如果需要按盈亏或其他规则平仓,需修改 CloseExcessPositions 函数。
  3. 移动止盈可能过早平仓TrailingStopPoints 设置过小会导致价格小幅回撤时触发止损,截断盈利。根据品种波动性和交易周期调整点数(波动大的品种如黄金可能需要更大的跟踪距离)。
  4. 点数参数需校准:外汇 5 位报价品种(如 EURUSD)的 1 点是 0.00001,4 位品种(如 USDJPY)是 0.01,黄金可能是 0.01 或 0.1。参数中的"点"是 MT5 的 Point 单位,不同品种含义不同,需根据实际报价调整数值。
  5. 滑点和延迟:一键平仓和自动超限平仓都使用市价单,在快速行情或流动性差的品种上可能产生滑点。
  6. 仅操作当前品种:EA 只统计和平仓挂载图表的品种,不管理账户内其他品种的持仓。如果需要全账户手数限制,需修改代码去掉品种过滤。
  7. 不保证订单一定能平:平仓操作可能因网络延迟、经纪商限制、流动性不足等原因失败,代码会在日志中记录失败订单但不会重试。

常见问题

这个 EA 会自动开仓吗
不会。它只负责监控和管理现有持仓,不会主动开新单。所有开仓由用户手动操作或其他 EA 执行。
手数限制功能会限制其他品种吗
不会。EA 只统计和限制挂载图表对应品种的持仓,账户中其他品种不受影响。如果需要限制全账户手数,需修改代码去掉 `POSITION_SYMBOL != _Symbol` 的过滤。
移动止盈会重绘或取消已设置的止损吗
移动止盈只会向保护浮盈的方向移动止损(多单向上,空单向下),不会放宽或取消止损。如果当前止损已经比新计算的更有利,EA 不会修改。
默认参数适合什么品种和周期
默认值(移动止盈 300 点、固定止损 500 点)以 5 位报价的外汇品种(如 EURUSD)为参考,大致对应 30 / 50 个基点。黄金等波动大的品种需要成倍增加点数,4 位报价品种需要除以 10。周期不限,但日内短线建议缩小点数,长线则放大。
超限自动平仓后能恢复订单吗
不能。平仓操作不可逆,被平掉的订单无法恢复。建议先在模拟账户测试 MaxTotalLots 和 AutoCloseLimitExcess 的组合,确认符合预期再用于实盘。

Position Limit Controller EA for MT5

An EA that displays real-time position statistics, monitors total lot size, automatically closes excess positions when limits are reached, and provides one-click close, trailing stop, fixed stop-loss, and break-even functions for MT5 multi-strategy risk management.

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

评论

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

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