找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 4|回复: 0

[MT5] 手工单自动止损 + ATR追踪止损 + 能量衰竭平仓 + 手工交易操作面板

[复制链接]

269

主题

6

回帖

1001

积分

管理员

积分
1001
楼主
发表于 昨天 17:30 | 只看该作者 |倒序浏览 |阅读模式
//+------------------------------------------------------------------+
//|                    ManualProtection_TradingPanel.mq5             |
//|  手工单自动止损 + ATR追踪止损 + 能量衰竭平仓 + 手工交易操作面板   |
//|  www.115KU.com                                  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026 - QQ Group: 3131288278"
#property link      ""
#property version   "3.22"
#property description "手工交易面板 + 自动止损 + ATR追踪止损 + 能量衰竭保护 + www.115KU.com"
#property strict

#include <Trade\Trade.mqh>

//====================================================================
// 输入参数
//====================================================================
enum ENUM_TRAILING_START_MODE
{
   TRAIL_START_BY_POINTS = 0,   // 达到盈利点数即可启动
   TRAIL_START_BY_TIME   = 1,   // 达到持仓时间即可启动
   TRAIL_START_BY_BOTH   = 2    // 盈利点数和持仓时间都满足才启动
};

input group "══════════ 手工下单面板 ══════════"
input double   InpDefaultLots       = 0.01;   // 默认下单手数
input double   InpQuickLot1         = 0.01;   // 快捷手数1
input double   InpQuickLot2         = 0.02;   // 快捷手数2
input double   InpQuickLot3         = 0.05;   // 快捷手数3
input double   InpQuickLot4         = 0.10;   // 快捷手数4
input double   InpQuickLot5         = 0.20;   // 快捷手数5
input double   InpQuickLot6         = 0.50;   // 快捷手数6
input double   InpQuickLot7         = 1.00;   // 快捷手数7
input bool     InpBuyButtonOnRight  = false;  // 开仓按钮位置:false=开多左/开空右,true=开空左/开多右
input int      InpDeviationPoints   = 30;     // 最大允许滑点(点)
input bool     InpManageManualOnly  = true;   // 管理真正手工单(Magic=0)
input long     InpPanelMagicNumber  = 26081922; // 本面板专属Magic,避免旧EA/其他面板实例误管理
input bool     InpShowPanel         = true;   // 显示交易面板
input double   InpPanelScale        = 0.68;   // 默认缩放(当自定义宽高<=0时使用)
input int      InpPanelX            = 10;     // 面板X坐标
input int      InpPanelY            = 10;     // 面板Y坐标
input int      InpPanelWidth        = 520;    // 面板宽度(px),<=0使用设计比例
input int      InpPanelHeight       = 730;    // 面板高度(px),<=0使用设计比例
input ENUM_BASE_CORNER InpPanelCorner = CORNER_LEFT_UPPER; // 面板停靠角
input bool     InpPanelAutoFit      = true;   // 空间不足时自动缩小(false=严格使用自定义尺寸)

input group "══════════ 信息 ══════════"
input string   InpQQGroupInfo        = "www.115KU.com "; // 本站网址

input group "══════════ 手工单保护 ══════════"
input bool     InpAutoStopManual    = true;   // 新手工单自动设置止损
input int      InpAutoStopPoints    = 200;    // 手工单固定止损距离(统一2位点;3位报价自动换算)
input bool     InpUseTrailing       = true;   // 启用追踪止损
input bool     InpUseATRTrailing    = true;   // ATR动态距离(否则固定点数)
input double   InpTrailingATRMult   = 1.5;    // 追踪止损ATR倍数
input int      InpFixedTrailing     = 350;    // 固定追踪点数(统一2位点;3位报价自动换算)
input int      InpTrailingStep      = 80;     // 最小移动步长(统一2位点)
input int      InpATRPeriod         = 14;     // ATR计算周期
input int      InpTrailingStartProfitPoints = 100; // 追踪启动盈利(统一2位点)
input int      InpTrailingStartMinutes      = 5;   // 追踪启动持仓时间(分钟)
input ENUM_TRAILING_START_MODE InpTrailingStartMode = TRAIL_START_BY_BOTH; // 追踪启动模式
input int      InpTrailingFirstLockPoints   = 20;  // 首次最低锁盈(统一2位点,0=保本)

input group "══════════ 能量衰竭平仓 ══════════"
input bool     InpUseEnergyExit     = true;   // 启用能量衰竭平仓
input int      InpEnergyExitMinMinutes = 5;   // 新仓能量衰竭保护期(分钟),防止刚开仓立即被策略平掉
input int      RegPeriod            = 20;     // 线性回归周期
input double   InpMinSlopeATR       = 0.02;   // 最小标准化斜率
input int      InpATRPercentileWin  = 100;    // ATR百分位窗口
input double   InpChanWidthUp       = 0.9;    // 保留原参数
input double   InpChanWidthDn       = 2.1;    // 保留原参数

//====================================================================
// 全局变量
//====================================================================
CTrade m_tradeManual;
int    atrHandle = INVALID_HANDLE;

string PANEL_PREFIX = "MTP_";
double g_panelLots   = 0.01;
bool   g_panelVisible = true;

color  CLR_BG        = C'13,17,23';
color  CLR_BG2       = C'20,25,32';
color  CLR_TEXT      = C'242,244,247';
color  CLR_SUB       = C'190,196,205';
color  CLR_GREEN     = C'25,190,80';
color  CLR_RED       = C'235,48,55';
color  CLR_BLUE      = C'35,100,205';
color  CLR_ORANGE    = C'245,170,0';
color  CLR_YELLOW    = C'245,185,0';
color  CLR_BUTTON    = C'43,49,59';

string OBJ_BG        = PANEL_PREFIX + "BG";
string OBJ_TITLE     = PANEL_PREFIX + "TITLE";
string OBJ_SYMBOL    = PANEL_PREFIX + "SYMBOL";
string OBJ_LOTS_LBL  = PANEL_PREFIX + "LOTSLBL";
string OBJ_LOTS_EDIT = PANEL_PREFIX + "LOTSEDIT";

string BTN_BUY       = PANEL_PREFIX + "BUY";
string BTN_SELL      = PANEL_PREFIX + "SELL";
string BTN_CLOSE_BUY = PANEL_PREFIX + "CLOSEBUY";
string BTN_CLOSE_SELL= PANEL_PREFIX + "CLOSESELL";
string BTN_CLOSE_PROFIT = PANEL_PREFIX + "CLOSEPROFIT";
string BTN_CLOSE_LOSS   = PANEL_PREFIX + "CLOSELOSS";
string BTN_CLOSE25   = PANEL_PREFIX + "CLOSE25";
string BTN_CLOSE50   = PANEL_PREFIX + "CLOSE50";
string BTN_CLOSE75   = PANEL_PREFIX + "CLOSE75";
string BTN_CLOSEALL  = PANEL_PREFIX + "CLOSEALL";
string BTN_BREAKEVEN = PANEL_PREFIX + "BREAKEVEN";
string BTN_APPLY_SL  = PANEL_PREFIX + "APPLYSL";
string BTN_APPLY_TP  = PANEL_PREFIX + "APPLYTP";
string BTN_LOT_MINUS  = PANEL_PREFIX + "LOTMINUS";
string BTN_LOT_PLUS   = PANEL_PREFIX + "LOTPLUS";
string BTN_LOT_001    = PANEL_PREFIX + "LOT001";
string BTN_LOT_002    = PANEL_PREFIX + "LOT002";
string BTN_LOT_005    = PANEL_PREFIX + "LOT005";
string BTN_LOT_010    = PANEL_PREFIX + "LOT010";
string BTN_LOT_020    = PANEL_PREFIX + "LOT020";
string BTN_LOT_050    = PANEL_PREFIX + "LOT050";
string BTN_LOT_100    = PANEL_PREFIX + "LOT100";
string BTN_SETTINGS   = PANEL_PREFIX + "SETTINGS";
string BTN_PANEL_CLOSE= PANEL_PREFIX + "PANELCLOSE";
string BTN_PANEL_SHOW = "MTP_SHOW_PANEL";
string OBJ_QQ_PROMO   = "MTP_QQ_PROMO";

string CARD_BUY      = PANEL_PREFIX + "CARD_BUY";
string CARD_SELL     = PANEL_PREFIX + "CARD_SELL";
string CARD_NET      = PANEL_PREFIX + "CARD_NET";
string CARD_ACCOUNT  = PANEL_PREFIX + "CARD_ACCOUNT";
string CARD_TODAY    = PANEL_PREFIX + "CARD_TODAY";
string CARD_STATS    = PANEL_PREFIX + "CARD_STATS";
string CARD_PROTECT  = PANEL_PREFIX + "CARD_PROTECT";
string CARD_ACTIONS  = PANEL_PREFIX + "CARD_ACTIONS";

string LBL_BUY_TITLE = PANEL_PREFIX + "BUY_TITLE";
string LBL_SELL_TITLE= PANEL_PREFIX + "SELL_TITLE";
string LBL_NET_TITLE = PANEL_PREFIX + "NET_TITLE";
string LBL_BUY_LOTS  = PANEL_PREFIX + "BUY_LOTS";
string LBL_BUY_COUNT = PANEL_PREFIX + "BUY_COUNT";
string LBL_BUY_AVG   = PANEL_PREFIX + "BUY_AVG";
string LBL_BUY_PNL   = PANEL_PREFIX + "BUY_PNL";
string LBL_SELL_LOTS = PANEL_PREFIX + "SELL_LOTS";
string LBL_SELL_COUNT= PANEL_PREFIX + "SELL_COUNT";
string LBL_SELL_AVG  = PANEL_PREFIX + "SELL_AVG";
string LBL_SELL_PNL  = PANEL_PREFIX + "SELL_PNL";
string LBL_NET_LOTS  = PANEL_PREFIX + "NET_LOTS";
string LBL_NET_PNL   = PANEL_PREFIX + "NET_PNL";

string LBL_ACC_BAL   = PANEL_PREFIX + "ACC_BAL";
string LBL_ACC_EQ    = PANEL_PREFIX + "ACC_EQ";
string LBL_ACC_MARGIN= PANEL_PREFIX + "ACC_MARGIN";
string LBL_ACC_FREE  = PANEL_PREFIX + "ACC_FREE";
string LBL_ACC_LEVEL = PANEL_PREFIX + "ACC_LEVEL";

string LBL_TD_REAL   = PANEL_PREFIX + "TD_REAL";
string LBL_TD_FLOAT  = PANEL_PREFIX + "TD_FLOAT";
string LBL_TD_TOTAL  = PANEL_PREFIX + "TD_TOTAL";
string LBL_TD_COUNT  = PANEL_PREFIX + "TD_COUNT";
string LBL_TD_RATE   = PANEL_PREFIX + "TD_RATE";

string LBL_ST_COUNT  = PANEL_PREFIX + "ST_COUNT";
string LBL_ST_WIN    = PANEL_PREFIX + "ST_WIN";
string LBL_ST_MAXWIN = PANEL_PREFIX + "ST_MAXWIN";
string LBL_ST_MAXLOSS= PANEL_PREFIX + "ST_MAXLOSS";

string LBL_PROT_STOP = PANEL_PREFIX + "PROT_STOP";
string LBL_PROT_TRAIL= PANEL_PREFIX + "PROT_TRAIL";
string LBL_PROT_ATR  = PANEL_PREFIX + "PROT_ATR";
string LBL_PROT_ENERGY=PANEL_PREFIX + "PROT_ENERGY";
string LBL_PROT_VOL  = PANEL_PREFIX + "PROT_VOL";
string LBL_PROT_SLOPE= PANEL_PREFIX + "PROT_SLOPE";

string LBL_ATR_INFO  = PANEL_PREFIX + "ATRINFO";

string LBL_LOT_QUICK = PANEL_PREFIX + "LOTQUICK";
string LBL_POS       = PANEL_PREFIX + "POS";
string LBL_PNL       = PANEL_PREFIX + "PNL";
string LBL_ACCOUNT   = PANEL_PREFIX + "ACCOUNT";
string LBL_TODAY     = PANEL_PREFIX + "TODAY";
string LBL_PROTECT   = PANEL_PREFIX + "PROTECT";
string LBL_FOOTER    = PANEL_PREFIX + "FOOTER";
string LBL_STATUS    = PANEL_PREFIX + "STATUS";

// 面板统计分行显示,避免OBJ_LABEL不自动换行导致文字溢出
string LBL_POS_H    = PANEL_PREFIX + "POS_H";
string LBL_POS_1    = PANEL_PREFIX + "POS_1";
string LBL_POS_2    = PANEL_PREFIX + "POS_2";
string LBL_POS_3    = PANEL_PREFIX + "POS_3";
string LBL_PNL_H    = PANEL_PREFIX + "PNL_H";
string LBL_PNL_1    = PANEL_PREFIX + "PNL_1";
string LBL_PNL_2    = PANEL_PREFIX + "PNL_2";
string LBL_ACCOUNT_H = PANEL_PREFIX + "ACCOUNT_H";
string LBL_ACCOUNT_1 = PANEL_PREFIX + "ACCOUNT_1";
string LBL_ACCOUNT_2 = PANEL_PREFIX + "ACCOUNT_2";
string LBL_ACCOUNT_3 = PANEL_PREFIX + "ACCOUNT_3";
string LBL_TODAY_H  = PANEL_PREFIX + "TODAY_H";
string LBL_TODAY_1  = PANEL_PREFIX + "TODAY_1";
string LBL_TODAY_2  = PANEL_PREFIX + "TODAY_2";
string LBL_TODAY_3  = PANEL_PREFIX + "TODAY_3";
string LBL_PROTECT_H = PANEL_PREFIX + "PROTECT_H";
string LBL_PROTECT_1 = PANEL_PREFIX + "PROTECT_1";
string LBL_PROTECT_2 = PANEL_PREFIX + "PROTECT_2";
string LBL_PROTECT_3 = PANEL_PREFIX + "PROTECT_3";
string LBL_PROTECT_4 = PANEL_PREFIX + "PROTECT_4";

// 面板设计基准尺寸;实际显示支持自定义宽/高、坐标和停靠角。
// 自定义宽高会按X/Y分别缩放整个UI,保证布局始终完整。
const int BASE_PANEL_W = 760;
const int BASE_PANEL_H = 1065;
int PANEL_X = 10;
int PANEL_Y = 10;
int PANEL_ORIGIN_X = 10;
int PANEL_ORIGIN_Y = 10;
int PANEL_W = 520;
int PANEL_H = 730;
double PANEL_SCALE_X = 0.68;
double PANEL_SCALE_Y = 0.68;
double PANEL_FONT_SCALE = 0.68;

int UIX(int v)
{
   return (int)MathRound((double)v * PANEL_SCALE_X);
}

int UIY(int v)
{
   return (int)MathRound((double)v * PANEL_SCALE_Y);
}

int UIFont(int v)
{
   return (int)MathRound((double)v * PANEL_FONT_SCALE);
}

void CalculatePanelScale()
{
   int reqW = InpPanelWidth;
   int reqH = InpPanelHeight;

   double desired = InpPanelScale;
   if(desired < 0.40) desired = 0.40;
   if(desired > 1.50) desired = 1.50;

   // <=0 时退回原来的等比例缩放逻辑。
   if(reqW <= 0) reqW = (int)MathRound(BASE_PANEL_W * desired);
   if(reqH <= 0) reqH = (int)MathRound(BASE_PANEL_H * desired);

   reqW = MathMax(260, reqW);
   reqH = MathMax(360, reqH);

   PANEL_X = MathMax(0, InpPanelX);
   PANEL_Y = MathMax(0, InpPanelY);
   PANEL_W = reqW;
   PANEL_H = reqH;

   long chartW = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
   long chartH = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);

   // 根据停靠角判断剩余空间。X/Y是距离对应角的偏移。
   long availW = chartW - PANEL_X - 8;
   long availH = chartH - PANEL_Y - 8;
   if(availW < 1) availW = 1;
   if(availH < 1) availH = 1;

   if(InpPanelAutoFit)
   {
      PANEL_W = (int)MathMin(PANEL_W, availW);
      PANEL_H = (int)MathMin(PANEL_H, availH);
      PANEL_W = MathMax(260, PANEL_W);
      PANEL_H = MathMax(360, PANEL_H);
   }

   PANEL_SCALE_X = (double)PANEL_W / BASE_PANEL_W;
   PANEL_SCALE_Y = (double)PANEL_H / BASE_PANEL_H;
   PANEL_FONT_SCALE = MathMin(PANEL_SCALE_X, PANEL_SCALE_Y);
   PANEL_FONT_SCALE = MathMax(0.55, MathMin(1.20, PANEL_FONT_SCALE));

   // 将“停靠角 + 坐标”转换为面板左上角实际屏幕坐标。
   // 所有子控件统一使用左上角坐标系,避免右侧/底部停靠时布局镜像。
   long cw = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
   long ch = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);
   PANEL_ORIGIN_X = PANEL_X;
   PANEL_ORIGIN_Y = PANEL_Y;

   if(InpPanelCorner == CORNER_RIGHT_UPPER || InpPanelCorner == CORNER_RIGHT_LOWER)
      PANEL_ORIGIN_X = (int)MathMax(0, cw - PANEL_X - PANEL_W);
   if(InpPanelCorner == CORNER_LEFT_LOWER || InpPanelCorner == CORNER_RIGHT_LOWER)
      PANEL_ORIGIN_Y = (int)MathMax(0, ch - PANEL_Y - PANEL_H);
}

//====================================================================
// 工具函数
//====================================================================
string Money(double value)
{
   return DoubleToString(value, 2);
}

string Price(double value)
{
   return DoubleToString(value, _Digits);
}

double NormalizeVolume(double volume)
{
   double vmin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double vmax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   if(vmin <= 0.0)  vmin = 0.01;
   if(vmax <= 0.0)  vmax = 100.0;
   if(vstep <= 0.0) vstep = vmin;

   volume = MathMax(vmin, MathMin(vmax, volume));
   volume = MathFloor(volume / vstep + 1e-9) * vstep;

   int digits = 0;
   double step = vstep;
   while(digits < 8 && MathAbs(step - MathRound(step)) > 1e-9)
   {
      step *= 10.0;
      digits++;
   }

   return NormalizeDouble(MathMax(vmin, volume), digits);
}

double ReadPanelLots()
{
   if(ObjectFind(0, OBJ_LOTS_EDIT) < 0)
      return NormalizeVolume(InpDefaultLots);

   string text = ObjectGetString(0, OBJ_LOTS_EDIT, OBJPROP_TEXT);
   double lots = StringToDouble(text);
   if(lots <= 0.0)
      lots = InpDefaultLots;

   return NormalizeVolume(lots);
}

//====================================================================
// 参数点值换算
// 用户输入的“点”统一按2位小数报价理解:
// 2位报价:1点 = 0.01
// 3位报价:1点仍 = 0.01(即10个交易商原生_Point)
// 其它位数保持交易商原生_Point,避免改变既有品种行为。
//====================================================================
double UserPointSize()
{
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double nativePoint = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   if(nativePoint <= 0.0)
      nativePoint = _Point;

   if(digits == 3)
      return nativePoint * 10.0;

   return nativePoint;
}

bool IsManagedPosition()
{
   if(PositionGetString(POSITION_SYMBOL) != _Symbol)
      return false;

   long magic = PositionGetInteger(POSITION_MAGIC);

   // v2.22:本面板下单使用专属Magic,避免旧版本/其他Magic=0 EA误管理。
   // 同时可继续保护真正由终端手工开的 Magic=0 仓位。
   if(magic == InpPanelMagicNumber)
      return true;

   if(InpManageManualOnly && magic == 0)
      return true;

   return false;
}

int ManagedPositionCount()
{
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;
      if(IsManagedPosition())
         count++;
   }
   return count;
}

double ManagedFloatingProfit()
{
   double result = 0.0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;
      if(!IsManagedPosition())
         continue;

      result += PositionGetDouble(POSITION_PROFIT)
             + PositionGetDouble(POSITION_SWAP);
   }
   return result;
}

void GetPositionStats(double &buyLots, double &sellLots,
                      double &buyAvg, double &sellAvg,
                      double &buyProfit, double &sellProfit,
                      int &buyCount, int &sellCount)
{
   buyLots = sellLots = 0.0;
   buyAvg = sellAvg = 0.0;
   buyProfit = sellProfit = 0.0;
   buyCount = sellCount = 0;

   double buyValue = 0.0;
   double sellValue = 0.0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;
      if(!IsManagedPosition())
         continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double volume = PositionGetDouble(POSITION_VOLUME);
      double open = PositionGetDouble(POSITION_PRICE_OPEN);
      double profit = PositionGetDouble(POSITION_PROFIT)
                    + PositionGetDouble(POSITION_SWAP);

      if(type == POSITION_TYPE_BUY)
      {
         buyLots += volume;
         buyValue += volume * open;
         buyProfit += profit;
         buyCount++;
      }
      else if(type == POSITION_TYPE_SELL)
      {
         sellLots += volume;
         sellValue += volume * open;
         sellProfit += profit;
         sellCount++;
      }
   }

   if(buyLots > 0.0)  buyAvg = buyValue / buyLots;
   if(sellLots > 0.0) sellAvg = sellValue / sellLots;
}

double TodayRealizedProfit(int &closedDeals, int &winningDeals, int &losingDeals,
                           double &maxWin, double &maxLoss)
{
   closedDeals = 0;
   winningDeals = 0;
   losingDeals = 0;
   maxWin = 0.0;
   maxLoss = 0.0;

   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   dt.hour = 0;
   dt.min  = 0;
   dt.sec  = 0;

   datetime dayStart = StructToTime(dt);
   datetime now = TimeCurrent();

   if(!HistorySelect(dayStart, now))
      return 0.0;

   double total = 0.0;
   int deals = HistoryDealsTotal();

   for(int i = 0; i < deals; i++)
   {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0)
         continue;

      string symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
      if(symbol != _Symbol)
         continue;

      if(InpManageManualOnly)
      {
         long magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
         if(magic != 0)
            continue;
      }

      long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
      if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_OUT_BY)
         continue;

      double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
                    + HistoryDealGetDouble(ticket, DEAL_SWAP)
                    + HistoryDealGetDouble(ticket, DEAL_COMMISSION);

      total += profit;
      closedDeals++;

      if(profit > 0.0)
      {
         winningDeals++;
         if(profit > maxWin) maxWin = profit;
      }
      else if(profit < 0.0)
      {
         losingDeals++;
         if(profit < maxLoss) maxLoss = profit;
      }
   }

   return total;
}

//====================================================================
// 线性回归
//====================================================================
bool CalcLinearRegression(const double &y[], int period, int startIdx,
                          double &a, double &b)
{
   if(period <= 0 || startIdx < 0)
   {
      a = 0.0;
      b = 0.0;
      return false;
   }

   double sumX = 0.0, sumY = 0.0, sumXY = 0.0, sumX2 = 0.0;

   for(int i = 0; i < period; i++)
   {
      int idx = startIdx + i;
      double x = i;
      double v = y[idx];

      sumX  += x;
      sumY  += v;
      sumXY += x * v;
      sumX2 += x * x;
   }

   double n = (double)period;
   double denom = n * sumX2 - sumX * sumX;

   if(MathAbs(denom) < 1e-10)
   {
      a = 0.0;
      b = sumY / n;
   }
   else
   {
      a = (n * sumXY - sumX * sumY) / denom;
      b = (sumY - a * sumX) / n;
   }

   return true;
}

//====================================================================
// ATR 百分位
//====================================================================
double CalcATRPercentile(int lookback)
{
   if(lookback < 5)
      return 50.0;

   double atr[];
   ArraySetAsSeries(atr, true);

   if(CopyBuffer(atrHandle, 0, 0, lookback, atr) < lookback)
      return 50.0;

   double current = atr[0];
   int lessCnt = 0;

   for(int i = 1; i < lookback; i++)
      if(atr < current)
         lessCnt++;

   return 100.0 * (double)lessCnt / (double)(lookback - 1);
}

enum ENUM_VOL_REGIME
{
   VOL_LOW = 0,
   VOL_NORMAL = 1,
   VOL_HIGH = 2
};

ENUM_VOL_REGIME GetVolRegime()
{
   double pct = CalcATRPercentile(InpATRPercentileWin);

   if(pct < 25.0) return VOL_LOW;
   if(pct > 75.0) return VOL_HIGH;
   return VOL_NORMAL;
}

string VolRegimeText()
{
   ENUM_VOL_REGIME regime = GetVolRegime();

   if(regime == VOL_LOW)    return "LOW";
   if(regime == VOL_HIGH)   return "HIGH";
   return "NORMAL";
}

bool CalcChannel(double &normSlope)
{
   int need = MathMax(RegPeriod + 5, InpATRPercentileWin + 5);

   double close[], atr[];
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(atr, true);

   if(CopyClose(_Symbol, _Period, 0, need, close) < need)
      return false;

   if(CopyBuffer(atrHandle, 0, 0, need, atr) < need)
      return false;

   double slope, intercept;
   if(!CalcLinearRegression(close, RegPeriod, 0, slope, intercept))
      return false;

   normSlope = (atr[0] > 0.0) ? slope / atr[0] : 0.0;
   return true;
}

//====================================================================
// 止损安全检查 / 面板下单后的净持仓SL重校验
//====================================================================
bool IsStopPriceValidForCurrentMarket(const long type, const double slPrice)
{
   if(slPrice <= 0.0)
      return false;

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   long stopsLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double brokerMinDist = MathMax(0, stopsLevelPoints) * _Point;

   if(type == POSITION_TYPE_BUY)
      return (slPrice < bid - brokerMinDist);

   return (slPrice > ask + brokerMinDist);
}

// 在净持仓账户中,再次加仓后旧SL会继续保留。
// 这里根据成交对应的 POSITION_IDENTIFIER,只重算刚刚被本面板改变的那一个持仓,
// 避免旧SL因平均开仓价变化而突然贴得很近。
void RebaseStopAfterPanelDeal()
{
   if(!InpAutoStopManual)
      return;

   ulong deal = m_tradeManual.ResultDeal();
   if(deal == 0)
      return;
   if(!HistoryDealSelect(deal))
      return;

   long positionId = (long)HistoryDealGetInteger(deal, DEAL_POSITION_ID);
   if(positionId <= 0)
      return;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;
      if(!IsManagedPosition())
         continue;
      if((long)PositionGetInteger(POSITION_IDENTIFIER) != positionId)
         continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double tp = PositionGetDouble(POSITION_TP);
      double slDist = MathMax(0, InpAutoStopPoints) * UserPointSize();
      if(slDist <= 0.0)
         return;

      double desiredSl = (type == POSITION_TYPE_BUY)
                       ? openPrice - slDist
                       : openPrice + slDist;
      desiredSl = NormalizeDouble(desiredSl, _Digits);

      if(!IsStopPriceValidForCurrentMarket(type, desiredSl))
      {
         PrintFormat("面板下单后SL重校验暂缓(持仓 %I64u): 目标SL %.5f 距当前价过近/无效", ticket, desiredSl);
         return;
      }

      double oldSl = PositionGetDouble(POSITION_SL);
      // 无SL,或净持仓加减仓后旧SL偏离新的固定距离时,统一重设。
      if(oldSl == 0.0 || MathAbs(oldSl - desiredSl) > UserPointSize() * 0.5)
      {
         if(m_tradeManual.PositionModify(ticket, desiredSl, tp))
            PrintFormat("面板下单后SL重校验(持仓 %I64u): %.5f -> %.5f", ticket, oldSl, desiredSl);
         else
            PrintFormat("面板下单后SL重校验失败(持仓 %I64u): %s", ticket, m_tradeManual.ResultRetcodeDescription());
      }
      return;
   }
}

//====================================================================
// 自动止损
//====================================================================
void AutoSetStopForManualPositions()
{
   if(!InpAutoStopManual)
      return;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      double sl = PositionGetDouble(POSITION_SL);
      if(sl != 0.0)
         continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double tp = PositionGetDouble(POSITION_TP);
      double slDist = InpAutoStopPoints * UserPointSize();

      double newSl = (type == POSITION_TYPE_BUY)
                   ? NormalizeDouble(openPrice - slDist, _Digits)
                   : NormalizeDouble(openPrice + slDist, _Digits);

      // 不发送贴近当前价格或落在经纪商禁区内的SL。
      if(!IsStopPriceValidForCurrentMarket(type, newSl))
         continue;

      if(m_tradeManual.PositionModify(ticket, newSl, tp))
      {
         PrintFormat("手工单 %I64u 自动设置止损 %.5f", ticket, newSl);
      }
   }
}

//====================================================================
// 追踪启动条件
//====================================================================
bool TrailingStartConditionMet(const long type,
                               const double openPrice,
                               const double bid,
                               const double ask,
                               const datetime openTime)
{
   double profitPoints = (type == POSITION_TYPE_BUY)
                         ? (bid - openPrice) / UserPointSize()
                         : (openPrice - ask) / UserPointSize();

   int heldSeconds = (int)MathMax(0, (long)(TimeCurrent() - openTime));
   bool pointsOk = (profitPoints >= MathMax(0, InpTrailingStartProfitPoints));
   bool timeOk   = (heldSeconds >= MathMax(0, InpTrailingStartMinutes) * 60);

   if(InpTrailingStartMode == TRAIL_START_BY_POINTS)
      return pointsOk;
   if(InpTrailingStartMode == TRAIL_START_BY_TIME)
      return timeOk;

   return (pointsOk && timeOk);
}

//====================================================================
// ATR 追踪止损
// 设计规则:
// 1) 未启动前,不因为刚刚盈利就立即推到保本;
// 2) 启动条件可选:盈利点数 / 持仓时间 / 两者都满足;
// 3) 首次接管后至少锁定 InpTrailingFirstLockPoints 点利润;
// 4) 多单SL不低于开仓价,空单SL不高于开仓价;
// 5) SL只向盈利方向移动,绝不倒退。
//====================================================================
void TrailingStop()
{
   if(!InpUseTrailing)
      return;

   double atr[1];

   if(InpUseATRTrailing)
   {
      if(CopyBuffer(atrHandle, 0, 0, 1, atr) != 1)
         return;
   }

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      double trailingDist = InpUseATRTrailing
                           ? InpTrailingATRMult * atr[0]
                           : InpFixedTrailing * UserPointSize();

      double minStep = MathMax(0, InpTrailingStep) * UserPointSize();
      double lockDist = MathMax(0, InpTrailingFirstLockPoints) * UserPointSize();

      double sl = PositionGetDouble(POSITION_SL);
      double tp = PositionGetDouble(POSITION_TP);
      long type = PositionGetInteger(POSITION_TYPE);

      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);

      // 用“SL已经进入锁盈/保本区”作为追踪已启动的持久标记。
      // 这样启动后,即使价格回撤到启动点数以下,追踪状态也不会被取消。
      bool alreadyStarted = false;
      if(sl > 0.0)
      {
         if(type == POSITION_TYPE_BUY)
            alreadyStarted = (sl >= openPrice + lockDist - UserPointSize() * 0.1);
         else
            alreadyStarted = (sl <= openPrice - lockDist + UserPointSize() * 0.1);
      }

      if(!alreadyStarted && !TrailingStartConditionMet(type, openPrice, bid, ask, openTime))
         continue;

      // 经纪商最小止损距离。若当前利润不足以同时满足“锁盈距离”和最小StopLevel,
      // 就继续等待,不把SL放到亏损区,也不强行发送无效修改请求。
      long stopsLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
      double brokerMinDist = MathMax(0, stopsLevelPoints) * _Point;

      if(type == POSITION_TYPE_BUY)
      {
         double minProtectedSl = openPrice + lockDist;
         double maxAllowedSl = bid - brokerMinDist;

         if(maxAllowedSl < minProtectedSl)
            continue;

         double newSl = MathMax(bid - trailingDist, minProtectedSl);
         newSl = MathMin(newSl, maxAllowedSl);
         newSl = NormalizeDouble(newSl, _Digits);

         // 当前SL仍在锁盈线下方时,首次启动优先抬到锁盈线,不受最小移动步长限制。
         bool needInitialLock = (sl == 0.0 || sl < minProtectedSl);
         bool canMove = (needInitialLock || newSl > sl + minStep);

         if(canMove && newSl >= minProtectedSl && newSl < bid)
         {
            if(m_tradeManual.PositionModify(ticket, newSl, tp))
               PrintFormat("追踪止损上调(持仓 %I64u): %.5f", ticket, newSl);
         }
      }
      else
      {
         double maxProtectedSl = openPrice - lockDist;
         double minAllowedSl = ask + brokerMinDist;

         if(minAllowedSl > maxProtectedSl)
            continue;

         double newSl = MathMin(ask + trailingDist, maxProtectedSl);
         newSl = MathMax(newSl, minAllowedSl);
         newSl = NormalizeDouble(newSl, _Digits);

         // 当前SL仍在锁盈线上方时,首次启动优先压到锁盈线,不受最小移动步长限制。
         bool needInitialLock = (sl == 0.0 || sl > maxProtectedSl);
         bool canMove = (needInitialLock || newSl < sl - minStep);

         if(canMove && newSl <= maxProtectedSl && newSl > ask)
         {
            if(m_tradeManual.PositionModify(ticket, newSl, tp))
               PrintFormat("追踪止损下调(持仓 %I64u): %.5f", ticket, newSl);
         }
      }
   }
}

//====================================================================
// 能量衰竭平仓
//====================================================================
void CheckEnergyExhaustion()
{
   if(!InpUseEnergyExit)
      return;

   if(ManagedPositionCount() == 0)
      return;

   double normSlope;
   if(!CalcChannel(normSlope))
      return;

   double slopeAbs = MathAbs(normSlope);
   ENUM_VOL_REGIME regime = GetVolRegime();

   bool exit = false;

   switch(regime)
   {
      case VOL_LOW:
         if(slopeAbs < InpMinSlopeATR * 0.5) exit = true;
         break;

      case VOL_NORMAL:
         if(slopeAbs < InpMinSlopeATR * 0.3) exit = true;
         break;

      case VOL_HIGH:
         if(slopeAbs < InpMinSlopeATR * 0.2) exit = true;
         break;
   }

   if(!exit)
      return;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      // 新仓保护期:能量衰竭逻辑不得在刚开仓后立即平仓。
      datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
      int heldSeconds = (int)MathMax(0, (long)(TimeCurrent() - openTime));
      if(heldSeconds < MathMax(0, InpEnergyExitMinMinutes) * 60)
         continue;

      double profit = PositionGetDouble(POSITION_PROFIT);

      if(m_tradeManual.PositionClose(ticket))
      {
         PrintFormat("能量衰竭平仓(持仓 %I64u): 盈亏=%.2f 波动=%s 斜率=%.4f",
                     ticket, profit, VolRegimeText(), normSlope);
      }
   }
}

//====================================================================
// 面板对象创建
//====================================================================
void CreateLabel(string name, string text, int x, int y, int size=10,
                 color clr=clrWhite, ENUM_ANCHOR_POINT anchor=ANCHOR_LEFT_UPPER)
{
   if(ObjectFind(0, name) >= 0)
      ObjectDelete(0, name);

   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_ORIGIN_X + UIX(x));
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_ORIGIN_Y + UIY(y));
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, MathMax(8, UIFont(size)));
   ObjectSetString(0, name, OBJPROP_FONT, "Arial");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 10);
}

void CreateButton(string name, string text, int x, int y, int w, int h,
                  color bg=C'43,49,59', color fg=clrWhite, int fontSize=11)
{
   if(ObjectFind(0, name) >= 0)
      ObjectDelete(0, name);

   ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_ORIGIN_X + UIX(x));
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_ORIGIN_Y + UIY(y));
   ObjectSetInteger(0, name, OBJPROP_XSIZE, UIX(w));
   ObjectSetInteger(0, name, OBJPROP_YSIZE, UIY(h));
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
   ObjectSetInteger(0, name, OBJPROP_COLOR, fg);
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, C'100,108,120');
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, MathMax(8, UIFont(fontSize)));
   ObjectSetString(0, name, OBJPROP_FONT, "Arial");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_ALIGN, ALIGN_CENTER);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 20);
}

void CreateEdit(string name, string text, int x, int y, int w, int h)
{
   if(ObjectFind(0, name) >= 0)
      ObjectDelete(0, name);

   ObjectCreate(0, name, OBJ_EDIT, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_ORIGIN_X + UIX(x));
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_ORIGIN_Y + UIY(y));
   ObjectSetInteger(0, name, OBJPROP_XSIZE, UIX(w));
   ObjectSetInteger(0, name, OBJPROP_YSIZE, UIY(h));
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, C'25,30,38');
   ObjectSetInteger(0, name, OBJPROP_COLOR, CLR_TEXT);
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, C'90,98,112');
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, MathMax(10, UIFont(18)));
   ObjectSetString(0, name, OBJPROP_FONT, "Arial");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_ALIGN, ALIGN_CENTER);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, true);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 20);
}

void CreatePanelBackground()
{
   if(ObjectFind(0, OBJ_BG) >= 0)
      ObjectDelete(0, OBJ_BG);

   ObjectCreate(0, OBJ_BG, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_XDISTANCE, PANEL_ORIGIN_X);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_YDISTANCE, PANEL_ORIGIN_Y);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_XSIZE, PANEL_W);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_YSIZE, PANEL_H);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_BGCOLOR, CLR_BG);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_COLOR, C'85,94,108');
   ObjectSetInteger(0, OBJ_BG, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_BACK, false);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, OBJ_BG, OBJPROP_ZORDER, 0);
}

void CreateRect(string name, int x, int y, int w, int h,
                color bg=C'20,25,32', color border=C'75,84,96')
{
   if(ObjectFind(0, name) >= 0)
      ObjectDelete(0, name);

   ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_ORIGIN_X + UIX(x));
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_ORIGIN_Y + UIY(y));
   ObjectSetInteger(0, name, OBJPROP_XSIZE, UIX(w));
   ObjectSetInteger(0, name, OBJPROP_YSIZE, UIY(h));
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
   ObjectSetInteger(0, name, OBJPROP_COLOR, border);
   ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 5);
}

void SetText(string name, string text, color clr=clrWhite, int size=-1)
{
   if(ObjectFind(0, name) < 0)
      return;
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   if(size > 0)
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, MathMax(8, UIFont(size)));
}

void CreateValueLabel(string name, string text, int x, int y, int size=12,
                      color clr=C'242,244,247')
{
   CreateLabel(name, text, x, y, size, clr, ANCHOR_LEFT_UPPER);
}

void CreateCardTitle(string name, string text, int x, int y, int w, color clr)
{
   CreateLabel(name, text, x + w/2, y, 13, clr, ANCHOR_UPPER);
}

void CreatePanel()
{
   if(!InpShowPanel)
      return;

   CalculatePanelScale();
   DeletePanel();
   CreatePanelBackground();

   // ================================================================
   // 1. 顶部标题栏
   // ================================================================
   CreateLabel(OBJ_TITLE, "手工交易控制面板", 20, 14, 20, CLR_TEXT);
   CreateLabel(OBJ_SYMBOL, _Symbol, 380, 17, 20, CLR_ORANGE, ANCHOR_LEFT_UPPER);
   // 设置位置改为“隐藏面板”按钮;点击后仅隐藏UI,EA继续运行。
   CreateButton(BTN_SETTINGS, "隐藏面板", 646, 10, 74, 34, CLR_BUTTON, CLR_TEXT, 11);
   // X:退出EA。点击后 ExpertRemove(),EA停止运行。
   CreateButton(BTN_PANEL_CLOSE, "×", 722, 9, 28, 36, C'160,45,50', clrWhite, 18);

   // ================================================================
   // 2. 手数控制
   // ================================================================
   CreateLabel(OBJ_LOTS_LBL, "手数(Lot)", 20, 67, 16, CLR_TEXT);
   CreateButton(BTN_LOT_MINUS, "−", 145, 50, 58, 50, CLR_BUTTON, CLR_TEXT, 20);
   CreateEdit(OBJ_LOTS_EDIT, DoubleToString(g_panelLots, 2), 205, 50, 210, 50);
   CreateButton(BTN_LOT_PLUS, "+", 417, 50, 58, 50, CLR_BUTTON, CLR_TEXT, 20);

   // 快捷手数
   int qy=110, qw=91, qgap=9;
   CreateButton(BTN_LOT_001, DoubleToString(InpQuickLot1, 2), 20, qy, qw, 42, C'20,160,70', clrWhite, 13);
   CreateButton(BTN_LOT_002, DoubleToString(InpQuickLot2, 2), 20+(qw+qgap)*1, qy, qw, 42, CLR_BUTTON, CLR_TEXT, 13);
   CreateButton(BTN_LOT_005, DoubleToString(InpQuickLot3, 2), 20+(qw+qgap)*2, qy, qw, 42, CLR_BUTTON, CLR_TEXT, 13);
   CreateButton(BTN_LOT_010, DoubleToString(InpQuickLot4, 2), 20+(qw+qgap)*3, qy, qw, 42, CLR_BUTTON, CLR_TEXT, 13);
   CreateButton(BTN_LOT_020, DoubleToString(InpQuickLot5, 2), 20+(qw+qgap)*4, qy, qw, 42, CLR_BUTTON, CLR_TEXT, 13);
   CreateButton(BTN_LOT_050, DoubleToString(InpQuickLot6, 2), 20+(qw+qgap)*5, qy, qw, 42, CLR_BUTTON, CLR_TEXT, 13);
   CreateButton(BTN_LOT_100, DoubleToString(InpQuickLot7, 2), 20+(qw+qgap)*6, qy, qw, 42, CLR_BUTTON, CLR_TEXT, 13);

   // ================================================================
   // 3. 一键开仓(参数可切换左右位置)
   // ================================================================
   int buyX  = InpBuyButtonOnRight ? 390 : 20;
   int sellX = InpBuyButtonOnRight ? 20  : 390;
   CreateButton(BTN_BUY, "↑   一键开多   BUY", buyX, 163, 350, 74, CLR_GREEN, clrWhite, 20);
   CreateButton(BTN_SELL, "↓   一键开空   SELL", sellX, 163, 350, 74, CLR_RED, clrWhite, 20);

   // ================================================================
   // 4. 平仓快捷区
   // ================================================================
   int bx[4]={20,205,390,575};
   CreateButton(BTN_CLOSE_BUY, "▲  平多单", bx[0], 247, 170, 48, CLR_BLUE, clrWhite, 14);
   CreateButton(BTN_CLOSE_SELL,"▼  平空单", bx[1], 247, 170, 48, CLR_BLUE, clrWhite, 14);
   CreateButton(BTN_CLOSE_PROFIT,"$  平盈利单", bx[2], 247, 170, 48, CLR_BLUE, clrWhite, 14);
   CreateButton(BTN_CLOSE_LOSS,"⌃  平亏损单", bx[3], 247, 170, 48, CLR_BLUE, clrWhite, 14);

   CreateButton(BTN_CLOSE25, "平 25%", bx[0], 305, 170, 48, CLR_BUTTON, CLR_TEXT, 14);
   CreateButton(BTN_CLOSE50, "平 50%", bx[1], 305, 170, 48, CLR_BUTTON, CLR_TEXT, 14);
   CreateButton(BTN_CLOSE75, "平 75%", bx[2], 305, 170, 48, CLR_BUTTON, CLR_TEXT, 14);
   CreateButton(BTN_CLOSEALL, "全部平仓", bx[3], 305, 170, 48, CLR_RED, clrWhite, 14);

   // ================================================================
   // 5. 当前持仓:三列卡片
   // ================================================================
   int cw=230, cg=15, cx1=20, cx2=265, cx3=510;
   CreateRect(CARD_BUY,  cx1, 375, cw, 150);
   CreateRect(CARD_SELL, cx2, 375, cw, 150);
   CreateRect(CARD_NET,  cx3, 375, cw, 150);

   CreateCardTitle(LBL_BUY_TITLE,  "多单 (BUY)", cx1, 389, cw, CLR_GREEN);
   CreateCardTitle(LBL_SELL_TITLE, "空单 (SELL)",cx2, 389, cw, CLR_RED);
   CreateCardTitle(LBL_NET_TITLE,  "净仓位",      cx3, 389, cw, CLR_TEXT);

   CreateValueLabel(LBL_BUY_LOTS,  "", cx1+20, 423, 13);
   CreateValueLabel(LBL_BUY_COUNT, "", cx1+20, 451, 13);
   CreateValueLabel(LBL_BUY_AVG,   "", cx1+20, 479, 13);
   CreateValueLabel(LBL_BUY_PNL,   "", cx1+20, 507, 13);

   CreateValueLabel(LBL_SELL_LOTS,  "", cx2+20, 423, 13);
   CreateValueLabel(LBL_SELL_COUNT, "", cx2+20, 451, 13);
   CreateValueLabel(LBL_SELL_AVG,   "", cx2+20, 479, 13);
   CreateValueLabel(LBL_SELL_PNL,   "", cx2+20, 507, 13);

   CreateLabel(LBL_NET_LOTS, "", cx3+20, 435, 22, CLR_GREEN);
   CreateLabel(LBL_NET_PNL,  "", cx3+20, 478, 15, CLR_GREEN);

   // ================================================================
   // 6. 账户 / 今日盈亏 / 交易统计
   // ================================================================
   CreateRect(CARD_ACCOUNT, 20, 540, 230, 155);
   CreateRect(CARD_TODAY, 265, 540, 230, 155);
   CreateRect(CARD_STATS, 510, 540, 230, 155);

   CreateCardTitle(LBL_ACCOUNT_H, "账户信息", 20, 554, 230, CLR_TEXT);
   CreateCardTitle(LBL_TODAY_H, "今日盈亏", 265, 554, 230, CLR_TEXT);
   CreateCardTitle(LBL_PROTECT_H, "交易统计(今日)", 510, 554, 230, CLR_TEXT);

   CreateValueLabel(LBL_ACC_BAL, "", 40, 588, 13);
   CreateValueLabel(LBL_ACC_EQ, "", 40, 616, 13);
   CreateValueLabel(LBL_ACC_MARGIN, "", 40, 644, 13);
   CreateValueLabel(LBL_ACC_FREE, "", 40, 672, 13);

   CreateValueLabel(LBL_TD_REAL, "", 285, 588, 13);
   CreateValueLabel(LBL_TD_FLOAT, "", 285, 616, 13);
   CreateValueLabel(LBL_TD_TOTAL, "", 285, 644, 13);

   CreateValueLabel(LBL_ST_COUNT, "", 530, 588, 13);
   CreateValueLabel(LBL_ST_WIN, "", 530, 616, 13);
   CreateValueLabel(LBL_ST_MAXWIN, "", 530, 644, 13);
   CreateValueLabel(LBL_ST_MAXLOSS, "", 530, 672, 13);

   // ================================================================
   // 7. 自动保护区:左侧状态 + 右侧动作
   // ================================================================
   CreateRect(CARD_PROTECT, 20, 710, 270, 190);
   CreateRect(CARD_ACTIONS, 305, 710, 435, 190);

   CreateLabel(PANEL_PREFIX+"PROTECT_TITLE", "自动保护设置", 38, 724, 16, CLR_TEXT);

   CreateLabel(PANEL_PREFIX+"STOP_TXT", "自动止损", 38, 760, 13, CLR_TEXT);
   CreateLabel(PANEL_PREFIX+"TRAIL_TXT","ATR追踪", 38, 801, 13, CLR_TEXT);
   CreateLabel(PANEL_PREFIX+"ENERGY_TXT","能量衰竭",38, 842, 13, CLR_TEXT);

   CreateRect(LBL_PROT_STOP, 190, 752, 58, 30, C'24,170,78', C'24,170,78');
   CreateRect(LBL_PROT_TRAIL,190, 793, 58, 30, C'24,170,78', C'24,170,78');
   CreateRect(LBL_PROT_ENERGY,190, 834, 58, 30, C'24,170,78', C'24,170,78');
   CreateLabel(PANEL_PREFIX+"STOP_STATE", "ON", 219, 758, 11, clrWhite, ANCHOR_UPPER);
   CreateLabel(PANEL_PREFIX+"TRAIL_STATE","ON", 219, 799, 11, clrWhite, ANCHOR_UPPER);
   CreateLabel(PANEL_PREFIX+"ENERGY_STATE","ON",219, 840, 11, clrWhite, ANCHOR_UPPER);

   CreateLabel(LBL_PROT_ATR, "", 330, 730, 13, CLR_TEXT);
   CreateLabel(LBL_PROT_VOL, "", 330, 755, 13, CLR_TEXT);
   CreateLabel(LBL_PROT_SLOPE, "", 330, 780, 13, CLR_TEXT);

   CreateButton(BTN_BREAKEVEN, "全部保本", 330, 820, 125, 55, CLR_ORANGE, clrWhite, 15);
   CreateButton(BTN_APPLY_SL, "设置 SL", 465, 820, 125, 55, CLR_ORANGE, clrWhite, 15);
   CreateButton(BTN_APPLY_TP, "设置 TP", 600, 820, 125, 55, CLR_ORANGE, clrWhite, 15);

   CreateLabel(LBL_ATR_INFO, "", 330, 883, 13, CLR_TEXT);

   // ================================================================
   // 8. 底部状态栏
   // ================================================================
   CreateLabel(LBL_STATUS, "状态:等待操作", 20, 1015, 14, CLR_GREEN);

   // QQ推广信息独立显示在图表右下角,不属于交易面板。
   CreateQQPromo();

   ChartRedraw();
}

void CreateQQPromo()
{
   // 独立于面板的右下角提示文字:面板隐藏/显示时均保留。
   if(ObjectFind(0, OBJ_QQ_PROMO) >= 0)
      ObjectDelete(0, OBJ_QQ_PROMO);

   ObjectCreate(0, OBJ_QQ_PROMO, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_XDISTANCE, 15);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_YDISTANCE, 15);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_COLOR, CLR_YELLOW);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_FONTSIZE, 12);
   ObjectSetString(0, OBJ_QQ_PROMO, OBJPROP_FONT, "Arial");
   ObjectSetString(0, OBJ_QQ_PROMO, OBJPROP_TEXT, InpQQGroupInfo);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_SELECTED, false);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_BACK, false);
   ObjectSetInteger(0, OBJ_QQ_PROMO, OBJPROP_ZORDER, 1000);
}

void CreateShowPanelButton()
{
   // 面板隐藏后保留一个独立的小按钮,供用户再次显示。
   // 使用独立对象名,不会被 DeletePanel() 清掉。
   CalculatePanelScale();

   if(ObjectFind(0, BTN_PANEL_SHOW) >= 0)
      ObjectDelete(0, BTN_PANEL_SHOW);

   int w = 96;
   int h = 34;
   int x = PANEL_ORIGIN_X;
   int y = PANEL_ORIGIN_Y;

   ObjectCreate(0, BTN_PANEL_SHOW, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_XSIZE, MathMax(80, UIX(w)));
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_YSIZE, MathMax(28, UIY(h)));
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_BGCOLOR, CLR_BUTTON);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_COLOR, CLR_TEXT);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_BORDER_COLOR, C'100,108,120');
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_FONTSIZE, MathMax(9, UIFont(12)));
   ObjectSetString(0, BTN_PANEL_SHOW, OBJPROP_FONT, "Arial");
   ObjectSetString(0, BTN_PANEL_SHOW, OBJPROP_TEXT, "显示面板");
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_ALIGN, ALIGN_CENTER);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, BTN_PANEL_SHOW, OBJPROP_ZORDER, 100);
}

void HidePanelUI()
{
   g_panelVisible = false;
   DeletePanel();
   CreateShowPanelButton();
   ChartRedraw();
}

void ShowPanelUI()
{
   g_panelVisible = true;
   if(ObjectFind(0, BTN_PANEL_SHOW) >= 0)
      ObjectDelete(0, BTN_PANEL_SHOW);
   CreatePanel();
   UpdatePanel();
   ChartRedraw();
}

void DeletePanel()
{
   for(int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--)
   {
      string name = ObjectName(0, i, -1, -1);
      if(StringFind(name, PANEL_PREFIX) == 0 && name != BTN_PANEL_SHOW)
         ObjectDelete(0, name);
   }
}

//====================================================================
// 面板状态
//====================================================================
void SetStatus(string text, color clr=clrWhite)
{
   if(ObjectFind(0, LBL_STATUS) < 0)
      return;

   ObjectSetString(0, LBL_STATUS, OBJPROP_TEXT, "状态:" + text);
   ObjectSetInteger(0, LBL_STATUS, OBJPROP_COLOR, clr);
}

void UpdatePanel()
{
   if(!InpShowPanel || !g_panelVisible)
      return;

   double buyLots, sellLots, buyAvg, sellAvg;
   double buyProfit, sellProfit;
   int buyCount, sellCount;

   GetPositionStats(buyLots, sellLots, buyAvg, sellAvg,
                    buyProfit, sellProfit, buyCount, sellCount);

   double floating = buyProfit + sellProfit;
   double netLots = buyLots - sellLots;

   // ================================================================
   // 当前持仓
   // ================================================================
   SetText(LBL_BUY_LOTS,   "手数        " + DoubleToString(buyLots, 2) + " Lot", CLR_TEXT, 13);
   SetText(LBL_BUY_COUNT,  "笔数        " + IntegerToString(buyCount) + " 笔", CLR_TEXT, 13);
   SetText(LBL_BUY_AVG,    "均价        " + (buyLots > 0 ? Price(buyAvg) : "-"), CLR_TEXT, 13);
   SetText(LBL_BUY_PNL,    "浮盈        " + Money(buyProfit), buyProfit >= 0 ? CLR_GREEN : CLR_RED, 13);

   SetText(LBL_SELL_LOTS,  "手数        " + DoubleToString(sellLots, 2) + " Lot", CLR_TEXT, 13);
   SetText(LBL_SELL_COUNT, "笔数        " + IntegerToString(sellCount) + " 笔", CLR_TEXT, 13);
   SetText(LBL_SELL_AVG,   "均价        " + (sellLots > 0 ? Price(sellAvg) : "-"), CLR_TEXT, 13);
   SetText(LBL_SELL_PNL,   "浮盈        " + Money(sellProfit), sellProfit >= 0 ? CLR_GREEN : CLR_RED, 13);

   SetText(LBL_NET_LOTS,
           (netLots >= 0 ? "+" : "") + DoubleToString(netLots, 2) + " Lot",
           netLots > 0 ? CLR_GREEN : (netLots < 0 ? CLR_RED : CLR_TEXT), 22);
   SetText(LBL_NET_PNL,
           "总浮盈  " + Money(floating),
           floating > 0 ? CLR_GREEN : (floating < 0 ? CLR_RED : CLR_TEXT), 15);

   // ================================================================
   // 账户信息
   // ================================================================
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   double margin = AccountInfoDouble(ACCOUNT_MARGIN);
   double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   double marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);

   SetText(LBL_ACC_BAL,    "余额        " + Money(balance), CLR_TEXT, 13);
   SetText(LBL_ACC_EQ,     "净值        " + Money(equity), CLR_TEXT, 13);
   SetText(LBL_ACC_MARGIN, "保证金      " + Money(margin), CLR_TEXT, 13);
   SetText(LBL_ACC_FREE,   "可用保证金  " + Money(freeMargin), CLR_TEXT, 13);

   // ================================================================
   // 今日盈亏
   // ================================================================
   int closedDeals, winningDeals, losingDeals;
   double maxWin, maxLoss;
   double today = TodayRealizedProfit(closedDeals, winningDeals, losingDeals,
                                      maxWin, maxLoss);
   double totalToday = today + floating;
   double winRate = (closedDeals > 0)
                  ? 100.0 * (double)winningDeals / (double)closedDeals
                  : 0.0;

   SetText(LBL_TD_REAL,  "已实现盈亏  " + Money(today),
           today >= 0 ? CLR_GREEN : CLR_RED, 13);
   SetText(LBL_TD_FLOAT, "浮动盈亏    " + Money(floating),
           floating >= 0 ? CLR_GREEN : CLR_RED, 13);
   SetText(LBL_TD_TOTAL, "今日总盈亏  " + Money(totalToday),
           totalToday >= 0 ? CLR_GREEN : CLR_RED, 13);

   // ================================================================
   // 交易统计
   // ================================================================
   SetText(LBL_ST_COUNT,   "平仓次数    " + IntegerToString(closedDeals), CLR_TEXT, 13);
   SetText(LBL_ST_WIN,     "胜率        " + DoubleToString(winRate, 1) + "%",
           winRate >= 50.0 ? CLR_GREEN : CLR_TEXT, 13);
   SetText(LBL_ST_MAXWIN,  "最大盈利    " + Money(maxWin), CLR_GREEN, 13);
   SetText(LBL_ST_MAXLOSS, "最大亏损    " + Money(maxLoss), maxLoss < 0 ? CLR_RED : CLR_TEXT, 13);

   // ================================================================
   // 自动保护
   // ================================================================
   double normSlope = 0.0;
   bool slopeOK = CalcChannel(normSlope);

   double atrNow = 0.0;
   double atrBuf[1];
   if(CopyBuffer(atrHandle, 0, 0, 1, atrBuf) == 1)
      atrNow = atrBuf[0];

   string stopState = InpAutoStopManual ? "ON" : "OFF";
   string trailState = InpUseTrailing ? "ON" : "OFF";
   string energyState = InpUseEnergyExit ? "ON" : "OFF";

   SetText(PANEL_PREFIX+"STOP_STATE", stopState,
           InpAutoStopManual ? clrWhite : CLR_SUB, 11);
   SetText(PANEL_PREFIX+"TRAIL_STATE", trailState,
           InpUseTrailing ? clrWhite : CLR_SUB, 11);
   SetText(PANEL_PREFIX+"ENERGY_STATE", energyState,
           InpUseEnergyExit ? clrWhite : CLR_SUB, 11);

   color stopBg = InpAutoStopManual ? C'24,170,78' : C'65,70,78';
   color trailBg = InpUseTrailing ? C'24,170,78' : C'65,70,78';
   color energyBg = InpUseEnergyExit ? C'24,170,78' : C'65,70,78';
   ObjectSetInteger(0, LBL_PROT_STOP, OBJPROP_BGCOLOR, stopBg);
   ObjectSetInteger(0, LBL_PROT_TRAIL, OBJPROP_BGCOLOR, trailBg);
   ObjectSetInteger(0, LBL_PROT_ENERGY, OBJPROP_BGCOLOR, energyBg);

   SetText(LBL_PROT_ATR, "ATR:" + DoubleToString(atrNow, _Digits) +
           "   倍数:" + DoubleToString(InpTrailingATRMult, 2), CLR_TEXT, 13);
   SetText(LBL_PROT_VOL, "波动率:" + VolRegimeText(), CLR_TEXT, 13);
   SetText(LBL_PROT_SLOPE, "斜率:" + (slopeOK ? DoubleToString(normSlope, 4) : "-") +
           "   SL距:" + IntegerToString(InpAutoStopPoints) + "点", CLR_TEXT, 13);

   SetText(LBL_ATR_INFO,
           "ATR追踪:" + string(InpUseATRTrailing ? "ON" : "OFF") +
           "    最小移动:" + IntegerToString(InpTrailingStep) + "点",
           CLR_SUB, 13);

   // 状态和页脚保持独立,不让动态数据挤出面板
   ChartRedraw();
}

bool OpenBuy()
{
   double lots = ReadPanelLots();

   m_tradeManual.SetExpertMagicNumber(InpPanelMagicNumber);
   m_tradeManual.SetDeviationInPoints(InpDeviationPoints);

   bool ok = m_tradeManual.Buy(lots, _Symbol, 0.0, 0.0, 0.0, "Manual Panel BUY");

   if(ok)
   {
      RebaseStopAfterPanelDeal();
      SetStatus("开多成功 " + DoubleToString(lots, 2) + " Lot", CLR_GREEN);
   }
   else
      SetStatus("开多失败:" + m_tradeManual.ResultRetcodeDescription(), CLR_RED);

   return ok;
}

bool OpenSell()
{
   double lots = ReadPanelLots();

   m_tradeManual.SetExpertMagicNumber(InpPanelMagicNumber);
   m_tradeManual.SetDeviationInPoints(InpDeviationPoints);

   bool ok = m_tradeManual.Sell(lots, _Symbol, 0.0, 0.0, 0.0, "Manual Panel SELL");

   if(ok)
   {
      RebaseStopAfterPanelDeal();
      SetStatus("开空成功 " + DoubleToString(lots, 2) + " Lot", CLR_GREEN);
   }
   else
      SetStatus("开空失败:" + m_tradeManual.ResultRetcodeDescription(), CLR_RED);

   return ok;
}

//====================================================================
// 平仓
//====================================================================
bool ClosePositionTicket(ulong ticket)
{
   if(ticket == 0 || !PositionSelectByTicket(ticket))
      return false;

   if(!IsManagedPosition())
      return false;

   return m_tradeManual.PositionClose(ticket);
}

void CloseByType(long type)
{
   int closed = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      if(PositionGetInteger(POSITION_TYPE) != type)
         continue;

      if(ClosePositionTicket(ticket))
         closed++;
   }

   SetStatus("平仓完成:" + IntegerToString(closed) + " 笔",
             closed > 0 ? CLR_GREEN : CLR_SUB);
}

void CloseByProfitState(bool profitOnly)
{
   int closed = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      double profit = PositionGetDouble(POSITION_PROFIT)
                    + PositionGetDouble(POSITION_SWAP);

      bool match = profitOnly ? (profit > 0.0) : (profit < 0.0);

      if(match && ClosePositionTicket(ticket))
         closed++;
   }

   SetStatus(profitOnly ? "平盈利:" : "平亏损:",
             closed > 0 ? CLR_GREEN : CLR_SUB);

   if(closed > 0)
      SetStatus((profitOnly ? "平盈利 " : "平亏损 ") +
                IntegerToString(closed) + " 笔", CLR_GREEN);
}

void CloseAllManaged()
{
   int closed = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      if(ClosePositionTicket(ticket))
         closed++;
   }

   SetStatus("全部平仓:" + IntegerToString(closed) + " 笔",
             closed > 0 ? CLR_GREEN : CLR_SUB);
}

void ClosePercent(double percent)
{
   if(percent <= 0.0 || percent > 100.0)
      return;

   int affected = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      double volume = PositionGetDouble(POSITION_VOLUME);
      double closeVolume = NormalizeVolume(volume * percent / 100.0);

      if(closeVolume >= volume)
      {
         if(ClosePositionTicket(ticket))
            affected++;
         continue;
      }

      double minVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
      if(closeVolume < minVol)
         continue;

      if(m_tradeManual.PositionClosePartial(ticket, closeVolume))
         affected++;
   }

   SetStatus("减仓 " + DoubleToString(percent, 0) + "%:"
             + IntegerToString(affected) + " 笔",
             affected > 0 ? CLR_GREEN : CLR_SUB);
}

//====================================================================
// 一键保本
//====================================================================
void MoveAllToBreakeven()
{
   int modified = 0;

   double stopLevel = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL)
                    * _Point;

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double open = PositionGetDouble(POSITION_PRICE_OPEN);
      double tp = PositionGetDouble(POSITION_TP);

      double newSl = open;

      if(type == POSITION_TYPE_BUY)
      {
         if(newSl >= bid - stopLevel)
            newSl = bid - stopLevel;
      }
      else
      {
         if(newSl <= ask + stopLevel)
            newSl = ask + stopLevel;
      }

      newSl = NormalizeDouble(newSl, _Digits);

      if(newSl <= 0.0)
         continue;

      if(m_tradeManual.PositionModify(ticket, newSl, tp))
         modified++;
   }

   SetStatus("保本完成:" + IntegerToString(modified) + " 笔",
             modified > 0 ? CLR_GREEN : CLR_SUB);
}

//====================================================================
// 设置全部SL
//====================================================================
void ApplyStopLoss()
{
   int modified = 0;
   double dist = InpAutoStopPoints * UserPointSize();

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double tp = PositionGetDouble(POSITION_TP);
      double newSl = (type == POSITION_TYPE_BUY)
                   ? bid - dist
                   : ask + dist;

      newSl = NormalizeDouble(newSl, _Digits);

      if(newSl <= 0.0)
         continue;

      if(m_tradeManual.PositionModify(ticket, newSl, tp))
         modified++;
   }

   SetStatus("设置SL:" + IntegerToString(modified) + " 笔",
             modified > 0 ? CLR_GREEN : CLR_SUB);
}

//====================================================================
// 设置全部TP
// 这里使用 SL 距离的同值作为 TP 距离,便于第一版统一操作。
//====================================================================
void ApplyTakeProfit()
{
   int modified = 0;
   double dist = InpAutoStopPoints * UserPointSize();

   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(!IsManagedPosition())
         continue;

      long type = PositionGetInteger(POSITION_TYPE);
      double sl = PositionGetDouble(POSITION_SL);

      double newTp = (type == POSITION_TYPE_BUY)
                   ? ask + dist
                   : bid - dist;

      newTp = NormalizeDouble(newTp, _Digits);

      if(newTp <= 0.0)
         continue;

      if(m_tradeManual.PositionModify(ticket, sl, newTp))
         modified++;
   }

   SetStatus("设置TP:" + IntegerToString(modified) + " 笔",
             modified > 0 ? CLR_GREEN : CLR_SUB);
}

//====================================================================
// 图表事件
//====================================================================
void SetPanelLots(double lots)
{
   lots = NormalizeVolume(lots);
   g_panelLots = lots;
   if(ObjectFind(0, OBJ_LOTS_EDIT) >= 0)
      ObjectSetString(0, OBJ_LOTS_EDIT, OBJPROP_TEXT, DoubleToString(lots, 2));
}

void ChangePanelLots(double delta)
{
   double current = ReadPanelLots();
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   if(step <= 0.0) step = 0.01;
   SetPanelLots(current + delta);
}

void OnChartEvent(const int id, const long &lparam,
                  const double &dparam, const string &sparam)
{
   // 图表窗口大小改变时自动重新计算缩放,避免面板偶尔变得过大/越界。
   if(id == CHARTEVENT_CHART_CHANGE)
   {
      if(ObjectFind(0, OBJ_QQ_PROMO) < 0)
         CreateQQPromo();

      if(g_panelVisible && InpShowPanel)
      {
         CreatePanel();
         UpdatePanel();
      }
      else if(!g_panelVisible)
      {
         CreateShowPanelButton();
      }
      return;
   }

   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      // 关闭 X:真正卸载 EA,面板及后台保护功能全部停止。
      if(sparam == BTN_PANEL_CLOSE)
      {
         ExpertRemove();
         return;
      }

      // “隐藏面板”:仅隐藏UI,EA继续运行。
      if(sparam == BTN_SETTINGS)
      {
         HidePanelUI();
         return;
      }

      // 独立的“显示面板”按钮:重新显示UI。
      if(sparam == BTN_PANEL_SHOW)
      {
         ShowPanelUI();
         return;
      }

      if(sparam == BTN_LOT_MINUS)
         ChangePanelLots(-SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP));
      else if(sparam == BTN_LOT_PLUS)
         ChangePanelLots(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP));
      else if(sparam == BTN_LOT_001)
         SetPanelLots(InpQuickLot1);
      else if(sparam == BTN_LOT_002)
         SetPanelLots(InpQuickLot2);
      else if(sparam == BTN_LOT_005)
         SetPanelLots(InpQuickLot3);
      else if(sparam == BTN_LOT_010)
         SetPanelLots(InpQuickLot4);
      else if(sparam == BTN_LOT_020)
         SetPanelLots(InpQuickLot5);
      else if(sparam == BTN_LOT_050)
         SetPanelLots(InpQuickLot6);
      else if(sparam == BTN_LOT_100)
         SetPanelLots(InpQuickLot7);
      else if(sparam == BTN_BUY)
         OpenBuy();
      else if(sparam == BTN_SELL)
         OpenSell();
      else if(sparam == BTN_CLOSE_BUY)
         CloseByType(POSITION_TYPE_BUY);
      else if(sparam == BTN_CLOSE_SELL)
         CloseByType(POSITION_TYPE_SELL);
      else if(sparam == BTN_CLOSE_PROFIT)
         CloseByProfitState(true);
      else if(sparam == BTN_CLOSE_LOSS)
         CloseByProfitState(false);
      else if(sparam == BTN_CLOSE25)
         ClosePercent(25.0);
      else if(sparam == BTN_CLOSE50)
         ClosePercent(50.0);
      else if(sparam == BTN_CLOSE75)
         ClosePercent(75.0);
      else if(sparam == BTN_CLOSEALL)
         CloseAllManaged();
      else if(sparam == BTN_BREAKEVEN)
         MoveAllToBreakeven();
      else if(sparam == BTN_APPLY_SL)
         ApplyStopLoss();
      else if(sparam == BTN_APPLY_TP)
         ApplyTakeProfit();

      UpdatePanel();
   }
}

int OnInit()
{
   m_tradeManual.SetExpertMagicNumber(InpPanelMagicNumber);
   m_tradeManual.SetDeviationInPoints(InpDeviationPoints);
   m_tradeManual.SetTypeFillingBySymbol(_Symbol);

   g_panelLots = NormalizeVolume(InpDefaultLots);

   atrHandle = iATR(_Symbol, _Period, InpATRPeriod);

   if(atrHandle == INVALID_HANDLE)
   {
      Print("ATR指标句柄创建失败");
      return INIT_FAILED;
   }

   g_panelVisible = InpShowPanel;

   if(g_panelVisible)
   {
      CalculatePanelScale();
      CreatePanel();
   }
   else
   {
      CreateShowPanelButton();
   }

   EventSetTimer(1);

   Print("手工交易面板 EA v2.19 启动");
   Print("加入QQ群,免费送EA:978875454");

   UpdatePanel();

   return INIT_SUCCEEDED;
}

//====================================================================
// 卸载
//====================================================================
void OnDeinit(const int reason)
{
   EventKillTimer();

   if(atrHandle != INVALID_HANDLE)
      IndicatorRelease(atrHandle);

   DeletePanel();
   if(ObjectFind(0, BTN_PANEL_SHOW) >= 0)
      ObjectDelete(0, BTN_PANEL_SHOW);
   if(ObjectFind(0, OBJ_QQ_PROMO) >= 0)
      ObjectDelete(0, OBJ_QQ_PROMO);

   Print("手工交易面板 EA 卸载");
}

//====================================================================
// 成交审计:任何平仓成交都在“专家”日志中打印真实触发原因。
// 这样可直接区分 SL、手工点击、EA程序或其他来源。
//====================================================================
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
{
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD || trans.deal == 0)
      return;

   if(!HistoryDealSelect(trans.deal))
      return;

   string symbol = HistoryDealGetString(trans.deal, DEAL_SYMBOL);
   if(symbol != _Symbol)
      return;

   long entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
   if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_OUT_BY)
      return;

   long reason = HistoryDealGetInteger(trans.deal, DEAL_REASON);
   long magic  = HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
   double price = HistoryDealGetDouble(trans.deal, DEAL_PRICE);
   double profit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT);
   string comment = HistoryDealGetString(trans.deal, DEAL_COMMENT);

   PrintFormat("[平仓审计] deal=%I64u reason=%s magic=%I64d price=%.*f profit=%.2f comment=%s",
               trans.deal,
               EnumToString((ENUM_DEAL_REASON)reason),
               magic,
               _Digits,
               price,
               profit,
               comment);
}

//====================================================================
// Tick
//====================================================================
void OnTick()
{
   AutoSetStopForManualPositions();
   TrailingStop();
   CheckEnergyExhaustion();
}

//====================================================================
// Timer
//====================================================================
void OnTimer()
{
   UpdatePanel();
}
//+------------------------------------------------------------------+


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|量化魔方 ( 陕ICP备2025062059号-3 )|网站地图

GMT+8, 2026-9-8 01:55 , Processed in 0.529399 second(s), 5 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表