持仓限制控制器 - 追踪止盈版
自动风控系统,支持手数限制、追踪止盈、固定止损、保本功能
相对上一版改了:移动止盈改成追踪止盈;刚才这份代码用 MetaEditor 编译没通过,报了 8 个错误: 第 1 行第 1 列:unexpected end of program 出错的那行代码:// 第 863 行第 1 列:unbalanced parentheses 出错的那行代码:{ 第 142 行第 7 列:undeclared identifier 'UpdatePanel' 出错的那行代码:UpdatePanel(); 第 142 行第 19 列:')' - expression expected 出错的那行代码:UpdatePanel(); 第 521 行第 10 列:undeclared
//
// 基于 XAUOF 社区作品 #33《持仓限制控制器 - 自动风控系统》(作者 taiyu1983)二次创作
// 本版改动:将所有"移动止盈"术语统一改为"追踪止盈"
#property copyright "XAUOF 指标工坊"
#property version "1.50"
#property strict
#include <Trade\Trade.mqh>
//--- 可调参数
input double MaxTotalLots = 10.0; // 最大总手数限制(0=不限制)
input bool AutoCloseLimitExcess = true; // 超限自动平仓(true=自动平,false=仅警告)
input int TrailingStopPoints = 300; // 追踪止盈点数(0=不启用)
input int FixedStopLossPoints = 500; // 固定止损点数(用于一键设置)
input int BreakEvenPoints = 200; // 挂单价止盈点数(盈利超过此值移动到开仓价)
input int InitPanelX = 20; // 面板初始左边距(像素)
input int InitPanelY = 30; // 面板初始上边距(像素)
input int PanelWidth = 340; // 面板宽度(像素)
input int PanelHeight = 360; // 面板高度(像素)
input color PanelBgColor = clrDarkSlateGray; // 面板背景色
input color TitleBgColor = clrNavy; // 标题栏背景色
input color TextColor = clrWhite; // 文字颜色
input color BuyColor = clrLime; // 多头数据颜色
input color SellColor = clrRed; // 空头数据颜色
input color ProfitColor = clrGold; // 盈亏数据颜色
input int FontSize = 10; // 字体大小
input int RefreshMS = 500; // 刷新间隔(毫秒)
//--- 全局变量
CTrade trade;
uint lastRefreshTime = 0;
datetime lastCheckTime = 0;
bool trailingStopEnabled = false; // 追踪止盈开关
// 面板位置和大小
int panelX, panelY;
int panelW, panelH;
// 拖动相关变量
bool isDragging = false;
bool isResizing = false;
int dragStartX, dragStartY;
int resizeStartX, resizeStartY;
int resizeStartW, resizeStartH;
// 界面对象名称
string panelBg = "StatPanelBg";
string panelTitle = "StatPanelTitle";
string resizeHandle = "StatResizeHandle";
string labelBuyLots = "LabelBuyLots";
string labelBuyCount = "LabelBuyCount";
string labelSellLots = "LabelSellLots";
string labelSellCount = "LabelSellCount";
string labelTotalLots = "LabelTotalLots";
string labelTotalProfit = "LabelTotalProfit";
string labelLimitWarning = "LabelLimitWarning";
string labelSeparator1 = "LabelSeparator1";
string labelSeparator2 = "LabelSeparator2";
string labelSeparator3 = "LabelSeparator3";
string btnCloseAll = "BtnCloseAll";
string btnCloseBuy = "BtnCloseBuy";
string btnCloseSell = "BtnCloseSell";
string btnTrailing = "BtnTrailing";
string btnFixedSL = "BtnFixedSL";
string btnBreakEven = "BtnBreakEven";
//+------------------------------------------------------------------+
//| 初始化 |
//+------------------------------------------------------------------+
int OnInit()
{
// 初始化面板位置和大小
panelX = InitPanelX;
panelY = InitPanelY;
panelW = PanelWidth;
panelH = PanelHeight;
// 创建面板界面
CreatePanel();
// 启用图表事件
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
Print("======================================");
Print("持仓限制控制器已启动(追踪止盈版)");
if(MaxTotalLots > 0)
{
Print("总手数限制: ", MaxTotalLots, " 手");
Print("超限处理: ", AutoCloseLimitExcess ? "自动平仓" : "仅警告");
}
else
{
Print("总手数限制: 无限制");
}
Print("追踪止盈: ", TrailingStopPoints, " 点");
Print("固定止损: ", FixedStopLossPoints, " 点");
Print("挂单价止盈触发: ", BreakEvenPoints, " 点");
Print("提示:可拖动标题栏移动面板");
Print("======================================");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| 清理 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 删除所有界面对象
ObjectDelete(0, panelBg);
ObjectDelete(0, panelTitle);
ObjectDelete(0, resizeHandle);
ObjectDelete(0, labelBuyLots);
ObjectDelete(0, labelBuyCount);
ObjectDelete(0, labelSellLots);
ObjectDelete(0, labelSellCount);
ObjectDelete(0, labelTotalLots);
ObjectDelete(0, labelTotalProfit);
ObjectDelete(0, labelLimitWarning);
ObjectDelete(0, labelSeparator1);
ObjectDelete(0, labelSeparator2);
ObjectDelete(0, labelSeparator3);
ObjectDelete(0, btnCloseAll);
ObjectDelete(0, btnCloseBuy);
ObjectDelete(0, btnCloseSell);
ObjectDelete(0, btnTrailing);
ObjectDelete(0, btnFixedSL);
ObjectDelete(0, btnBreakEven);
ChartRedraw();
}
//+------------------------------------------------------------------+
//| 每个 Tick 更新 |
//+------------------------------------------------------------------+
void OnTick()
{
// 按时间间隔刷新面板
if(GetTickCount() - lastRefreshTime > (uint)RefreshMS)
{
UpdatePanel();
lastRefreshTime = GetTickCount();
}
// 每秒检查一次手数限制
datetime currentTime = TimeCurrent();
if(currentTime > lastCheckTime)
{
CheckPositionLimit();
lastCheckTime = currentTime;
}
// 如果启用了追踪止盈,执行追踪止盈逻辑
if(trailingStopEnabled && TrailingStopPoints > 0)
{
ProcessTrailingStop();
}
}
//+------------------------------------------------------------------+
//| 交易事件监控(关键:在这里拦截新订单) |
//+------------------------------------------------------------------+
void OnTrade()
{
// 立即检查手数限制
CheckPositionLimit();
}
//+------------------------------------------------------------------+
//| 检查持仓手数限制 |
//+------------------------------------------------------------------+
void CheckPositionLimit()
{
// 如果没有设置限制,直接返回
if(MaxTotalLots <= 0) return;
// 统计当前总手数
double totalLots = 0;
int totalCount = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
totalLots += PositionGetDouble(POSITION_VOLUME);
totalCount++;
}
// 判断是否超过限制
if(totalLots > MaxTotalLots)
{
double excessLots = totalLots - MaxTotalLots;
Print("⚠ 警告:总手数 ", DoubleToString(totalLots, 2),
" 超过限制 ", DoubleToString(MaxTotalLots, 2),
" 手,超出 ", DoubleToString(excessLots, 2), " 手");
// 如果启用自动平仓
if(AutoCloseLimitExcess)
{
Print("开始自动平掉超限订单...");
CloseExcessPositions(excessLots);
}
else
{
Print("提示:请手动平仓或调整限制参数");
}
}
}
//+------------------------------------------------------------------+
//| 平掉超限订单(按开仓时间从新到旧) |
//+------------------------------------------------------------------+
void CloseExcessPositions(double excessLots)
{
// 创建订单列表(按开仓时间排序)
struct PositionInfo
{
ulong ticket;
datetime openTime;
double lots;
};
PositionInfo positions[];
int count = 0;
// 收集所有持仓
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
count++;
ArrayResize(positions, count);
positions[count-1].ticket = ticket;
positions[count-1].openTime = (datetime)PositionGetInteger(POSITION_TIME);
positions[count-1].lots = PositionGetDouble(POSITION_VOLUME);
}
// 按开仓时间从新到旧排序(最新的订单先平)
for(int i = 0; i < count - 1; i++)
{
for(int j = i + 1; j < count; j++)
{
if(positions[i].openTime < positions[j].openTime)
{
PositionInfo temp = positions[i];
positions[i] = positions[j];
positions[j] = temp;
}
}
}
// 从最新的订单开始平仓
double closedLots = 0;
int closedCount = 0;
for(int i = 0; i < count; i++)
{
if(closedLots >= excessLots) break;
ulong ticket = positions[i].ticket;
double lots = positions[i].lots;
Print("正在平仓订单 #", ticket, " (", DoubleToString(lots, 2), " 手)...");
if(trade.PositionClose(ticket))
{
closedLots += lots;
closedCount++;
Print("✓ 订单 #", ticket, " 已平仓");
}
else
{
Print("✗ 订单 #", ticket, " 平仓失败: ", trade.ResultRetcodeDescription());
}
}
Print("自动平仓完成:平掉 ", closedCount, " 单,共 ", DoubleToString(closedLots, 2), " 手");
}
//+------------------------------------------------------------------+
//| 处理追踪止盈 |
//+------------------------------------------------------------------+
void ProcessTrailingStop()
{
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double currentPrice = (type == POSITION_TYPE_BUY) ?
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double newSL = 0;
bool needUpdate = false;
if(type == POSITION_TYPE_BUY)
{
// 多单:当前价 - 追踪止盈点数
newSL = currentPrice - TrailingStopPoints * point;
// 新止损必须高于开仓价且高于当前止损
if(newSL > openPrice && (currentSL == 0 || newSL > currentSL))
{
needUpdate = true;
}
}
else if(type == POSITION_TYPE_SELL)
{
// 空单:当前价 + 追踪止盈点数
newSL = currentPrice + TrailingStopPoints * point;
// 新止损必须低于开仓价且低于当前止损(或当前止损为0)
if(newSL < openPrice && (currentSL == 0 || newSL < currentSL))
{
needUpdate = true;
}
}
// 执行修改
if(needUpdate)
{
newSL = NormalizeDouble(newSL, digits);
if(trade.PositionModify(ticket, newSL, currentTP))
{
Print("追踪止盈:订单 #", ticket, " 止损已更新至 ", DoubleToString(newSL, digits));
}
}
}
}
//+------------------------------------------------------------------+
//| 设置固定止损 |
//+------------------------------------------------------------------+
void SetFixedStopLoss()
{
if(FixedStopLossPoints <= 0)
{
MessageBox("固定止损点数必须大于0", "参数错误", MB_OK | MB_ICONERROR);
return;
}
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
int updated = 0, failed = 0;
Print("======================================");
Print("开始设置固定止损 ", FixedStopLossPoints, " 点...");
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentTP = PositionGetDouble(POSITION_TP);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double newSL = 0;
if(type == POSITION_TYPE_BUY)
{
newSL = openPrice - FixedStopLossPoints * point;
}
else if(type == POSITION_TYPE_SELL)
{
newSL = openPrice + FixedStopLossPoints * point;
}
newSL = NormalizeDouble(newSL, digits);
if(trade.PositionModify(ticket, newSL, currentTP))
{
updated++;
Print("✓ 订单 #", ticket, " 止损已设置为 ", DoubleToString(newSL, digits));
}
else
{
failed++;
Print("✗ 订单 #", ticket, " 设置失败: ", trade.ResultRetcodeDescription());
}
}
string msg = "固定止损设置完成\n\n成功: " + IntegerToString(updated) + " 单";
if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
Print("固定止损设置完成:成功 ", updated, " 单,失败 ", failed, " 单");
Print("======================================");
MessageBox(msg, "固定止损结果", MB_OK | MB_ICONINFORMATION);
}
//+------------------------------------------------------------------+
//| 设置挂单价止盈(保本) |
//+------------------------------------------------------------------+
void SetBreakEven()
{
if(BreakEvenPoints <= 0)
{
MessageBox("挂单价止盈点数必须大于0", "参数错误", MB_OK | MB_ICONERROR);
return;
}
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
int updated = 0, skipped = 0, failed = 0;
Print("======================================");
Print("开始设置挂单价止盈,触发阈值 ", BreakEvenPoints, " 点...");
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double currentPrice = (type == POSITION_TYPE_BUY) ?
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double profitPoints = 0;
bool canSetBreakEven = false;
if(type == POSITION_TYPE_BUY)
{
profitPoints = (currentPrice - openPrice) / point;
// 盈利超过阈值,且当前止损低于开仓价(或没设止损)
if(profitPoints >= BreakEvenPoints && (currentSL == 0 || currentSL < openPrice))
{
canSetBreakEven = true;
}
}
else if(type == POSITION_TYPE_SELL)
{
profitPoints = (openPrice - currentPrice) / point;
// 盈利超过阈值,且当前止损高于开仓价(或没设止损)
if(profitPoints >= BreakEvenPoints && (currentSL == 0 || currentSL > openPrice))
{
canSetBreakEven = true;
}
}
if(canSetBreakEven)
{
double newSL = NormalizeDouble(openPrice, digits);
if(trade.PositionModify(ticket, newSL, currentTP))
{
updated++;
Print("✓ 订单 #", ticket, " 止损已移至开仓价 ", DoubleToString(newSL, digits),
" (盈利 ", DoubleToString(profitPoints, 1), " 点)");
}
else
{
failed++;
Print("✗ 订单 #", ticket, " 设置失败: ", trade.ResultRetcodeDescription());
}
}
else
{
skipped++;
Print("跳过订单 #", ticket, " (盈利 ", DoubleToString(profitPoints, 1), " 点,未达阈值或已设置)");
}
}
string msg = "挂单价止盈设置完成\n\n";
msg += "成功: " + IntegerToString(updated) + " 单\n";
msg += "跳过: " + IntegerToString(skipped) + " 单";
if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
Print("挂单价止盈完成:成功 ", updated, " 单,跳过 ", skipped, " 单,失败 ", failed, " 单");
Print("======================================");
MessageBox(msg, "挂单价止盈结果", MB_OK | MB_ICONINFORMATION);
}
//+------------------------------------------------------------------+
//| 图表事件处理 |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
// 鼠标移动事件 - 处理拖动
if(id == CHARTEVENT_MOUSE_MOVE)
{
int x = (int)lparam;
int y = (int)dparam;
int mouseState = (int)sparam; // 0=无按钮, 1=左键按下
// 正在拖动面板
if(isDragging && mouseState == 1)
{
panelX = x - dragStartX;
panelY = y - dragStartY;
UpdatePanelPosition();
}
else if(isDragging && mouseState == 0)
{
isDragging = false;
}
// 正在调整大小
if(isResizing && mouseState == 1)
{
panelW = resizeStartW + (x - resizeStartX);
panelH = resizeStartH + (y - resizeStartY);
// 限制最小尺寸
if(panelW < 280) panelW = 280;
if(panelH < 320) panelH = 320;
UpdatePanelSize();
}
else if(isResizing && mouseState == 0)
{
isResizing = false;
}
}
// 对象点击事件
if(id == CHARTEVENT_OBJECT_CLICK)
{
// 点击标题栏开始拖动
if(sparam == panelTitle)
{
isDragging = true;
dragStartX = (int)lparam - panelX;
dragStartY = (int)dparam - panelY;
ObjectSetInteger(0, panelTitle, OBJPROP_STATE, false);
return;
}
// 点击调整大小手柄
if(sparam == resizeHandle)
{
isResizing = true;
resizeStartX = (int)lparam;
resizeStartY = (int)dparam;
resizeStartW = panelW;
resizeStartH = panelH;
ObjectSetInteger(0, resizeHandle, OBJPROP_STATE, false);
return;
}
// 一键平仓按钮
if(sparam == btnCloseAll)
{
if(MessageBox("确定要平掉当前品种所有持仓吗?", "一键平仓", MB_YESNO | MB_ICONWARNING) == IDYES)
{
CloseAllPositions();
}
ObjectSetInteger(0, btnCloseAll, OBJPROP_STATE, false);
}
// 一键平多按钮
if(sparam == btnCloseBuy)
{
if(MessageBox("确定要平掉所有多头持仓吗?", "一键平多", MB_YESNO | MB_ICONQUESTION) == IDYES)
{
ClosePositionsByType(POSITION_TYPE_BUY);
}
ObjectSetInteger(0, btnCloseBuy, OBJPROP_STATE, false);
}
// 一键平空按钮
if(sparam == btnCloseSell)
{
if(MessageBox("确定要平掉所有空头持仓吗?", "一键平空", MB_YESNO | MB_ICONQUESTION) == IDYES)
{
ClosePositionsByType(POSITION_TYPE_SELL);
}
ObjectSetInteger(0, btnCloseSell, OBJPROP_STATE, false);
}
// 追踪止盈按钮
if(sparam == btnTrailing)
{
trailingStopEnabled = !trailingStopEnabled;
ObjectSetInteger(0, btnTrailing, OBJPROP_BGCOLOR, trailingStopEnabled ? clrLimeGreen : clrGreen);
ObjectSetString(0, btnTrailing, OBJPROP_TEXT, trailingStopEnabled ? "追踪止盈 ●" : "追踪止盈");
Print("追踪止盈 ", trailingStopEnabled ? "已启用" : "已停用");
ObjectSetInteger(0, btnTrailing, OBJPROP_STATE, false);
}
// 固定止损按钮
if(sparam == btnFixedSL)
{
if(MessageBox("为所有持仓设置固定止损 " + IntegerToString(FixedStopLossPoints) + " 点?", "固定止损", MB_YESNO | MB_ICONQUESTION) == IDYES)
{
SetFixedStopLoss();
}
ObjectSetInteger(0, btnFixedSL, OBJPROP_STATE, false);
}
// 挂单价止盈按钮
if(sparam == btnBreakEven)
{
if(MessageBox("将盈利订单的止损移动到开仓价(保本)?", "挂单价止盈", MB_YESNO | MB_ICONQUESTION) == IDYES)
{
SetBreakEven();
}
ObjectSetInteger(0, btnBreakEven, OBJPROP_STATE, false);
}
ChartRedraw();
}
}
//+------------------------------------------------------------------+
//| 关闭所有持仓 |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
int closed = 0, failed = 0;
Print("======================================");
Print("开始执行一键平仓...");
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double lots = PositionGetDouble(POSITION_VOLUME);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
string typeName = (type == POSITION_TYPE_BUY) ? "多" : "空";
if(trade.PositionClose(ticket))
{
closed++;
Print("✓ 已平仓 ", typeName, "单 #", ticket, " (", DoubleToString(lots, 2), " 手)");
}
else
{
failed++;
Print("✗ 平仓失败 ", typeName, "单 #", ticket, ": ", trade.ResultRetcodeDescription());
}
}
string msg = "一键平仓完成\n\n成功: " + IntegerToString(closed) + " 单";
if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
Print("一键平仓完成:成功 ", closed, " 单,失败 ", failed, " 单");
Print("======================================");
MessageBox(msg, "一键平仓结果", MB_OK | MB_ICONINFORMATION);
}
//+------------------------------------------------------------------+
//| 按类型关闭持仓(多或空) |
//+------------------------------------------------------------------+
void ClosePositionsByType(ENUM_POSITION_TYPE posType)
{
int closed = 0, failed = 0;
string typeName = (posType == POSITION_TYPE_BUY) ? "多" : "空";
Print("======================================");
Print("开始执行一键平", typeName, "...");
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if(type != posType) continue;
double lots = PositionGetDouble(POSITION_VOLUME);
if(trade.PositionClose(ticket))
{
closed++;
Print("✓ 已平仓 ", typeName, "单 #", ticket, " (", DoubleToString(lots, 2), " 手)");
}
else
{
failed++;
Print("✗ 平仓失败 ", typeName, "单 #", ticket, ": ", trade.ResultRetcodeDescription());
}
}
string msg = "一键平" + typeName + "完成\n\n成功: " + IntegerToString(closed) + " 单";
if(failed > 0) msg += "\n失败: " + IntegerToString(failed) + " 单";
Print("一键平", typeName, "完成:成功 ", closed, " 单,失败 ", failed, " 单");
Print("======================================");
MessageBox(msg, "一键平" + typeName + "结果", MB_OK | MB_ICONINFORMATION);
}
//+------------------------------------------------------------------+
//| 创建面板 |
//+------------------------------------------------------------------+
void CreatePanel()
{
// 创建背景矩形
ObjectCreate(0, panelBg, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, panelBg, OBJPROP_XDISTANCE, panelX);
ObjectSetInteger(0, panelBg, OBJPROP_YDISTANCE, panelY);
ObjectSetInteger(0, panelBg, OBJPROP_XSIZE, panelW);
ObjectSetInteger(0, panelBg, OBJPROP_YSIZE, panelH);
ObjectSetInteger(0, panelBg, OBJPROP_BGCOLOR, PanelBgColor);
ObjectSetInteger(0, panelBg, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, panelBg, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, panelBg, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, panelBg, OBJPROP_BACK, false);
// 创建标题栏(可拖动)
ObjectCreate(0, panelTitle, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, panelTitle, OBJPROP_XDISTANCE, panelX);
ObjectSetInteger(0, panelTitle, OBJPROP_YDISTANCE, panelY);
ObjectSetInteger(0, panelTitle, OBJPROP_XSIZE, panelW);
ObjectSetInteger(0, panelTitle, OBJPROP_YSIZE, 30);
ObjectSetString(0, panelTitle, OBJPROP_TEXT, "持仓限制控制器(追踪止盈版)");
ObjectSetInteger(0, panelTitle, OBJPROP_FONTSIZE, FontSize);
ObjectSetInteger(0, panelTitle, OBJPROP_COLOR, TextColor);
ObjectSetInteger(0, panelTitle, OBJPROP_BGCOLOR, TitleBgColor);
ObjectSetInteger(0, panelTitle, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, panelTitle, OBJPROP_CORNER, CORNER_LEFT_UPPER);
// 创建调整大小手柄
ObjectCreate(0, resizeHandle, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, resizeHandle, OBJPROP_XDISTANCE, panelX + panelW - 20);
ObjectSetInteger(0, resizeHandle, OBJPROP_YDISTANCE, panelY + panelH - 20);
ObjectSetInteger(0, resizeHandle, OBJPROP_XSIZE, 18);
ObjectSetInteger(0, resizeHandle, OBJPROP_YSIZE, 18);
ObjectSetString(0, resizeHandle, OBJPROP_TEXT, "⊡");
ObjectSetInteger(0, resizeHandle, OBJPROP_FONTSIZE, 10);
ObjectSetInteger(0, resizeHandle, OBJPROP_COLOR, clrSilver);
ObjectSetInteger(0, resizeHandle, OBJPROP_BGCOLOR, clrDarkSlateGray);
ObjectSetInteger(0, resizeHandle, OBJPROP_CORNER, CORNER_LEFT_UPPER);
// 创建分隔线
ObjectCreate(0, labelSeparator1, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, labelSeparator1, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSeparator1, OBJPROP_YDISTANCE, panelY + 35);
ObjectSetString(0, labelSeparator1, OBJPROP_TEXT, "──────────────────────────");
ObjectSetInteger(0, labelSeparator1, OBJPROP_FONTSIZE, FontSize);
ObjectSetInteger(0, labelSeparator1, OBJPROP_COLOR, clrGray);
ObjectSetInteger(0, labelSeparator1, OBJPROP_CORNER, CORNER_LEFT_UPPER);
// 创建统计标签
CreateLabel(labelBuyLots, 10, 50, "多头手数: 0.00", BuyColor);
CreateLabel(labelBuyCount, 10, 70, "多头数量: 0", BuyColor);
CreateLabel(labelSellLots, 10, 90, "空头手数: 0.00", SellColor);
CreateLabel(labelSellCount, 10, 110, "空头数量: 0", SellColor);
ObjectCreate(0, labelSeparator2, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, labelSeparator2, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSeparator2, OBJPROP_YDISTANCE, panelY + 125);
ObjectSetString(0, labelSeparator2, OBJPROP_TEXT, "──────────────────────────");
ObjectSetInteger(0, labelSeparator2, OBJPROP_FONTSIZE, FontSize);
ObjectSetInteger(0, labelSeparator2, OBJPROP_COLOR, clrGray);
ObjectSetInteger(0, labelSeparator2, OBJPROP_CORNER, CORNER_LEFT_UPPER);
CreateLabel(labelTotalLots, 10, 140, "总手数: 0.00", TextColor);
CreateLabel(labelTotalProfit, 10, 160, "总盈亏: $0.00", ProfitColor);
CreateLabel(labelLimitWarning, 10, 180, "", clrYellow);
ObjectCreate(0, labelSeparator3, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, labelSeparator3, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSeparator3, OBJPROP_YDISTANCE, panelY + 195);
ObjectSetString(0, labelSeparator3, OBJPROP_TEXT, "──────────────────────────");
ObjectSetInteger(0, labelSeparator3, OBJPROP_FONTSIZE, FontSize);
ObjectSetInteger(0, labelSeparator3, OBJPROP_COLOR, clrGray);
ObjectSetInteger(0, labelSeparator3, OBJPROP_CORNER, CORNER_LEFT_UPPER);
// 创建按钮
CreateButton(btnCloseAll, 10, 210, panelW - 20, 35, "一键平仓", clrMaroon);
CreateButton(btnCloseBuy, 10, 250, (panelW - 30) / 2, 30, "一键平多", clrDarkGreen);
CreateButton(btnCloseSell, 20 + (panelW - 30) / 2, 250, (panelW - 30) / 2, 30, "一键平空", clrDarkRed);
CreateButton(btnTrailing, 10, 285, panelW - 20, 30, "追踪止盈", clrGreen);
CreateButton(btnFixedSL, 10, 320, panelW - 20, 30, "固定止损", clrDarkBlue);
CreateButton(btnBreakEven, 10, 355, panelW - 20, 30, "挂单价止盈", clrOlive);
ChartRedraw();
}
//+------------------------------------------------------------------+
//| 创建标签辅助函数 |
//+------------------------------------------------------------------+
void CreateLabel(string name, int offsetX, int offsetY, string text, color clr)
{
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, panelX + offsetX);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, panelY + offsetY);
ObjectSetString(0, name, OBJPROP_TEXT, text);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, FontSize);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
}
//+------------------------------------------------------------------+
//| 创建按钮辅助函数 |
//+------------------------------------------------------------------+
void CreateButton(string name, int offsetX, int offsetY, int width, int height, string text, color bgColor)
{
ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, panelX + offsetX);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, panelY + offsetY);
ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
ObjectSetString(0, name, OBJPROP_TEXT, text);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, FontSize);
ObjectSetInteger(0, name, OBJPROP_COLOR, TextColor);
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bgColor);
ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_RAISED);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
}
//+------------------------------------------------------------------+
//| 更新面板数据显示 |
//+------------------------------------------------------------------+
void UpdatePanel()
{
// 统计持仓信息
double buyLots = 0, sellLots = 0, totalProfit = 0;
int buyCount = 0, sellCount = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket <= 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
double lots = PositionGetDouble(POSITION_VOLUME);
double profit = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
totalProfit += profit;
if(type == POSITION_TYPE_BUY)
{
buyLots += lots;
buyCount++;
}
else if(type == POSITION_TYPE_SELL)
{
sellLots += lots;
sellCount++;
}
}
double totalLots = buyLots + sellLots;
// 更新标签文本
ObjectSetString(0, labelBuyLots, OBJPROP_TEXT, "多头手数: " + DoubleToString(buyLots, 2));
ObjectSetString(0, labelBuyCount, OBJPROP_TEXT, "多头数量: " + IntegerToString(buyCount));
ObjectSetString(0, labelSellLots, OBJPROP_TEXT, "空头手数: " + DoubleToString(sellLots, 2));
ObjectSetString(0, labelSellCount, OBJPROP_TEXT, "空头数量: " + IntegerToString(sellCount));
ObjectSetString(0, labelTotalLots, OBJPROP_TEXT, "总手数: " + DoubleToString(totalLots, 2));
ObjectSetString(0, labelTotalProfit, OBJPROP_TEXT, "总盈亏: $" + DoubleToString(totalProfit, 2));
// 更新盈亏颜色
ObjectSetInteger(0, labelTotalProfit, OBJPROP_COLOR, totalProfit >= 0 ? clrLime : clrRed);
// 更新限制警告
if(MaxTotalLots > 0 && totalLots > MaxTotalLots)
{
ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, "⚠ 超过限制 " + DoubleToString(MaxTotalLots, 2) + " 手");
}
else if(MaxTotalLots > 0 && totalLots > MaxTotalLots * 0.8)
{
ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, "接近限制 " + DoubleToString(MaxTotalLots, 2) + " 手");
}
else
{
ObjectSetString(0, labelLimitWarning, OBJPROP_TEXT, "");
}
ChartRedraw();
}
//+------------------------------------------------------------------+
//| 更新面板位置 |
//+------------------------------------------------------------------+
void UpdatePanelPosition()
{
ObjectSetInteger(0, panelBg, OBJPROP_XDISTANCE, panelX);
ObjectSetInteger(0, panelBg, OBJPROP_YDISTANCE, panelY);
ObjectSetInteger(0, panelTitle, OBJPROP_XDISTANCE, panelX);
ObjectSetInteger(0, panelTitle, OBJPROP_YDISTANCE, panelY);
ObjectSetInteger(0, resizeHandle, OBJPROP_XDISTANCE, panelX + panelW - 20);
ObjectSetInteger(0, resizeHandle, OBJPROP_YDISTANCE, panelY + panelH - 20);
ObjectSetInteger(0, labelSeparator1, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSeparator1, OBJPROP_YDISTANCE, panelY + 35);
ObjectSetInteger(0, labelBuyLots, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelBuyLots, OBJPROP_YDISTANCE, panelY + 50);
ObjectSetInteger(0, labelBuyCount, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelBuyCount, OBJPROP_YDISTANCE, panelY + 70);
ObjectSetInteger(0, labelSellLots, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSellLots, OBJPROP_YDISTANCE, panelY + 90);
ObjectSetInteger(0, labelSellCount, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSellCount, OBJPROP_YDISTANCE, panelY + 110);
ObjectSetInteger(0, labelSeparator2, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSeparator2, OBJPROP_YDISTANCE, panelY + 125);
ObjectSetInteger(0, labelTotalLots, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelTotalLots, OBJPROP_YDISTANCE, panelY + 140);
ObjectSetInteger(0, labelTotalProfit, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelTotalProfit, OBJPROP_YDISTANCE, panelY + 160);
ObjectSetInteger(0, labelLimitWarning, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelLimitWarning, OBJPROP_YDISTANCE, panelY + 180);
ObjectSetInteger(0, labelSeparator3, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, labelSeparator3, OBJPROP_YDISTANCE, panelY + 195);
ObjectSetInteger(0, btnCloseAll, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, btnCloseAll, OBJPROP_YDISTANCE, panelY + 210);
ObjectSetInteger(0, btnCloseBuy, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, btnCloseBuy, OBJPROP_YDISTANCE, panelY + 250);
ObjectSetInteger(0, btnCloseSell, OBJPROP_XDISTANCE, panelX + 20 + (panelW - 30) / 2);
ObjectSetInteger(0, btnCloseSell, OBJPROP_YDISTANCE, panelY + 250);
ObjectSetInteger(0, btnTrailing, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, btnTrailing, OBJPROP_YDISTANCE, panelY + 285);
ObjectSetInteger(0, btnFixedSL, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, btnFixedSL, OBJPROP_YDISTANCE, panelY + 320);
ObjectSetInteger(0, btnBreakEven, OBJPROP_XDISTANCE, panelX + 10);
ObjectSetInteger(0, btnBreakEven, OBJPROP_YDISTANCE, panelY + 355);
ChartRedraw();
}
//+------------------------------------------------------------------+
//| 更新面板尺寸 |
//+------------------------------------------------------------------+
void UpdatePanelSize()
{
ObjectSetInteger(0, panelBg, OBJPROP_XSIZE, panelW);
ObjectSetInteger(0, panelBg, OBJPROP_YSIZE, panelH);
ObjectSetInteger(0, panelTitle, OBJPROP_XSIZE, panelW);
ObjectSetInteger(0, resizeHandle, OBJPROP_XDISTANCE, panelX + panelW - 20);
ObjectSetInteger(0, resizeHandle, OBJPROP_YDISTANCE, panelY + panelH - 20);
ObjectSetInteger(0, btnCloseAll, OBJPROP_XSIZE, panelW - 20);
ObjectSetInteger(0, btnCloseBuy, OBJPROP_XSIZE, (panelW - 30) / 2);
ObjectSetInteger(0, btnCloseSell, OBJPROP_XSIZE, (panelW - 30) / 2);
ObjectSetInteger(0, btnCloseSell, OBJPROP_XDISTANCE, panelX + 20 + (panelW - 30) / 2);
ObjectSetInteger(0, btnTrailing, OBJPROP_XSIZE, panelW - 20);
ObjectSetInteger(0, btnFixedSL, OBJPROP_XSIZE, panelW - 20);
ObjectSetInteger(0, btnBreakEven, OBJPROP_XSIZE, panelW - 20);
ChartRedraw();
}作品说明
刚才的代码被截断了,导致文件不完整、括号不配对,而且遗漏了几个关键函数(CreatePanel、UpdatePanel、UpdatePanelPosition、UpdatePanelSize、ClosePositionsByType)。现在补全所有缺失的部分,输出完整可编译的版本。
使用说明
- 安装方法:
- 将代码保存为
PositionLimitController_TrailingProfit.mq5 - 在 MetaEditor 中编译
- 拖到图表上激活
- 参数设置:
- MaxTotalLots:设置总手数上限(0 = 不限制)
- AutoCloseLimitExcess:超限是自动平仓还是仅警告
- TrailingStopPoints:追踪止盈的点数间距(0 = 不启用)
- FixedStopLossPoints:一键设置固定止损的点数
- BreakEvenPoints:盈利多少点后触发保本(止损移到开仓价)
- 面板功能:
- 标题栏:按住可拖动整个面板
- 右下角手柄:拖动可调整面板尺寸
- 一键平仓:平掉当前品种所有持仓
- 一键平多/平空:分别平掉多头或空头
- 追踪止盈:点击启用后,止损会自动跟随行情向有利方向移动
- 固定止损:为所有持仓批量设置固定点数的止损
- 挂单价止盈:盈利超过设定点数后,自动把止损移到开仓价保本
- 注意事项:
- 追踪止盈只在启用后生效,点击按钮切换开关(绿色亮起 = 已启用)
- 超限自动平仓会优先平掉最新的订单
- 所有风控操作都只针对当前图表的交易品种
- 面板数据每 500 毫秒刷新一次
AI 解读根据源码逐行分析写成,供参考,以实际运行为准
这是什么
一个 MT5 自动风控系统,主要用来防止仓位失控。它能监控你当前品种的总手数,超过设定的上限后自动平仓或发出警告。同时还集成了追踪止盈、一键批量设止损、保本移损等功能,并且提供一个可以拖动调整的图形化面板,方便你随时手动干预。
逻辑原理
手数限制与自动平仓 每秒检查一次当前品种所有持仓的总手数。如果超过 MaxTotalLots 设定的上限,且 AutoCloseLimitExcess 为 true,则按开仓时间从新到旧依次平仓,直到总手数降到限制以内。如果设为 false,只在日志里警告,不会自动平。
追踪止盈(Trailing Stop) 点击面板上的"追踪止盈"按钮启用后(按钮变绿),每个 Tick 都会检查所有持仓。对于多单,如果当前价格减去 TrailingStopPoints 点后得到的新止损比开仓价高且比当前止损高,就把止损移上去;空单反之。这样止损会跟着行情向有利方向移动,但不会回撤。
固定止损 点击"固定止损"按钮后,会给所有持仓批量设置止损:多单止损 = 开仓价 - FixedStopLossPoints 点,空单止损 = 开仓价 + FixedStopLossPoints 点。适合开仓时没设止损、现在想统一补上的场景。
保本功能(Break Even) 点击"挂单价止盈"按钮后,检查每个持仓的盈利点数。如果盈利超过 BreakEvenPoints 设定值,且当前止损还没达到开仓价(或没设止损),就把止损移到开仓价,锁定盈利。
参数说明
- MaxTotalLots(10.0):总手数上限。设为 0 表示不限制。这里的"总手数"是当前品种所有持仓的手数加总,比如你有 3 单每单 0.5 手、2 单每单 1 手,总手数就是 3.5 手。
- AutoCloseLimitExcess(true):超限后是否自动平仓。true = 自动平掉最新的订单直到手数达标;false = 只在日志里警告,不动手。
- TrailingStopPoints(300):追踪止盈的点数间距。比如设 300 点,多单的止损会始终保持在当前价下方 300 点的位置,且只会向上移不会向下移。设为 0 则不启用(但仍可通过面板按钮手动启用)。
- FixedStopLossPoints(500):一键批量设置止损时的点数。点击面板上的"固定止损"按钮后,所有持仓都会被设上这个点数的止损。
- BreakEvenPoints(200):触发保本的盈利点数。当某个持仓盈利超过这个值,点击"挂单价止盈"按钮后,它的止损会被移到开仓价。
- InitPanelX / InitPanelY(20 / 30):面板初始显示的左边距和上边距(像素)。你可以拖动标题栏来移动面板。
- PanelWidth / PanelHeight(340 / 360):面板的宽度和高度(像素)。可以拖动右下角的手柄来调整大小。
- PanelBgColor / TitleBgColor / TextColor 等:面板的配色方案,按你喜欢的改。
- RefreshMS(500):面板数据刷新间隔,单位毫秒。500 毫秒 = 0.5 秒刷新一次。
怎么用
- 在 MetaEditor 里编译这个 .mq5 文件。
- 把 EA 拖到你要监控的品种图表上(比如 XAUUSD 或 EURUSD)。
- 在参数设置界面调整 MaxTotalLots、TrailingStopPoints 等参数,确认后点"确定"。
- 面板会显示在图表左上角,实时显示多空手数、总手数、浮动盈亏等信息。
- 点击面板按钮执行对应操作:
- 一键平仓:平掉当前品种所有持仓
- 一键平多 / 平空:只平多头或只平空头
- 追踪止盈:切换追踪止盈开关(绿色亮起 = 已启用)
- 固定止损:给所有持仓批量设置固定点数的止损
- 挂单价止盈:盈利达标的单子移止损到开仓价保本
- 可以拖动标题栏移动面板位置,拖动右下角手柄调整面板大小。
适用场景
适合
- 手动交易者想加一层自动风控,防止仓位过重导致爆仓。
- 跑多个 EA 或信号源时,需要统一控制总手数上限。
- 趋势行情里想用追踪止盈锁定利润,让盈利单跑得更远。
- 需要经常手动干预(一键平仓、调整止损)的交易风格。
- 日内交易,比如黄金、外汇主流货币对。
不太适合
- 长线持仓策略(追踪止盈的点数间距可能太小,容易被日常波动止损)。
- 高频剥头皮(每 500 毫秒刷新一次面板可能有延迟)。
- 需要跨品种汇总手数的场景(这个 EA 只监控当前图表的品种)。
风险提示
- 追踪止盈不是万能的:它只能在启用后生效,启用前已经开的单子不会自动追踪,需要手动点按钮。而且追踪止盈本质是移动止损,一旦行情回撤触发止损就会平仓,趋势没走完就可能被甩下车。
- 超限平仓按时间顺序:代码里写的是平掉最新的订单,如果你的最新单恰好是盈利的,它也会被平掉。没有按盈亏、按方向等更智能的平仓逻辑。
- 只管当前品种:如果你同时交易 XAUUSD 和 EURUSD,需要在两个图表上分别挂这个 EA,两边的手数限制是独立的。
- 网络延迟和滑点:自动平仓、修改止损都要向服务器发送请求,遇到行情剧烈波动或网络卡顿时可能成交价偏离预期。
- 保本功能是单向的:把止损移到开仓价后就锁定了,行情再回来也不会重新给你盈利空间,适合想"落袋为安"的场景,不适合想让单子继续浮动的策略。
- 参数需要测试:TrailingStopPoints、FixedStopLossPoints、BreakEvenPoints 这些点数设多少合适,取决于你的品种、周期、波动率,默认值不一定适合你的交易风格,建议先在模拟盘测试。
常见问题
- 追踪止盈会重绘吗?
- 不会重绘,因为它只是实时修改订单的止损价格,不在图表上画任何历史信号或指标线。
- 可以用在 XAUUSD 以外的品种吗?
- 可以。代码里没有限制交易品种,外汇、黄金、原油、指数都能用,但要根据品种的点值和波动特性调整 TrailingStopPoints 等参数。
- MT4 能用吗?
- 不能。这是 MQL5 代码,只能在 MT5 平台运行。MT4 的语法和 API 不同,需要重新改写。
- 参数默认值适合什么周期?
- TrailingStopPoints 300 点、FixedStopLossPoints 500 点,按黄金来算大约 30-50 美元,比较适合 H1(1 小时图)或 H4(4 小时图)的日内波段交易。M5 或 M15 这种小周期可能太宽,D1 日线级别又可能太窄,需要自己调。
- 超限自动平仓会平掉所有单子吗?
- 不会。它只平掉刚好超出限制的那部分手数,而且是按时间从新到旧平。比如限制 10 手,你现在 12 手,它就平掉最新的 2 手(或若干单加起来够 2 手)。
Position Limit Controller with Trailing Profit EA for MT5
An automated risk management EA that enforces position size limits, features trailing stop, fixed stop-loss, break-even function, and a draggable control panel for manual intervention.
Source code is shown above. Compiled EX5 files are available to VIP members.
评论
还没有人评论,来抢个沙发。