|
|
//+------------------------------------------------------------------+
//| 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();
}
//+------------------------------------------------------------------+
|
|