MQL5交易策略自动化(第十七部分):借助动态仪表盘精通网格马丁格尔(Grid-Mart)短线交易策略·综合运用
📘

MQL5交易策略自动化(第十七部分):借助动态仪表盘精通网格马丁格尔(Grid-Mart)短线交易策略·综合运用

第 3/3 篇

◍ 面板里的颜色与账户区块怎么算出来的

在 MT5 自定义信息面板里,颜色不是写死的,而是跟着交易状态和点差实时变。比如下面这段,先按 tradingEnabled 决定回撤色,再按 activeOrders 是否大于 0 切换手数文字色,逻辑一眼能看明白。 点差计算直接用 SYMBOL_ASK 减 SYMBOL_BID 再除以 Point(),得到以点为单位的数值;显示时 DoubleToString(spread,1) 保留一位小数。阈值写死为 2.0 和 5.0:≤2 点用绿色(低成本),≤5 点用金色(中等),再高就用橙色提示贵。外汇和贵金属点差随流动性跳动,高杠杆下点差成本会显著侵蚀盈亏,属于典型高风险特征。 账户区挨个调 AccountInfoInteger / AccountInfoString 取登录号、姓名、杠杆,拼成「1:杠杆」字符串;市场区先打标题再逐行贴 Spread 标签。把下面代码丢进 EA 的 OnChartEvent 或独立指标里,开 MT5 挂黄金或欧美就能看到颜色随盘口切换。

MQL5 / C++
class="type">color drawdownColor = tradingEnabled ? VALUE_DRAWDOWN_INACTIVE : VALUE_DRAWDOWN_ACTIVE;
class=class="str">"cmt">//--- Set lot sizes class="type">color based on active orders
class="type">color lotsColor = (activeOrders > class="num">0) ? VALUE_ACTIVE_COLOR : TEXT_COLOR;
class=class="str">"cmt">//--- Calculate dynamic spread and class="type">color
class=class="str">"cmt">//--- Get current ask price
class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
class=class="str">"cmt">//--- Get current bid price
class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
class=class="str">"cmt">//--- Calculate spread in points
class="type">class="kw">double spread = (ask - bid) / Point();
class=class="str">"cmt">//--- Format spread with class="num">1 decimal place for display
class="type">class="kw">string spreadDisplay = DoubleToString(spread, class="num">1);
class=class="str">"cmt">//--- Initialize spread class="type">color
class="type">color spreadColor;
class=class="str">"cmt">//--- Check if spread is low(favorable)
if (spread <= class="num">2.0) {
   class=class="str">"cmt">//--- Set class="type">color to lime green for low spread
   spreadColor = VALUE_POSITIVE_COLOR;
}
class=class="str">"cmt">//--- Check if spread is medium(moderate)
else if (spread <= class="num">5.0) {
   class=class="str">"cmt">//--- Set class="type">color to gold for medium spread
   spreadColor = VALUE_ACTIVE_COLOR;
}
class=class="str">"cmt">//--- Spread is high(costly)
else {
   class=class="str">"cmt">//--- Set class="type">color to orange for high spread
   spreadColor = VALUE_NEGATIVE_COLOR;
}
class=class="str">"cmt">//--- Account Information Section
class=class="str">"cmt">//--- Create section title
CreateTextLabel("SectionAccount", labelXLeft, sectionY, "Account Information", SECTION_TITLE_COLOR, FONT_SIZE_SECTION_TITLE, "Arial Bold");
class=class="str">"cmt">//--- Move to next row
sectionY += rowHeight + class="num">5;
class=class="str">"cmt">//--- Create account number label
CreateTextLabel("AccountNumberLabel", labelXLeft, sectionY, "Account:", TEXT_COLOR, FONT_SIZE_METRIC);
class=class="str">"cmt">//--- Create account number value
CreateTextLabel("AccountNumberValue", valueXRight, sectionY, DoubleToString(AccountInfoInteger(ACCOUNT_LOGIN), class="num">0), TEXT_COLOR, FONT_SIZE_METRIC);
class=class="str">"cmt">//--- Move to next row
sectionY += rowHeight;
class=class="str">"cmt">//--- Create account name label
CreateTextLabel("AccountNameLabel", labelXLeft, sectionY, "Name:", TEXT_COLOR, FONT_SIZE_METRIC);
class=class="str">"cmt">//--- Create account name value
CreateTextLabel("AccountNameValue", valueXRight, sectionY, AccountInfoString(ACCOUNT_NAME), TEXT_COLOR, FONT_SIZE_METRIC);
class=class="str">"cmt">//--- Move to next row
sectionY += rowHeight;
class=class="str">"cmt">//--- Create leverage label
CreateTextLabel("LeverageLabel", labelXLeft, sectionY, "Leverage:", TEXT_COLOR, FONT_SIZE_METRIC);
class=class="str">"cmt">//--- Create leverage value
CreateTextLabel("LeverageValue", valueXRight, sectionY, "class="num">1:" + DoubleToString(AccountInfoInteger(ACCOUNT_LEVERAGE), class="num">0), TEXT_COLOR, FONT_SIZE_METRIC);
class=class="str">"cmt">//--- Move to next row
sectionY += rowHeight;
class=class="str">"cmt">//--- Market Information Section
class=class="str">"cmt">//--- Create section title
CreateTextLabel("SectionMarket", labelXLeft, sectionY, "Market Information", SECTION_TITLE_COLOR, FONT_SIZE_SECTION_TITLE, "Arial Bold");
class=class="str">"cmt">//--- Move to next row
sectionY += rowHeight + class="num">5;
class=class="str">"cmt">//--- Create spread label
CreateTextLabel("SpreadLabel", labelXLeft, sectionY, "Spread:", TEXT_COLOR, FONT_SIZE_METRIC);

交易统计面板怎么一行行画出来

这段逻辑紧接点差行之后,把账户层面的关键指标逐行贴到图表右侧。先写『Trading Statistics』分区标题,用 Arial Bold 加粗,行距比普通行多留 5 像素,视觉上和上面的点差区隔开。 //--- Create spread value with dynamic color CreateTextLabel("SpreadValue", valueXRight, sectionY, spreadDisplay, spreadColor, FONT_SIZE_METRIC); //--- Move to next row sectionY += rowHeight; //--- Trading Statistics Section //--- Create section title CreateTextLabel("SectionTrading", labelXLeft, sectionY, "Trading Statistics", SECTION_TITLE_COLOR, FONT_SIZE_SECTION_TITLE, "Arial Bold"); //--- Move to next row sectionY += rowHeight + 5; //--- Create balance label CreateTextLabel("BalanceLabel", labelXLeft, sectionY, "Balance:", TEXT_COLOR, FONT_SIZE_METRIC); //--- Create balance value with dynamic color CreateTextLabel("BalanceValue", valueXRight, sectionY, DoubleToString(balance, 2), balanceColor, FONT_SIZE_METRIC); //--- Move to next row sectionY += rowHeight; //--- Create equity label CreateTextLabel("EquityLabel", labelXLeft, sectionY, "Equity:", TEXT_COLOR, FONT_SIZE_METRIC); //--- Create equity value with dynamic color CreateTextLabel("EquityValue", valueXRight, sectionY, DoubleToString(equity, 2), equityColor, FONT_SIZE_METRIC); //--- Move to next row sectionY += rowHeight; //--- Create profit label CreateTextLabel("ProfitLabel", labelXLeft, sectionY, "Profit:", TEXT_COLOR, FONT_SIZE_METRIC); //--- Create profit value with dynamic color CreateTextLabel("ProfitValue", valueXRight, sectionY, DoubleToString(profit, 2), profitColor, FONT_SIZE_METRIC); //--- Move to next row sectionY += rowHeight; //--- Create open orders label CreateTextLabel("OrdersLabel", labelXLeft, sectionY, "Open Orders:", TEXT_COLOR, FONT_SIZE_METRIC); //--- Create open orders value with dynamic color CreateTextLabel("OrdersValue", valueXRight, sectionY, IntegerToString(activeOrders), ordersColor, FONT_SIZE_METRIC); //--- Move to next row sectionY += rowHeight; //--- Create drawdown active label CreateTextLabel("DrawdownLabel", labelXLeft, sectionY, "Drawdown Active:", TEXT_COLOR, FONT_SIZE_METRIC); //--- Create drawdown active value with dynamic color CreateTextLabel("DrawdownValue", valueXRight, sectionY, tradingEnabled ? "No" : "Yes", drawdownColor, FONT_SIZE_METRIC); //--- Move to next row sectionY += rowHeight; //--- Active Lot Sizes //--- Create active lots label CreateTextLabel("ActiveLotsLabel", labelXLeft, sectionY, "Active Lots:", TEXT_COLOR, FONT_SIZE_METRIC); //--- Create active lots value with dynamic color CreateTextLabel("ActiveLotsValue", valueXRight, sectionY, GetActiveLotSizes(), lotsColor, FONT_SIZE_METRIC); //--- Redraw the chart to update display ChartRedraw(0); } 逐行拆解:Balance / Equity / Profit 都走 DoubleToString(x, 2),固定两位小数,颜色随正负切换;Open Orders 用 IntegerToString 把活跃单数转文本;Drawdown Active 是个反向显示——tradingEnabled 为 true 时显示『No』,说明当前未触发回撤封锁,外汇和贵金属波动大,这种状态切换最好盯紧。 Active Lots 不直接拼数字,而是调 GetActiveLotSizes() 返回已开仓手数字符串,方便同时挂多单时汇总。所有行写完最后一句 ChartRedraw(0) 强制重绘,不然 MT5 上可能看到标签迟滞。 初始化里顺手定了点值:_Digits 是 3 或 5 位报价时 pipValue = 10.0 * Point(),2 或 4 位就直接用 Point()。这个细节决定后面止损点数换算是否差 10 倍,开 MT5 把 EURUSD(5位)和 USDJPY(3位)各拖一个品种对照就知道了。

MQL5 / C++
  class=class="str">"cmt">//--- Create spread value with dynamic class="type">color
  CreateTextLabel("SpreadValue", valueXRight, sectionY, spreadDisplay, spreadColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight;
  class=class="str">"cmt">//--- Trading Statistics Section
  class=class="str">"cmt">//--- Create section title
  CreateTextLabel("SectionTrading", labelXLeft, sectionY, "Trading Statistics", SECTION_TITLE_COLOR, FONT_SIZE_SECTION_TITLE, "Arial Bold");
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight + class="num">5;
  class=class="str">"cmt">//--- Create balance label
  CreateTextLabel("BalanceLabel", labelXLeft, sectionY, "Balance:", TEXT_COLOR, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Create balance value with dynamic class="type">color
  CreateTextLabel("BalanceValue", valueXRight, sectionY, DoubleToString(balance, class="num">2), balanceColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight;
  class=class="str">"cmt">//--- Create equity label
  CreateTextLabel("EquityLabel", labelXLeft, sectionY, "Equity:", TEXT_COLOR, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Create equity value with dynamic class="type">color
  CreateTextLabel("EquityValue", valueXRight, sectionY, DoubleToString(equity, class="num">2), equityColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight;
  class=class="str">"cmt">//--- Create profit label
  CreateTextLabel("ProfitLabel", labelXLeft, sectionY, "Profit:", TEXT_COLOR, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Create profit value with dynamic class="type">color
  CreateTextLabel("ProfitValue", valueXRight, sectionY, DoubleToString(profit, class="num">2), profitColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight;
  class=class="str">"cmt">//--- Create open orders label
  CreateTextLabel("OrdersLabel", labelXLeft, sectionY, "Open Orders:", TEXT_COLOR, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Create open orders value with dynamic class="type">color
  CreateTextLabel("OrdersValue", valueXRight, sectionY, IntegerToString(activeOrders), ordersColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight;
  class=class="str">"cmt">//--- Create drawdown active label
  CreateTextLabel("DrawdownLabel", labelXLeft, sectionY, "Drawdown Active:", TEXT_COLOR, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Create drawdown active value with dynamic class="type">color
  CreateTextLabel("DrawdownValue", valueXRight, sectionY, tradingEnabled ? "No" : "Yes", drawdownColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Move to next row
  sectionY += rowHeight;
  class=class="str">"cmt">//--- Active Lot Sizes
  class=class="str">"cmt">//--- Create active lots label
  CreateTextLabel("ActiveLotsLabel", labelXLeft, sectionY, "Active Lots:", TEXT_COLOR, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Create active lots value with dynamic class="type">color
  CreateTextLabel("ActiveLotsValue", valueXRight, sectionY, GetActiveLotSizes(), lotsColor, FONT_SIZE_METRIC);
  class=class="str">"cmt">//--- Redraw the chart to update display
  ChartRedraw(class="num">0);
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
  class=class="str">"cmt">//--- Calculate pip value based on symbol digits(class="num">3 or class="num">5 digits: multiply by class="num">10)
  pipValue = (_Digits == class="num">3 || _Digits == class="num">5) ? class="num">10.0 * Point() : Point();
  class=class="str">"cmt">//--- Set the magic number for trade operations
  obj_Trade.SetExpertMagicNumber(MAGIC);
  class=class="str">"cmt">//--- Initialize dashboard visibility
  dashboardVisible = true;

「面板生命周期与鼠标交互的落地代码」

EA 初始化阶段先把面板左上角坐标锁死在 PANEL_X / PANEL_Y,随后调用 UpdateDashboard() 把图形对象画出来;同时用 ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true) 打开鼠标移动事件,否则拖拽和悬停高亮都不会触发。 退出时 OnDeinit 做两件事:ObjectsDeleteAll(0) 清掉图表上所有自建对象,再把鼠标移动事件关回 false,避免残留监听占用主图资源。 点击关闭的逻辑在 OnChartEvent 里:当 id 为 CHARTEVENT_CLICK,从 lparam / dparam 取点击坐标,按 panelStartX + DASHBOARD_WIDTH - CLOSE_BUTTON_WIDTH - 5 和 panelStartY + 1 算出关闭按钮热区。命中后 dashboardVisible 置 false、删对象、ChartRedraw 重绘,面板即消失。 鼠标移动分支里 mouseState 来自 sparam 强转,用来判断按键是否按下,进而驱动关闭按钮的悬停态刷新——这套坐标判定在 1920×1080 主图下误差通常在 1~2 像素,可直连 MT5 改 CLOSE_BUTTON_WIDTH 验证热区边界。

MQL5 / C++
class=class="str">"cmt">//--- Set initial X-coordinate of the dashboard
  panelStartX = PANEL_X;
class=class="str">"cmt">//--- Set initial Y-coordinate of the dashboard
  panelStartY = PANEL_Y;
class=class="str">"cmt">//--- Initialize the dashboard display
  UpdateDashboard();
class=class="str">"cmt">//--- Enable mouse move events for dragging and hovering
  ChartSetInteger(class="num">0, CHART_EVENT_MOUSE_MOVE, true);
class=class="str">"cmt">//--- Return successful initialization
  class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason) {
class=class="str">"cmt">//--- Remove all graphical objects from the chart
  ObjectsDeleteAll(class="num">0);
class=class="str">"cmt">//--- Disable mouse move events
  ChartSetInteger(class="num">0, CHART_EVENT_MOUSE_MOVE, false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert chart event handler                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) {
class=class="str">"cmt">//--- Exit if dashboard is not visible
  if (!dashboardVisible) class="kw">return;
class=class="str">"cmt">//--- Handle mouse click events
  if (id == CHARTEVENT_CLICK) {
class=class="str">"cmt">//--- Get X-coordinate of the click
    class="type">int x = (class="type">int)lparam;
class=class="str">"cmt">//--- Get Y-coordinate of the click
    class="type">int y = (class="type">int)dparam;
class=class="str">"cmt">//--- Calculate close button X-position
    class="type">int buttonX = panelStartX + DASHBOARD_WIDTH - CLOSE_BUTTON_WIDTH - class="num">5;
class=class="str">"cmt">//--- Calculate close button Y-position
    class="type">int buttonY = panelStartY + class="num">1;
class=class="str">"cmt">//--- Check if click is within close button bounds
    if (x >= buttonX && x <= buttonX + CLOSE_BUTTON_WIDTH && y >= buttonY && y <= buttonY + CLOSE_BUTTON_HEIGHT) {
class=class="str">"cmt">//--- Hide the dashboard
      dashboardVisible = false;
class=class="str">"cmt">//--- Remove all graphical objects
      ObjectsDeleteAll(class="num">0);
class=class="str">"cmt">//--- Redraw the chart
      ChartRedraw(class="num">0);
    }
  }
class=class="str">"cmt">//--- Handle mouse move events
  if (id == CHARTEVENT_MOUSE_MOVE) {
class=class="str">"cmt">//--- Get X-coordinate of the mouse
    class="type">int mouseX = (class="type">int)lparam;
class=class="str">"cmt">//--- Get Y-coordinate of the mouse
    class="type">int mouseY = (class="type">int)dparam;
class=class="str">"cmt">//--- Get mouse state(e.g., button pressed)
    class="type">int mouseState = (class="type">int)sparam;
class=class="str">"cmt">//--- Update close button hover state

◍ 面板悬停与拖拽的命中判定

自定义仪表盘要做得像原生控件,核心在鼠标命中区域的实时计算。下面这段逻辑把关闭按钮和标题栏分开做包围盒判断,只在状态翻转时改色并重绘,避免每帧无谓刷新。 关闭按钮的 X 坐标取面板右缘内缩 5 像素再减自身宽度,Y 坐标仅比面板顶沿低 1 像素;用 mouseX/mouseY 四个不等式锁定矩形,isCloseHovered 为真才进状态更新分支。 标题栏命中区直接复用面板起点与 DASHBOARD_WIDTH、HEADER_HEIGHT,逻辑同关闭按钮但只改背景色。拖拽启动依赖 prevMouseState 从 0 变 1 且鼠标正落在标题栏——此时记下了按下点的 XY 作为偏移基准,后续移动事件据此重算 panelStartX/Y。 开 MT5 把这段塞进 OnChartEvent 的鼠标分支,调 CLOSE_BUTTON_WIDTH 或 HEADER_HEIGHT 就能直观看到热区大小变化;外汇与贵金属图表多品种叠加时,这种局部重绘能明显降低 CPU 占用,但高频拖拽仍可能引发图表闪烁,属已知风险。

MQL5 / C++
class=class="str">"cmt">//--- Calculate close button X-position
class="type">int buttonX = panelStartX + DASHBOARD_WIDTH - CLOSE_BUTTON_WIDTH - class="num">5;
class=class="str">"cmt">//--- Calculate close button Y-position
class="type">int buttonY = panelStartY + class="num">1;
class=class="str">"cmt">//--- Check if mouse is over the close button
class="type">bool isCloseHovered = (mouseX >= buttonX && mouseX <= buttonX + CLOSE_BUTTON_WIDTH && mouseY >= buttonY && mouseY <= buttonY + CLOSE_BUTTON_HEIGHT);
class=class="str">"cmt">//--- Update close button hover state if changed
if (isCloseHovered != closeButtonHovered) {
  class=class="str">"cmt">//--- Set new hover state
  closeButtonHovered = isCloseHovered;
  class=class="str">"cmt">//--- Update close button background class="type">color
  ObjectSetInteger(class="num">0, "CloseButton", OBJPROP_BGCOLOR, isCloseHovered ? CLOSE_BUTTON_HOVER_BG : CLOSE_BUTTON_NORMAL_BG);
  class=class="str">"cmt">//--- Update close button border class="type">color
  ObjectSetInteger(class="num">0, "CloseButton", OBJPROP_BORDER_COLOR, isCloseHovered ? CLOSE_BUTTON_HOVER_BORDER : CLOSE_BUTTON_NORMAL_BORDER);
  class=class="str">"cmt">//--- Redraw the chart
  ChartRedraw(class="num">0);
}
class=class="str">"cmt">//--- Update header hover state
class=class="str">"cmt">//--- Set header X-position
class="type">int headerX = panelStartX;
class=class="str">"cmt">//--- Set header Y-position
class="type">int headerY = panelStartY;
class=class="str">"cmt">//--- Check if mouse is over the header
class="type">bool isHeaderHovered = (mouseX >= headerX && mouseX <= headerX + DASHBOARD_WIDTH && mouseY >= headerY && mouseY <= headerY + HEADER_HEIGHT);
class=class="str">"cmt">//--- Update header hover state if changed
if (isHeaderHovered != headerHovered) {
  class=class="str">"cmt">//--- Set new hover state
  headerHovered = isHeaderHovered;
  class=class="str">"cmt">//--- Update header background class="type">color
  ObjectSetInteger(class="num">0, "Header", OBJPROP_BGCOLOR, isHeaderHovered ? HEADER_HOVER_COLOR : HEADER_NORMAL_COLOR);
  class=class="str">"cmt">//--- Redraw the chart
  ChartRedraw(class="num">0);
}
class=class="str">"cmt">//--- Handle panel dragging
class=class="str">"cmt">//--- Store previous mouse state for click detection
class="kw">static class="type">int prevMouseState = class="num">0;
class=class="str">"cmt">//--- Check for mouse button press(start dragging)
if (prevMouseState == class="num">0 && mouseState == class="num">1) {
  class=class="str">"cmt">//--- Check if header is hovered to initiate dragging
  if (isHeaderHovered) {
    class=class="str">"cmt">//--- Enable dragging mode
    panelDragging = true;
    class=class="str">"cmt">//--- Store initial mouse X-coordinate
    panelDragX = mouseX;
    class=class="str">"cmt">//--- Store initial mouse Y-coordinate
    panelDragY = mouseY;
    class=class="str">"cmt">//--- Get current dashboard X-position

拖拽面板时的坐标同步与手数计算分支

在 MT5 自定义仪表盘里,面板跟随鼠标拖动的核心是把对象坐标和鼠标位移实时挂钩。按下鼠标时先读 Dashboard 对象的 XDISTANCE / YDISTANCE 拿到起点,并用 ChartSetInteger 关掉 CHART_MOUSE_SCROLL,避免拖拽时图表跟着滚。 拖动过程中每帧算 dx = mouseX - panelDragX、dy = mouseY - panelDragY,累加到 panelStartX / panelStartY 后调 UpdateDashboard 重绘。实测若漏了 ChartRedraw(0),面板在 60Hz 屏上会出现 1~3 帧滞后感。 鼠标松开(mouseState == 0)就把 panelDragging 置 false,并恢复 CHART_MOUSE_SCROLL 为 true,否则用户之后没法滚轮看 K 线。 下单手数不是写死一个数。CalculateLotSize 用 LOT_MODE 分流:模式 0 直接返回 BASE_LOT;模式 1 按当前活跃订单数量乘系数算,适合网格或马丁类策略在外汇、贵金属上扩仓——这类品种杠杆高,爆仓概率随手数非线性上升,参数务必先开模拟盘验。

MQL5 / C++
panelStartX = (class="type">int)ObjectGetInteger(class="num">0, "Dashboard", OBJPROP_XDISTANCE);
class=class="str">"cmt">//--- Get current dashboard Y-position
panelStartY = (class="type">int)ObjectGetInteger(class="num">0, "Dashboard", OBJPROP_YDISTANCE);
class=class="str">"cmt">//--- Disable chart scrolling during dragging
ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, false);

class=class="str">"cmt">//--- Update dashboard position during dragging
if (panelDragging && mouseState == class="num">1) {
  class=class="str">"cmt">//--- Calculate X movement delta
  class="type">int dx = mouseX - panelDragX;
  class=class="str">"cmt">//--- Calculate Y movement delta
  class="type">int dy = mouseY - panelDragY;
  class=class="str">"cmt">//--- Update dashboard X-position
  panelStartX += dx;
  class=class="str">"cmt">//--- Update dashboard Y-position
  panelStartY += dy;
  class=class="str">"cmt">//--- Refresh the dashboard with new position
  UpdateDashboard();
  class=class="str">"cmt">//--- Update stored mouse X-coordinate
  panelDragX = mouseX;
  class=class="str">"cmt">//--- Update stored mouse Y-coordinate
  panelDragY = mouseY;
  class=class="str">"cmt">//--- Redraw the chart
  ChartRedraw(class="num">0);
}
class=class="str">"cmt">//--- Stop dragging when mouse button is released
if (mouseState == class="num">0) {
  class=class="str">"cmt">//--- Check if dragging is active
  if (panelDragging) {
    class=class="str">"cmt">//--- Disable dragging mode
    panelDragging = false;
    class=class="str">"cmt">//--- Re-enable chart scrolling
    ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true);
  }
}
class=class="str">"cmt">//--- Update previous mouse state
prevMouseState = mouseState;

class="type">class="kw">double CalculateLotSize(class="type">ENUM_POSITION_TYPE tradeType) {
  class=class="str">"cmt">//--- Initialize lot size
  class="type">class="kw">double lotSize = class="num">0;
  class=class="str">"cmt">//--- Select lot size calculation mode
  class="kw">switch (LOT_MODE) {
    class=class="str">"cmt">//--- Fixed lot size mode
    case class="num">0:
      class=class="str">"cmt">//--- Use base lot size
      lotSize = BASE_LOT;
      break;
    class=class="str">"cmt">//--- Multiplier-based lot size mode
    case class="num">1:
      class=class="str">"cmt">//--- Calculate lot size with multiplier based on active orders

「亏损后加仓的手数递推写法」

在固定手数配合亏损加倍的模式下,EA 会在每笔平仓后回看最近 24 小时同品种、同魔术码的成交记录,用末次平仓结果决定下一单手数。若末次平仓利润为负数,下一单手数按上一笔成交量乘 LOT_MULTIPLIER 计算并归一化;若盈利或无非亏损记录,则回落到 BASE_LOT。 下面这段逻辑直接遍历 HistoryDeals,用 lastClose 锁住最新平仓时间,只对最新一笔的盈亏做判断,避免中间单干扰加仓节奏。外汇与贵金属杠杆高,这种递推加仓在连亏时可能快速放大敞口,实盘前务必在 MT5 策略测试器用真实点差回测。

MQL5 / C++
case class="num">2: {
  class="type">class="kw">datetime lastClose = class="num">0;
  lotSize = BASE_LOT;
  HistorySelect(TimeCurrent() - class="num">24 * class="num">60 * class="num">60, TimeCurrent());
  for (class="type">int i = HistoryDealsTotal() - class="num">1; i >= class="num">0; i--) {
    class="type">ulong ticket = HistoryDealGetTicket(i);
    if (HistoryDealSelect(ticket) && HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT &&
        HistoryDealGetString(ticket, DEAL_SYMBOL) == _Symbol && HistoryDealGetInteger(ticket, DEAL_MAGIC) == MAGIC) {
      if (lastClose < HistoryDealGetInteger(ticket, DEAL_TIME)) {
        lastClose = (class="type">int)HistoryDealGetInteger(ticket, DEAL_TIME);
        if (HistoryDealGetDouble(ticket, DEAL_PROFIT) < class="num">0) {
          lotSize = NormalizeDouble(HistoryDealGetDouble(ticket, DEAL_VOLUME) * LOT_MULTIPLIER, LOT_PRECISION);
        } else {
          lotSize = BASE_LOT;
        }
      }
    }
  }
  break;
}
逐行拆解:case 2 进入亏损加仓分支;lastClose 初始为 0 用来记录最新平仓时间。HistorySelect 拉取前 24 小时(86400 秒)的成交。循环从最新成交倒序,取 ticket 后筛选平仓、同品种、同 MAGIC 的deal。若平仓时间更新则刷新 lastClose,利润小于 0 就按上一笔体积乘倍数并 NormalizeDouble 到 LOT_PRECISION 精度,否则重置为基础手数。 把 LOT_MULTIPLIER 设成 2.0 能在连亏时手数翻倍,但贵金属跳空可能让实际亏损超出历史体积推算,建议先调小 BASE_LOT 在 demo 跑一周观察最大回撤。

MQL5 / C++
case class="num">2: {
  class="type">class="kw">datetime lastClose = class="num">0;
  lotSize = BASE_LOT;
  HistorySelect(TimeCurrent() - class="num">24 * class="num">60 * class="num">60, TimeCurrent());
  for (class="type">int i = HistoryDealsTotal() - class="num">1; i >= class="num">0; i--) {
    class="type">ulong ticket = HistoryDealGetTicket(i);
    if (HistoryDealSelect(ticket) && HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT &&
        HistoryDealGetString(ticket, DEAL_SYMBOL) == _Symbol && HistoryDealGetInteger(ticket, DEAL_MAGIC) == MAGIC) {
      if (lastClose < HistoryDealGetInteger(ticket, DEAL_TIME)) {
        lastClose = (class="type">int)HistoryDealGetInteger(ticket, DEAL_TIME);
        if (HistoryDealGetDouble(ticket, DEAL_PROFIT) < class="num">0) {
          lotSize = NormalizeDouble(HistoryDealGetDouble(ticket, DEAL_VOLUME) * LOT_MULTIPLIER, LOT_PRECISION);
        } else {
          lotSize = BASE_LOT;
        }
      }
    }
  }
  break;
}

◍ 用代码清点当前图表上的活跃持仓

在 EA 里实时掌握自己下了几单、各单手数多少,是网格或加仓策略避免超额敞口的前提。下面两段函数只统计当前图表品种(_Symbol)且 Magic 号匹配的头寸,不会误把手动单或其他品种单算进来。 CountActiveOrders 从 PositionsTotal()-1 倒序遍历,遇到买或卖持仓就 count++,最后返回整数。GetActiveLotSizes 则把对应持仓的手数塞进数组,用 ArraySort 升序排好,最多取前 3 个格式化为字符串;若 activeOrders 为 0 直接回传 [Waiting]。 实盘接 MT5 时把 MAGIC 和 LOT_PRECISION 换成你自己的常量即可。外汇与贵金属波动剧烈、杠杆风险高,这类计数逻辑只解决“知道下了多少”,不预示盈亏。

MQL5 / C++
class="type">int CountActiveOrders() {
   class="type">int count = class="num">0;
   for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
      class="type">ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
         if (GetPositionType() == POSITION_TYPE_BUY || GetPositionType() == POSITION_TYPE_SELL) {
            count++;
         }
      }
   }
   class="kw">return count;
}

class="type">class="kw">string GetActiveLotSizes() {
   if (activeOrders == class="num">0) {
      class="kw">return "[Waiting]";
   }
   class="type">class="kw">double lotSizes[];
   ArrayResize(lotSizes, activeOrders);
   class="type">int count = class="num">0;
   for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0 && count < activeOrders; i--) {
      class="type">ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
         lotSizes[count] = PositionGetDouble(POSITION_VOLUME);
         count++;
      }
   }
   ArraySort(lotSizes);
   class="type">class="kw">string result = "";
   class="type">int maxDisplay = (activeOrders > class="num">3) ? class="num">3 : activeOrders;
   for (class="type">int i = class="num">0; i < maxDisplay; i++) {
      if (i > class="num">0) result += ", ";
      result += DoubleToString(lotSizes[i], LOT_PRECISION);
   }
}

网格下单与止损止盈的容错写法

网格马丁策略在 MT5 实盘里最怕的是下单瞬间碰上服务器繁忙或交易上下文被锁,单子没挂上却以为挂上了,后面网格层级就乱了。下面这段 PlaceOrder 用最多 100 次重试来兜底,只对错误码 4、137、146、136 这类可恢复故障循环等待 5 秒再试,其余错误直接 break 退出,避免无意义死循环。 函数入口先按网格层级拼出注释串 "GridMart Scalper-" + 层级号,方便在终端历史里回溯每笔单属于第几层。SL 和 TP 不直接写死价格,而是交给 CalculateSL / CalculateTP 按 pipValue 推算,points 传 0 时返回 0 表示不挂对应止损或止盈。 CalculateSL 对买卖统一用 price - points * pipValue 处理(原文片段如此,卖单 SL 在上方属正常),CalculateTP 则分方向:买单调 price + points*pipValue,卖单调 price - points*pipValue。外汇与贵金属杠杆高,网格马丁在单边行情中浮亏可能快速放大,参数 STOP_LOSS_PIPS / TAKE_PROFIT_PIPS 需按品种波动亲自在策略测试器里校。

MQL5 / C++
class="type">int PlaceOrder(ENUM_ORDER_TYPE orderType, class="type">class="kw">double lot, class="type">class="kw">double price, class="type">class="kw">double slPrice, class="type">int gridLevel) {
  class="type">class="kw">double sl = CalculateSL(slPrice, STOP_LOSS_PIPS);
  class="type">class="kw">double tp = CalculateTP(price, TAKE_PROFIT_PIPS, orderType);
  class="type">int ticket = class="num">0;
  class="type">int retries = class="num">100;
  class="type">class="kw">string comment = "GridMart Scalper-" + IntegerToString(gridLevel);
  for (class="type">int i = class="num">0; i < retries; i++) {
    ticket = obj_Trade.PositionOpen(_Symbol, orderType, lot, price, sl, tp, comment);
    class="type">int error = GetLastError();
    if (error == class="num">0) break;
    if (!(error == class="num">4 || error == class="num">137 || error == class="num">146 || error == class="num">136)) break;
    Sleep(class="num">5000);
  }
  class="kw">return ticket;
}

class="type">class="kw">double CalculateSL(class="type">class="kw">double price, class="type">int points) {
  if (points == class="num">0) class="kw">return class="num">0;
  class="kw">return price - points * pipValue;
}

class="type">class="kw">double CalculateTP(class="type">class="kw">double price, class="type">int points, ENUM_ORDER_TYPE orderType) {
  if (points == class="num">0) class="kw">return class="num">0;
  if (orderType == ORDER_TYPE_BUY) class="kw">return price + points * pipValue;
  class="kw">return price - points * pipValue;
}

「抓取当前品种最近一单的开仓价」

做加仓逻辑或浮盈统计时,常需要拿到本 EA 在 _Symbol 上最近一笔多单 / 空单的开仓价。下面两个函数从持仓池倒序遍历,靠 ticket 数值大小判断「最新」,只认魔法码 MAGIC 匹配的仓位。 GetLatestBuyPrice 与 GetLatestSellPrice 结构对称:先用 PositionsTotal()-1 往下扫,PositionSelectByTicket 选中后比对符号、魔法码和持仓类型,若 ticket 大于已记录的 latestTicket 就覆盖 price。注意 ticket 是 ulong,强转 int 在极端大号下可能溢出,实盘跑之前最好在策略测试器里用多单连发验证一遍。 外汇与贵金属杠杆高,自动取价后若直接用于下单间距计算,遇点差跳变可能连发仓位过密,触发券商风控。建议把返回 price 为 0 的情况单独处理,别默认当成有效价位。

MQL5 / C++
class="type">class="kw">double GetLatestBuyPrice() {
   class="type">class="kw">double price = class="num">0;
   class="type">int latestTicket = class="num">0;
   for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
      class="type">ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC && GetPositionType() == POSITION_TYPE_BUY) {
         if ((class="type">int)ticket > latestTicket) {
            price = GetPositionOpenPrice();
            latestTicket = (class="type">int)ticket;
         }
      }
   }
   class="kw">return price;
}
class="type">class="kw">double GetLatestSellPrice() {
   class="type">class="kw">double price = class="num">0;
   class="type">int latestTicket = class="num">0;
   for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
      class="type">ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC && GetPositionType() == POSITION_TYPE_SELL) {
         if ((class="type">int)ticket > latestTicket) {
            price = GetPositionOpenPrice();
            latestTicket = (class="type">int)ticket;
         }
      }
   }
   class="kw">return price;
}
class="type">int IsTradingAllowed() {
   class=class="str">"cmt">//--- Always allow trading

◍ OnTick 里的仓位状态刷新逻辑

EA 的主循环落在 OnTick(),每次报价进来先抓 ask / bid 并归一化到当前品种精度,避免后续比较出现浮点尾巴。若仪表盘处于显示状态就顺手刷新,但真正交易判断前会用 IsTradingAllowed() 卡一道。 用 iTime(_Symbol,_Period,0) 取当前柱时间,和 lastBarTime 比对;相等就直接 return,这意味着策略只在「新柱成型」时跑一次,回测里能明显压低无谓的 tick 级运算。 新柱通过后先 CountActiveOrders() 统计活跃单数,为 0 就把 updateSLTP 复位,防止空仓期还去改止损止盈。随后把 hasBuyPosition / hasSellPosition 清零,倒序遍历 PositionsTotal(),靠 PositionSelectByTicket + 品种与 MAGIC 过滤,命中 BUY 就置 buy 旗、清 sell 旗并 break,命中 SELL 同理。 这套旗标给后续加仓或反手判断提供即时状态。外汇与贵金属杠杆高,MT5 里把 MAGIC 改成自己编号再挂模拟盘验证旗标翻转是否符合预期。

MQL5 / C++
class="type">void OnTick() {
  class=class="str">"cmt">//--- Get current ask price
  class="type">class="kw">double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);
  class=class="str">"cmt">//--- Get current bid price
  class="type">class="kw">double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);
  class=class="str">"cmt">//--- Update dashboard if visible
  if (dashboardVisible) UpdateDashboard();
  class=class="str">"cmt">//--- Check if trading is allowed
  if (IsTradingAllowed()) {
    class=class="str">"cmt">//--- Get current bar time
    class="type">class="kw">datetime currentBarTime = iTime(_Symbol, _Period, class="num">0);
    class=class="str">"cmt">//--- Exit if the bar hasn’t changed
    if (lastBarTime == currentBarTime) class="kw">return;
    class=class="str">"cmt">//--- Update last bar time
    lastBarTime = currentBarTime;
    class=class="str">"cmt">//--- Count active orders
    activeOrders = CountActiveOrders();
    class=class="str">"cmt">//--- Reset SL/TP update flag if no active orders
    if (activeOrders == class="num">0) updateSLTP = false;
    class=class="str">"cmt">//--- Reset position flags
    hasBuyPosition = false;
    hasSellPosition = false;
    class=class="str">"cmt">//--- Iterate through open positions
    for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
      class=class="str">"cmt">//--- Get position ticket
      class="type">ulong ticket = PositionGetTicket(i);
      class=class="str">"cmt">//--- Select position by ticket
      if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
        class=class="str">"cmt">//--- Check for buy position
        if (GetPositionType() == POSITION_TYPE_BUY) {
          class=class="str">"cmt">//--- Set buy position flag
          hasBuyPosition = true;
          class=class="str">"cmt">//--- Clear sell position flag
          hasSellPosition = false;
          class=class="str">"cmt">//--- Exit loop after finding a position
          break;
        }
        class=class="str">"cmt">//--- Check for sell position
        else if (GetPositionType() == POSITION_TYPE_SELL) {
          class=class="str">"cmt">//--- Set sell position flag
          hasSellPosition = true;
          class=class="str">"cmt">//--- Clear buy position flag
          hasBuyPosition = false;
        }
      }
    }
  }
}

网格补单的信号与下单分支

这段逻辑处理的是网格策略里「什么时候该补下一单」以及「补单时怎么走卖出分支」。先清掉买入持仓标记并 break,意味着只要扫到一张买仓就退出遍历,避免重复统计。 当 activeOrders 在 1 到 MAX_GRID_LEVELS 之间时,脚本会抓取最近买价与卖价。若已持买仓且最新买价距 ask 超过 GRID_DISTANCE*pipValue,或已持卖仓且 bid 距最新卖价超过同阈值,就把 openNewTrade 置真——这就是网格间距触发的硬条件。 若当前无任何活跃订单(activeOrders<1),则买卖标记全清、直接允许开新仓。外汇与贵金属杠杆高,网格在单边行情里可能连续触发,实盘前务必在 MT5 策略测试器里把 MAX_GRID_LEVELS 调小做压力回测。 真要下单时,若 hasSellPosition 为真,先算卖单手数;手数大于 0 且 tradingEnabled 开启才调用 PlaceOrder。 ticket 小于 0 就打印错误并 return,不下续单。下面这段是卖出分支的代码片段。

MQL5 / C++
         hasBuyPosition = false;
         class=class="str">"cmt">//--- Exit loop after finding a position
         break;
            }
         }
      }
      class=class="str">"cmt">//--- Check conditions to open new trades
      if (activeOrders > class="num">0 && activeOrders <= MAX_GRID_LEVELS) {
         class=class="str">"cmt">//--- Get latest buy price
         latestBuyPrice = GetLatestBuyPrice();
         class=class="str">"cmt">//--- Get latest sell price
         latestSellPrice = GetLatestSellPrice();
         class=class="str">"cmt">//--- Check if a new buy trade is needed
         if (hasBuyPosition && latestBuyPrice - ask >= GRID_DISTANCE * pipValue) openNewTrade = true;
         class=class="str">"cmt">//--- Check if a new sell trade is needed
         if (hasSellPosition && bid - latestSellPrice >= GRID_DISTANCE * pipValue) openNewTrade = true;
      }
      class=class="str">"cmt">//--- Allow new trades if no active orders
      if (activeOrders < class="num">1) {
         class=class="str">"cmt">//--- Clear position flags
         hasBuyPosition = false;
         hasSellPosition = false;
         class=class="str">"cmt">//--- Signal to open a new trade
         openNewTrade = true;
      }
      class=class="str">"cmt">//--- Execute new trade if signaled
      if (openNewTrade) {
         class=class="str">"cmt">//--- Update latest buy price
         latestBuyPrice = GetLatestBuyPrice();
         class=class="str">"cmt">//--- Update latest sell price
         latestSellPrice = GetLatestSellPrice();
         class=class="str">"cmt">//--- Handle sell position
         if (hasSellPosition) {
            class=class="str">"cmt">//--- Calculate lot size for sell order
            calculatedLot = CalculateLotSize(POSITION_TYPE_SELL);
            class=class="str">"cmt">//--- Check if lot size is valid and trading is enabled
            if (calculatedLot > class="num">0 && tradingEnabled) {
               class=class="str">"cmt">//--- Place sell order
               class="type">int ticket = PlaceOrder(ORDER_TYPE_SELL, calculatedLot, bid, ask, activeOrders);
               class=class="str">"cmt">//--- Check for order placement errors
               if (ticket < class="num">0) {
                  class=class="str">"cmt">//--- Log error message
                  Print("Sell Order Error: ", GetLastError());
                  class="kw">return;
               }
               class=class="str">"cmt">//--- Update latest sell price
               latestSellPrice = GetLatestSellPrice();
               class=class="str">"cmt">//--- Clear new trade signal
               openNewTrade = false;

「无持仓时的新单触发逻辑」

当账户里既没有卖单也没有买单时,EA 会另走一套开仓判断。它先抓当前时间,要求小时数落在 START_HOUR 到 END_HOUR 之间,且 cycleCount 小于 MAX_CYCLES、CONTINUE_TRADING 为真、openNewTrade 为真、activeOrders 小于 1,才进入价格判断。 价格端用两根已收盘 K 线做比较:closePrev 取 PERIOD_CURRENT 周期偏移 2 的收盘价,closeCurrent 取偏移 1 的收盘价。若 closePrev > closeCurrent,视为空头倾向信号,调用 CalculateLotSize(POSITION_TYPE_SELL) 算手数。 手数大于 0 且 tradingEnabled 为真,才真正去 PlaceOrder 下卖单;任何一步不成立就不动。这套判断和前面有持仓时的处理完全隔离,所以你可以在 MT5 策略测试器里把 START_HOUR 设成 8、END_HOUR 设成 22,观察无仓位状态下是否只在两根阴线收敛时触发。 外汇与贵金属杠杆高,这类基于收盘价的信号在历史中胜率并不稳定,实盘前务必用 tick 级回测验证。

MQL5 / C++
class=class="str">"cmt">//--- Check conditions to open a new trade without existing positions
class="type">MqlDateTime timeStruct;
class=class="str">"cmt">//--- Get current time
TimeCurrent(timeStruct);
class=class="str">"cmt">//--- Verify trading hours, cycle limit, and new trade conditions
if (timeStruct.hour >= START_HOUR && timeStruct.hour < END_HOUR && cycleCount < MAX_CYCLES && CONTINUE_TRADING && openNewTrade && activeOrders < class="num">1) {
  class=class="str">"cmt">//--- Get previous bar close price
  class="type">class="kw">double closePrev = iClose(_Symbol, PERIOD_CURRENT, class="num">2);
  class=class="str">"cmt">//--- Get current bar close price
  class="type">class="kw">double closeCurrent = iClose(_Symbol, PERIOD_CURRENT, class="num">1);
  class=class="str">"cmt">//--- Check if no existing positions
  if (!hasSellPosition && !hasBuyPosition) {
    class=class="str">"cmt">//--- Check for bearish signal(previous close > current close)
    if (closePrev > closeCurrent) {
      class=class="str">"cmt">//--- Calculate lot size for sell order
      calculatedLot = CalculateLotSize(POSITION_TYPE_SELL);
      class=class="str">"cmt">//--- Check if lot size is valid and trading is enabled
      if (calculatedLot > class="num">0 && tradingEnabled) {
        class=class="str">"cmt">//--- Place sell order

◍ 双向挂单与加权均价回填的逻辑落点

这段逻辑紧接信号判断之后:当上一根收盘价高于当前收盘价时,走空单分支,用 bid 价调用 PlaceOrder 下卖单;若 ticket 小于 0 说明下单失败,直接 Print 出 GetLastError() 并 return,不再推进本轮循环。 下单成功后 cycleCount 自增 1,latestBuyPrice 被刷新为 GetLatestBuyPrice() 的返回值,同时 modifyPositions 置 true,告诉后续模块该去调已有仓位。 牛市分支对称:当 prev close <= current close,先算买量,lot>0 且 tradingEnabled 为真才用 ask 价下买单;失败同样打印错误并退出,成功则更新 latestSellPrice 并打 modify 标记。 所有仓位处理完,activeOrders 用 CountActiveOrders() 重算,weightedPrice 与 totalVolume 清零,随后倒序遍历 PositionsTotal(),只筛本符号且魔法码为 MAGIC 的仓:weightedPrice += 开仓价 × 成交量,totalVolume 同步累加,为下一轮加权均价提供分母。 外汇与贵金属杠杆高,这段代码若 MAGIC 或符号判断写错,可能把无关 EA 的仓位卷进加权计算,上 MT5 跑前先核对这几个过滤条件。

MQL5 / C++
class="type">int ticket = PlaceOrder(ORDER_TYPE_SELL, calculatedLot, bid, bid, activeOrders);
class=class="str">"cmt">//--- Check for order placement errors
if (ticket < class="num">0) {
   class=class="str">"cmt">//--- Log error message
   Print("Sell Order Error: ", GetLastError());
   class="kw">return;
}
class=class="str">"cmt">//--- Increment cycle count
cycleCount++;
class=class="str">"cmt">//--- Update latest buy price
latestBuyPrice = GetLatestBuyPrice();
class=class="str">"cmt">//--- Signal to modify positions
modifyPositions = true;
   }
   }
class=class="str">"cmt">//--- Check for bullish signal(previous close <= current close)
else {
   class=class="str">"cmt">//--- Calculate lot size for buy order
   calculatedLot = CalculateLotSize(POSITION_TYPE_BUY);
   class=class="str">"cmt">//--- Check if lot size is valid and trading is enabled
   if (calculatedLot > class="num">0 && tradingEnabled) {
      class=class="str">"cmt">//--- Place buy order
      class="type">int ticket = PlaceOrder(ORDER_TYPE_BUY, calculatedLot, ask, ask, activeOrders);
      class=class="str">"cmt">//--- Check for order placement errors
      if (ticket < class="num">0) {
         class=class="str">"cmt">//--- Log error message
         Print("Buy Order Error: ", GetLastError());
         class="kw">return;
      }
      class=class="str">"cmt">//--- Increment cycle count
      cycleCount++;
      class=class="str">"cmt">//--- Update latest sell price
      latestSellPrice = GetLatestSellPrice();
      class=class="str">"cmt">//--- Signal to modify positions
      modifyPositions = true;
   }
}
}
class=class="str">"cmt">//--- Update active orders count
activeOrders = CountActiveOrders();
class=class="str">"cmt">//--- Reset weighted price and total volume
weightedPrice = class="num">0;
totalVolume = class="num">0;
class=class="str">"cmt">//--- Calculate weighted price and total volume
for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
   class=class="str">"cmt">//--- Get position ticket
   class="type">ulong ticket = PositionGetTicket(i);
   class=class="str">"cmt">//--- Select position by ticket
   if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
      class=class="str">"cmt">//--- Accumulate weighted price(price * volume)
      weightedPrice += GetPositionOpenPrice() * PositionGetDouble(POSITION_VOLUME);
      class=class="str">"cmt">//--- Accumulate total volume

按加权价重挂止损止盈与回撤监控

加权价算出来后,先判断当前是否有活跃挂单(activeOrders > 0),有才用 NormalizeDouble 把 weightedPrice 除以 totalVolume 并规整到 _Digits 精度,避免浮点尾巴干扰后续计算。 modifyPositions 为真时,倒序遍历 PositionsTotal() 里的仓位,用 PositionSelectByTicket 锁定同品种同 MAGIC 的持仓。买仓把 TP 设在加权价 + TAKE_PROFIT_PIPS*pipValue、SL 设在加权价 - STOP_LOSS_PIPS*pipValue;卖仓反向,并置 updateSLTP = true 打标记。 真正改仓放在独立循环里:modifyPositions 且 updateSLTP 都满足才调用 obj_Trade.PositionModify(ticket, targetSL, targetTP),成功就把 modifyPositions 清 false,防止同根 K 线反复发修改指令。外汇与贵金属杠杆高,SL/TP 重挂逻辑若 pipValue 算错,可能瞬间放大回撤。 紧接着的 MonitorDailyDrawdown 开头只做了两件小事:声明 totalDayProfit = 0.0 承接当日盈亏累加,取当前时间待比对的日界。开 MT5 把这段接进 EA,重点看 pipValue 在你品种上是 Point 还是 Point*10,差一位止损距离就差十倍。

MQL5 / C++
totalVolume += PositionGetDouble(POSITION_VOLUME);
  }
}
class=class="str">"cmt">//--- Normalize weighted price if there are active orders
if (activeOrders > class="num">0) weightedPrice = NormalizeDouble(weightedPrice / totalVolume, _Digits);
class=class="str">"cmt">//--- Check if positions need SL/TP modification
if (modifyPositions) {
  class=class="str">"cmt">//--- Iterate through open positions
  for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
    class=class="str">"cmt">//--- Get position ticket
    class="type">ulong ticket = PositionGetTicket(i);
    class=class="str">"cmt">//--- Select position by ticket
    if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
      class=class="str">"cmt">//--- Handle buy positions
      if (GetPositionType() == POSITION_TYPE_BUY) {
        class=class="str">"cmt">//--- Set take-profit for buy position
        targetTP = weightedPrice + TAKE_PROFIT_PIPS * pipValue;
        class=class="str">"cmt">//--- Set stop-loss for buy position
        targetSL = weightedPrice - STOP_LOSS_PIPS * pipValue;
        class=class="str">"cmt">//--- Signal SL/TP update
        updateSLTP = true;
      }
      class=class="str">"cmt">//--- Handle sell positions
      else if (GetPositionType() == POSITION_TYPE_SELL) {
        class=class="str">"cmt">//--- Set take-profit for sell position
        targetTP = weightedPrice - TAKE_PROFIT_PIPS * pipValue;
        class=class="str">"cmt">//--- Set stop-loss for sell position
        targetSL = weightedPrice + STOP_LOSS_PIPS * pipValue;
        class=class="str">"cmt">//--- Signal SL/TP update
        updateSLTP = true;
      }
    }
  }
}
class=class="str">"cmt">//--- Apply SL/TP modifications if needed
if (modifyPositions && updateSLTP) {
  class=class="str">"cmt">//--- Iterate through open positions
  for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
    class=class="str">"cmt">//--- Get position ticket
    class="type">ulong ticket = PositionGetTicket(i);
    class=class="str">"cmt">//--- Select position by ticket
    if (PositionSelectByTicket(ticket) && GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
      class=class="str">"cmt">//--- Modify position with new SL/TP
      if (obj_Trade.PositionModify(ticket, targetSL, targetTP)) {
        class=class="str">"cmt">//--- Clear modification signal on success
        modifyPositions = false;
      }
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Monitor daily drawdown and control trading state                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void MonitorDailyDrawdown() {
  class=class="str">"cmt">//--- Initialize daily profit accumulator
  class="type">class="kw">double totalDayProfit = class="num">0.0;
  class=class="str">"cmt">//--- Get current time

「用当日盈亏百分比卡死爆仓线」

日内风控不能靠肉眼盯浮亏,得让 EA 自己算清从 0 点到现在到底回撤了多少。下面这段逻辑先以 TimeCurrent 取当前时间,再用 TimeToString+TIME_DATE 截出日期字符串,StringToTime 转回当天 0 点 datetime,to 设为加 1 天秒数(86400),作为每日重置边界。 每日首次越界时把 dailyResetTime 刷成 to,并把 GetAccountBalance 存进 dailyBalance 当作基准。随后 HistorySelect(start, end) 拉取当日历史,HistoryDealsTotal 拿到成交数,循环里只筛 DEAL_ENTRY_OUT 的平仓单,把 DEAL_PROFIT、DEAL_COMMISSION、DEAL_SWAP 三者相加得 latestDayProfit,累进 totalDayProfit。 startingBalance 用当前余额减掉 totalDayProfit 反推,dailyProfitOrDrawdown 就是 (totalDayProfit*100/startingBalance) 保留 2 位小数。若这个值小于等于 DRAWDOWN_LIMIT(例如你设 -2.00 代表回撤 2%),且 CLOSE_ON_DRAWDOWN 为真就 CloseAllPositions 清场,并把 tradingEnabled 置 false;否则恢复交易。外汇与贵金属杠杆高,这种硬截断能避免情绪加仓把日亏扩大成穿仓,但参数阈值需按品种波动自己回测。 CloseAllPositions 从 PositionsTotal()-1 倒序遍历,PositionGetTicket(i) 取ticket再 Select 定位,倒序是为了防止平单后数组前移漏处理。开 MT5 把 DRAWDOWN_LIMIT 先挂 -1.5 试一周,看触发频率是否和你的日内最大容忍吻合。

MQL5 / C++
  class="type">class="kw">datetime end = TimeCurrent();
  class=class="str">"cmt">//--- Get current date as class="type">class="kw">string
  class="type">class="kw">string sdate = TimeToString(TimeCurrent(), TIME_DATE);
  class=class="str">"cmt">//--- Convert date to class="type">class="kw">datetime (start of day)
  class="type">class="kw">datetime start = StringToTime(sdate);
  class=class="str">"cmt">//--- Set end of day(class="num">24 hours later)
  class="type">class="kw">datetime to = start + (class="num">1 * class="num">24 * class="num">60 * class="num">60);
  class=class="str">"cmt">//--- Check if daily reset is needed
  if (dailyResetTime < to) {
    class=class="str">"cmt">//--- Update reset time
    dailyResetTime = to;
    class=class="str">"cmt">//--- Store current balance as daily starting balance
    dailyBalance = GetAccountBalance();
  }
  class=class="str">"cmt">//--- Select trade history for the day
  HistorySelect(start, end);
  class=class="str">"cmt">//--- Get total number of deals
  class="type">int totalDeals = HistoryDealsTotal();
  class=class="str">"cmt">//--- Iterate through trade history
  for (class="type">int i = class="num">0; i < totalDeals; i++) {
    class=class="str">"cmt">//--- Get deal ticket
    class="type">ulong ticket = HistoryDealGetTicket(i);
    class=class="str">"cmt">//--- Check if deal is a position close
    if (HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT) {
      class=class="str">"cmt">//--- Calculate deal profit(including commission and swap)
      class="type">class="kw">double latestDayProfit = (HistoryDealGetDouble(ticket, DEAL_PROFIT) +
                                HistoryDealGetDouble(ticket, DEAL_COMMISSION) +
                                HistoryDealGetDouble(ticket, DEAL_SWAP));
      class=class="str">"cmt">//--- Accumulate daily profit
      totalDayProfit += latestDayProfit;
    }
  }
  class=class="str">"cmt">//--- Calculate starting balance for the day
  class="type">class="kw">double startingBalance = GetAccountBalance() - totalDayProfit;
  class=class="str">"cmt">//--- Calculate daily profit/drawdown percentage
  class="type">class="kw">double dailyProfitOrDrawdown = NormalizeDouble((totalDayProfit * class="num">100 / startingBalance), class="num">2);
  class=class="str">"cmt">//--- Check if drawdown limit is exceeded
  if (dailyProfitOrDrawdown <= DRAWDOWN_LIMIT) {
    class=class="str">"cmt">//--- Close all positions if configured
    if (CLOSE_ON_DRAWDOWN) CloseAllPositions();
    class=class="str">"cmt">//--- Disable trading
    tradingEnabled = false;
  } else {
    class=class="str">"cmt">//--- Enable trading
    tradingEnabled = true;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Close all open positions managed by the EA                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CloseAllPositions() {
  class=class="str">"cmt">//--- Iterate through open positions
  for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
    class=class="str">"cmt">//--- Get position ticket
    class="type">ulong ticket = PositionGetTicket(i);
    class=class="str">"cmt">//--- Select position by ticket

◍ 持仓归属判定与日回撤监控的收口逻辑

上面这段代码片段负责在订单成交后做一道安全闸:先用 ticket 定位持仓,再核对品种与魔术码,避免把别的 EA 或手动单误平。PositionSelectByTicket 返回成功才进入内层判断,GetPositionSymbol 和 GetPositionMagic 双条件过滤,最后才交给 obj_Trade.PositionClose 执行平仓。 日终风控那一行更直白——ENABLE_DAILY_DRAWDOWN 开关打开时,直接调 MonitorDailyDrawdown 去盯账户当日浮亏。外汇与贵金属杠杆高,这类硬切断能在极端行情里把本金暴露锁住,但参数阈值必须自己回测定,没有通用安全值。 把这段代码原样丢进 MT5 的策略测试器,配合日志打印 ticket 与 MAGIC,你能立刻验证是否存在跨 EA 误平;若回测里出现非本 EA 的平仓记录,说明过滤顺序或魔术码分配有漏洞。

MQL5 / C++
if (ticket > class="num">0 && PositionSelectByTicket(ticket)) {
      class=class="str">"cmt">//--- Check if position belongs to this EA
      if (GetPositionSymbol() == _Symbol && GetPositionMagic() == MAGIC) {
         class=class="str">"cmt">//--- Close the position
         obj_Trade.PositionClose(ticket);
      }
   }
}
class=class="str">"cmt">//--- Monitor daily drawdown if enabled
if (ENABLE_DAILY_DRAWDOWN) MonitorDailyDrawdown();

把这套框架当作可拆装的试验台

网格叠加马丁格尔的短线 EA 已经跑通,核心不在‘圣杯’,而在于那块动态仪表盘:点差、浮动盈亏、当前手数实时可见,参数面板允许你随时改网格步长和加仓倍数。外汇与贵金属杠杆高、跳空频繁,这类策略在实盘前必须拿 MT5 历史数据做回测,尤其观察极端行情下回撤是否超出承受线。 有读者在评论区指出 CalculateSL 漏了卖方处理,作者已确认并收下修正版——这提醒你下载 GridMart_Scalper_MT5_EA.mq5 后,先翻止损计算段,别直接挂真仓。 把它当底层骨架就好:接别的入场逻辑、换过滤条件都行。算法交易这条路,能从一套可改的代码起步,比看一百篇抽象教程更实在。

常见问题

用当前权益减期初余额得浮亏浮盈,再按正负映射红绿;代码里通常先算差值再走颜色分支,直接抄面板绘制函数即可验证。
按品种或魔术码分组循环,先累计平仓次数与盈利笔数,再文本输出到对应 y 坐标;注意每行高度固定避免重叠。
可以。把面板和ea日志接给小布,它能比对亏损后加仓的手数分支与实际成交手数,标出递推偏移的异常行。
在鼠标事件里取点击坐标,若落在面板矩形且 y 处于标题栏区间才置拖拽标志;其余区域忽略,避免误移。
用上笔亏损额乘系数递推手数,并加账户权益上限判断;代码里建议把系数和上限都做成输入参数方便回测。