Manual_Money_Management/ManualTradeManager.mq5
2026-10-27 07:40:34 +00:00
2881 lines
106 KiB
MQL5
//+------------------------------------------------------------------+
//| ManualTradeManager.mq5 |
//| Manual Trading Panel + Martingale Basket Manager |
//| Bilingual Professional GUI |
//+------------------------------------------------------------------+
#property copyright "Manual Trade Manager"
#property link ""
#property version "1.64"
#property description "Manual Trade Manager - Bilingual Panel"
#property description "TP/SL + Trailing + Martingale Basket + Buy/Sell + Close buttons"
#property description "v1.64: Martingale Manual / Auto mode (auto adds trades at candle close by distance, with max trades) | Max by Eq / Max by Bal"
#include <Trade\PositionInfo.mqh>
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Label.mqh>
// Vertical shift applied to everything below the "Apply" row
// (Apply button now lives directly under the Lot field)
#define Y_SHIFT 14
// Extra vertical space taken by the "Lot Capacity" section (buttons below are shifted down)
// Extra vertical space taken by the Auto-Martingale rows (mode selector + gap/max-trades row)
#define MA_SHIFT 48
#define LOT_SHIFT (100 + MA_SHIFT)
//+------------------------------------------------------------------+
//| Input Parameters |
//+------------------------------------------------------------------+
enum ENUM_MARTIN_MODE
{
MARTIN_MANUAL = 0, // Manual / دستی
MARTIN_AUTO = 1 // Auto / اتوماتیک
};
input group "=== General Settings / تنظیمات کلی ==="
input long InpMagicNumber = 0; // Magic (0 = all symbol trades: panel + manual)
input string InpTradeComment = "ManualPanel"; // Trade Comment
input int InpSlippage = 30; // Slippage (points)
input double InpDefaultLot = 0.01; // Default Lot Size
input group "=== Lot Capacity & Martingale / ظرفیت لات و مارتینگل ==="
input double InpDefaultMartin = 1.0; // Default Martingale multiplier (1.0 = OFF, fixed lot)
input ENUM_MARTIN_MODE InpMartinMode = MARTIN_MANUAL; // Martingale Mode (Manual = you click / Auto = EA adds trades)
input int InpAutoMartinDistance = 200; // Auto: gap from the LAST trade to the next one (points)
input int InpAutoMartinMaxTrades = 5; // Auto: max trades per side in the martingale (incl. the first one)
input double InpMaxMarginUsePct = 100.0; // % of Free Margin allowed for NEW orders (100 = use all, lower = keep a safety buffer)
input group "=== Execution Speed / سرعت اجرا ==="
input bool InpAsyncOrders = true; // Async Buy/Sell (fastest, result logged)
input int InpModifyCooldownMs = 150; // Min gap between SL/TP modify batches per side (ms)
input group "=== Panel Colors / رنگ‌بندی پنل ==="
input color InpPanelBg = C'0,0,0'; // Panel Background (Black)
input color InpAccentColor = C'0,210,190'; // Accent (Cyan)
input color InpBuyColor = C'0,200,90'; // Buy Green
input color InpSellColor = C'230,55,55'; // Sell Red
input color InpTextColor = C'245,248,255'; // Main Text (high contrast)
input color InpLabelColor = C'210,215,230'; // Labels (readable on black)
input color InpHeaderColor = C'255,200,50'; // Section Headers (Gold)
input color InpEditBg = C'20,20,28'; // Edit Background (dark gray)
input color InpEditText = C'255,230,80'; // Edit Text (Yellow)
input color InpStatusColor = C'100,180,255'; // Status Blue
//+------------------------------------------------------------------+
//| Global Variables |
//+------------------------------------------------------------------+
CPositionInfo posInfo;
int g_SingleTP = 300;
int g_SingleSL = 200;
int g_TrailStart = 100;
int g_TrailStep = 20;
int g_BasketTP = 150;
int g_BasketTrailStart = 80;
int g_BasketTrailStep = 15;
bool g_EnableSingleTrail = true; // committed – used by ManagePositions
bool g_EnableBasketTrail = true;
bool g_EnableBasketTP = true;
bool g_MartinAuto = false; // committed: false = Manual (current behaviour), true = Auto
int g_AutoDist = 200; // committed: gap (points) from the last trade to the next
int g_AutoMax = 5; // committed: max trades per side (incl. first)
datetime g_LastBarTime = 0; // new-candle detector for Auto martingale
double g_LotSize = 0.01;
double g_Martin = 1.0; // martingale multiplier (committed via Apply). <=1.0 => fixed lot
// Pending toggle states (UI only until Apply is clicked)
bool g_PendingSingleTrail = true;
bool g_PendingBasketTrail = true;
bool g_PendingBasketTP = true;
bool g_PendingMartinAuto = false;
// Stable basket TP targets
double g_LastBuyAvg = 0.0;
double g_LastSellAvg = 0.0;
double g_StableBuyTP = 0.0;
double g_StableSellTP = 0.0;
int g_LastBuyCount = 0;
int g_LastSellCount = 0;
// ---- Symbol / execution cache (filled once, refreshed cheaply) ----
double g_MinStopDist = 0.0; // price units
double g_VolMin = 0.01;
double g_VolMax = 100.0;
double g_VolStep = 0.01;
int g_VolDigits = 2;
ENUM_ORDER_TYPE_FILLING g_Filling = ORDER_FILLING_FOK;
string g_Ccy = "";
MqlTradeRequest g_ReqOpen; // pre-built market-order template
// ---- Direct-click bookkeeping ----
string g_ObjBuy = "";
string g_ObjSell = "";
int g_BuyFastPending = 0; // clicks already executed by the fast path
int g_SellFastPending = 0;
// ---- Per-pass market snapshot ----
double g_Bid = 0.0;
double g_Ask = 0.0;
// ---- Position scan results (one pass feeds everything) ----
struct SideStats
{
int count;
double lots;
double sum; // sum(open * volume)
double lastLot; // volume of the most recently opened position on this side
double lastPrice;// open price of that most recently opened position
ulong lastMs; // open time (ms) of that position
};
SideStats g_Buy;
SideStats g_Sell;
double g_FloatPL = 0.0;
ulong g_PosSig = 0;
ulong g_BuyTk[];
ulong g_SellTk[];
// ---- Modify throttling (index = ENUM_POSITION_TYPE: 0 buy, 1 sell) ----
ulong g_CoolUntil[2] = {0, 0};
// ---- Retry window for rejected market requests ----
ulong g_RetryWinStart = 0;
int g_RetryCount = 0;
// ---- Forward declarations ----
bool Dispatch(MqlTradeRequest &req);
bool RecoverRequest(const MqlTradeRequest &req, const uint rc);
void ExecuteMarket(const ENUM_ORDER_TYPE type);
int CloseManaged(const int mode);
int ForceApplySettingsToAll();
void ManagePositions();
void ManageSinglePosition(const ulong ticket, const ENUM_POSITION_TYPE type);
void ManageBasket(ulong &tickets[], const int count, const ENUM_POSITION_TYPE type, const double avgPrice);
void ScanPositions(const bool wantSig);
//+------------------------------------------------------------------+
//| Small helpers |
//+------------------------------------------------------------------+
bool MagicOk(const long m)
{
if(InpMagicNumber == 0) return true; // all symbol trades
return (m == InpMagicNumber || m == 0); // panel + manual
}
bool IsManagedPosition()
{
if(posInfo.Symbol() != _Symbol) return false;
return MagicOk(posInfo.Magic());
}
ulong HashMix(ulong h, const double v)
{
h ^= (ulong)MathRound(v * 100000.0) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
void PrintThrottled(const string msg)
{
static ulong last = 0;
ulong now = GetTickCount64();
if(now - last < 1000) return;
last = now;
Print(msg);
}
// Accept Persian / Arabic-Indic digits and decimal separators in edit fields
string CleanNum(string s)
{
StringTrimLeft(s);
StringTrimRight(s);
int n = StringLen(s);
for(int i = 0; i < n; i++)
{
ushort c = StringGetCharacter(s, i);
if(c >= 0x06F0 && c <= 0x06F9)
StringSetCharacter(s, i, (ushort)('0' + (c - 0x06F0)));
else if(c >= 0x0660 && c <= 0x0669)
StringSetCharacter(s, i, (ushort)('0' + (c - 0x0660)));
else if(c == 0x066B || c == 0x060C || c == ',')
StringSetCharacter(s, i, '.');
}
StringReplace(s, " ", "");
return s;
}
int ParseIntField(const string raw)
{
string s = CleanNum(raw);
if(s == "") return 0;
return (int)StringToInteger(s);
}
//+------------------------------------------------------------------+
//| Symbol cache |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING DetectFilling()
{
long fm = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
if((fm & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK) return ORDER_FILLING_FOK;
if((fm & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC) return ORDER_FILLING_IOC;
return ORDER_FILLING_RETURN;
}
void RefreshStopLevel()
{
long sl = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
g_MinStopDist = (sl > 0 ? (double)sl : 1.0) * _Point;
}
void CacheSymbolData()
{
RefreshStopLevel();
g_VolMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
g_VolMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
g_VolStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(g_VolStep <= 0.0) g_VolStep = 0.01;
if(g_VolMin <= 0.0) g_VolMin = g_VolStep;
if(g_VolMax <= 0.0) g_VolMax = 1000.0;
g_VolDigits = 0;
double s = g_VolStep;
while(g_VolDigits < 8 && MathAbs(s - MathRound(s)) > 1e-9)
{
s *= 10.0;
g_VolDigits++;
}
g_Filling = DetectFilling();
g_Ccy = AccountInfoString(ACCOUNT_CURRENCY);
}
double NormalizeLot(double lot)
{
if(lot < g_VolMin) lot = g_VolMin;
if(lot > g_VolMax) lot = g_VolMax;
lot = MathFloor(lot / g_VolStep + 1e-6) * g_VolStep;
if(lot < g_VolMin) lot = g_VolMin;
return NormalizeDouble(lot, g_VolDigits);
}
void BuildOpenRequest()
{
ZeroMemory(g_ReqOpen);
g_ReqOpen.action = TRADE_ACTION_DEAL;
g_ReqOpen.symbol = _Symbol;
g_ReqOpen.deviation = (ulong)InpSlippage;
g_ReqOpen.magic = (ulong)InpMagicNumber;
g_ReqOpen.type_filling = g_Filling;
g_ReqOpen.type_time = ORDER_TIME_GTC;
g_ReqOpen.comment = InpTradeComment;
}
double MinStopDistance()
{
return g_MinStopDist;
}
//+------------------------------------------------------------------+
//| Raw trade helpers (no CTrade layer) |
//+------------------------------------------------------------------+
bool RetryAllowed()
{
ulong now = GetTickCount64();
if(now - g_RetryWinStart > 2000)
{
g_RetryWinStart = now;
g_RetryCount = 0;
}
if(g_RetryCount >= 8) return false;
g_RetryCount++;
return true;
}
// Recover our own rejected market requests (requote / invalid stops)
bool RecoverRequest(const MqlTradeRequest &req, const uint rc)
{
if(req.action != TRADE_ACTION_DEAL) return false;
if(req.type != ORDER_TYPE_BUY && req.type != ORDER_TYPE_SELL) return false;
bool stopsErr = (rc == TRADE_RETCODE_INVALID_STOPS);
bool priceErr = (rc == TRADE_RETCODE_REQUOTE ||
rc == TRADE_RETCODE_PRICE_CHANGED ||
rc == TRADE_RETCODE_PRICE_OFF);
if(!stopsErr && !priceErr) return false;
if(!RetryAllowed()) return false;
MqlTick t;
if(!SymbolInfoTick(_Symbol, t)) return false;
MqlTradeRequest r = req;
r.price = (req.type == ORDER_TYPE_BUY) ? t.ask : t.bid;
if(stopsErr && req.position == 0)
{
r.sl = 0.0; // open naked – ManagePositions attaches TP/SL next tick
r.tp = 0.0;
}
Print("Retrying request (ret=", rc, ") at fresh price");
return Dispatch(r);
}
bool Dispatch(MqlTradeRequest &req)
{
MqlTradeResult res;
ZeroMemory(res);
ResetLastError();
if(InpAsyncOrders)
{
if(OrderSendAsync(req, res)) return true;
Print("Order send failed (async) err=", GetLastError(), " ret=", res.retcode);
return false;
}
if(!OrderSend(req, res))
{
Print("Order send failed err=", GetLastError(), " ret=", res.retcode, " ", res.comment);
return false;
}
if(res.retcode == TRADE_RETCODE_DONE ||
res.retcode == TRADE_RETCODE_DONE_PARTIAL ||
res.retcode == TRADE_RETCODE_PLACED)
return true;
Print("Order rejected ret=", res.retcode, " ", res.comment);
return RecoverRequest(req, res.retcode);
}
bool SendSLTP(const ulong ticket, const double sl, const double tp)
{
MqlTradeRequest r;
MqlTradeResult res;
ZeroMemory(r);
ZeroMemory(res);
r.action = TRADE_ACTION_SLTP;
r.position = ticket;
r.symbol = _Symbol;
r.sl = (sl > 0.0) ? NormalizeDouble(sl, _Digits) : 0.0;
r.tp = (tp > 0.0) ? NormalizeDouble(tp, _Digits) : 0.0;
if(OrderSendAsync(r, res)) return true;
PrintThrottled("SL/TP send failed #" + (string)ticket + " err=" + (string)GetLastError());
return false;
}
void StartCooldown(const ENUM_POSITION_TYPE type)
{
g_CoolUntil[(int)type] = GetTickCount64() + (ulong)MathMax(0, InpModifyCooldownMs);
}
bool HasManagedSide(const ENUM_POSITION_TYPE type)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetTicket(i) == 0) continue;
if(PositionGetInteger(POSITION_TYPE) != (long)type) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(!MagicOk(PositionGetInteger(POSITION_MAGIC))) continue;
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Lot capacity helpers (leverage / balance / equity / floating) |
//| Free margin already contains equity, floating P/L and the margin |
//| used by every open position, so Max Lot reacts live to all of it |
//+------------------------------------------------------------------+
double ClampMartin(double m)
{
if(m < 1.0) m = 1.0;
if(m > 10.0) m = 10.0;
return NormalizeDouble(m, 2);
}
double FloorToStep(const double lot)
{
const double l = MathFloor(lot / g_VolStep + 1e-6) * g_VolStep;
return NormalizeDouble(l, g_VolDigits);
}
string LotStr(const double lot)
{
return DoubleToString(lot, MathMax(2, g_VolDigits));
}
// Margin the broker would require for `lots` at the current price
bool CalcMarginFor(const ENUM_ORDER_TYPE ot, const double lots, double &margin)
{
double price = SymbolInfoDouble(_Symbol, (ot == ORDER_TYPE_BUY) ? SYMBOL_ASK : SYMBOL_BID);
if(price <= 0.0) price = (ot == ORDER_TYPE_BUY) ? g_Ask : g_Bid;
if(price <= 0.0) return false;
return OrderCalcMargin(ot, _Symbol, lots, price, margin);
}
// Volume already on this symbol (all magics); optionally + pending orders (for SYMBOL_VOLUME_LIMIT)
double SymbolVolumeUsed(const bool withPending)
{
double v = 0.0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetTicket(i) == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
v += PositionGetDouble(POSITION_VOLUME);
}
if(withPending)
{
for(int j = OrdersTotal() - 1; j >= 0; j--)
{
if(OrderGetTicket(j) == 0) continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
v += OrderGetDouble(ORDER_VOLUME_CURRENT);
}
}
return v;
}
// Largest single order (lots) that fits in the CURRENT free margin
double MaxLotByMargin(const ENUM_ORDER_TYPE ot, const double freeMargin)
{
const double pct = MathMax(0.0, MathMin(100.0, InpMaxMarginUsePct));
const double avail = freeMargin * pct / 100.0;
if(avail <= 0.0) return 0.0;
double mpl = 0.0; // margin for 1.00 lot
if(!CalcMarginFor(ot, 1.0, mpl) || mpl <= 0.0) return 0.0;
double lot = FloorToStep(avail / mpl);
// linear estimate may be slightly off (tiered margin) → verify with the broker calc
for(int k = 0; k < 4 && lot >= g_VolMin; k++)
{
double m = 0.0;
if(!CalcMarginFor(ot, lot, m)) break;
if(m <= avail) break;
lot = FloorToStep(lot - g_VolStep);
}
lot = MathMin(lot, g_VolMax); // broker max per order
const double lim = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_LIMIT);
if(lim > 0.0) // broker max total volume per symbol
lot = MathMin(lot, FloorToStep(MathMax(0.0, lim - SymbolVolumeUsed(true))));
if(lot < g_VolMin) return 0.0;
return lot;
}
// Total volume of ALL open positions on the account (every symbol, every magic)
double TotalVolumeAllSymbols()
{
double v = 0.0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetTicket(i) == 0) continue;
v += PositionGetDouble(POSITION_VOLUME);
}
return v;
}
// Margin the broker needs for 1.00 lot of the chart symbol (max of Buy/Sell), 0 on failure
double MarginPerLotNow()
{
double mb = 0.0, ms = 0.0;
const bool okB = CalcMarginFor(ORDER_TYPE_BUY, 1.0, mb);
const bool okS = CalcMarginFor(ORDER_TYPE_SELL, 1.0, ms);
if(!okB && !okS) return 0.0;
return MathMax(okB ? mb : 0.0, okS ? ms : 0.0);
}
// Theoretical TOTAL lots a given amount of money (Balance or Equity) can carry at current leverage
double MaxLotByBalance(const double balance)
{
double mb = 0.0, ms = 0.0;
const bool okB = CalcMarginFor(ORDER_TYPE_BUY, 1.0, mb);
const bool okS = CalcMarginFor(ORDER_TYPE_SELL, 1.0, ms);
if(!okB && !okS) return 0.0;
const double mpl = MathMax(okB ? mb : 0.0, okS ? ms : 0.0);
if(mpl <= 0.0) return 0.0;
const double pct = MathMax(0.0, MathMin(100.0, InpMaxMarginUsePct));
return FloorToStep(balance * pct / 100.0 / mpl);
}
// Lot planned for the next order on a side:
// - no trade on that side, or multiplier <= 1.0 → base Lot from the panel
// - otherwise → multiplier applied with 2-decimal precision (e.g. 1.13).
// To stop the lot-step rounding from eating small multipliers (0.01 x 1.13 = 0.0113 → 0.01
// forever), the lot is taken from the exact series base x mult^count whenever the open
// trades of that side follow that series; if they do not (lot edited by hand, capped by
// margin, mixed manual trades...), it falls back to last lot x multiplier.
// (uses g_Buy / g_Sell from the latest ScanPositions)
double PlannedLot(const bool isBuy)
{
const int cnt = isBuy ? g_Buy.count : g_Sell.count;
const double last = isBuy ? g_Buy.lastLot : g_Sell.lastLot;
double lot = g_LotSize;
if(g_Martin > 1.0 && cnt > 0 && last > 0.0)
{
const double mult = NormalizeDouble(g_Martin, 2);
const double expLast = MathRound(g_LotSize * MathPow(mult, cnt - 1) / g_VolStep) * g_VolStep;
if(MathAbs(last - expLast) <= g_VolStep * 1.5)
lot = MathRound(g_LotSize * MathPow(mult, cnt) / g_VolStep) * g_VolStep; // exact series
else
lot = MathRound(last * mult / g_VolStep) * g_VolStep; // fallback
}
return NormalizeLot(lot);
}
// Limit a wanted lot to the live max lot
double ApplyCap(const double want, const double maxLot, bool &capped)
{
capped = false;
if(maxLot <= 0.0) { capped = true; return 0.0; }
if(want > maxLot) { capped = true; return maxLot; }
return want;
}
//+------------------------------------------------------------------+
//| FAST PATH – market Buy / Sell |
//| One tick read, one position scan, one pre-built request. |
//+------------------------------------------------------------------+
void ExecuteMarket(const ENUM_ORDER_TYPE type)
{
MqlTick t;
if(!SymbolInfoTick(_Symbol, t) || t.ask <= 0.0 || t.bid <= 0.0)
{
Print("Market order skipped: no quote");
return;
}
const bool isBuy = (type == ORDER_TYPE_BUY);
double sl = 0.0, tp = 0.0;
// Lot of this order: base lot, or last lot x martingale when the side already has trades,
// then limited by the live max lot (free margin / leverage / broker limits)
ScanPositions(false);
bool capped = false;
const double mxLot = MaxLotByMargin(type, AccountInfoDouble(ACCOUNT_MARGIN_FREE));
const double lot = ApplyCap(PlannedLot(isBuy), mxLot, capped);
if(lot <= 0.0)
{
Print("Order BLOCKED: free margin is not enough for the minimum lot (",
DoubleToString(g_VolMin, g_VolDigits), ")");
return;
}
if(capped)
Print("Lot capped to max allowed ", DoubleToString(lot, g_VolDigits),
" (free margin / leverage limit)");
// First order of a side gets SL/TP; later ones join the basket (no SL/TP)
if(!HasManagedSide(isBuy ? POSITION_TYPE_BUY : POSITION_TYPE_SELL))
{
const double minD = g_MinStopDist;
if(isBuy)
{
if(g_SingleSL > 0) sl = MathMin(t.ask - g_SingleSL * _Point, t.bid - minD);
if(g_SingleTP > 0) tp = MathMax(t.ask + g_SingleTP * _Point, t.bid + minD);
}
else
{
if(g_SingleSL > 0) sl = MathMax(t.bid + g_SingleSL * _Point, t.ask + minD);
if(g_SingleTP > 0) tp = MathMin(t.bid - g_SingleTP * _Point, t.ask - minD);
}
if(sl > 0.0) sl = NormalizeDouble(sl, _Digits);
if(tp > 0.0) tp = NormalizeDouble(tp, _Digits);
}
g_ReqOpen.type = type;
g_ReqOpen.volume = lot;
g_ReqOpen.price = isBuy ? t.ask : t.bid;
g_ReqOpen.sl = sl;
g_ReqOpen.tp = tp;
if(!Dispatch(g_ReqOpen))
Print(isBuy ? "BUY" : "SELL", " failed");
}
//+------------------------------------------------------------------+
//| Close managed positions – all requests fired in parallel |
//| mode: 0 = all, 1 = buys, 2 = sells |
//+------------------------------------------------------------------+
int CloseManaged(const int mode)
{
MqlTick t;
if(!SymbolInfoTick(_Symbol, t)) return 0;
int sent = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
const long ptype = PositionGetInteger(POSITION_TYPE);
if(mode == 1 && ptype != (long)POSITION_TYPE_BUY) continue;
if(mode == 2 && ptype != (long)POSITION_TYPE_SELL) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(!MagicOk(PositionGetInteger(POSITION_MAGIC))) continue;
MqlTradeRequest r;
MqlTradeResult res;
ZeroMemory(r);
ZeroMemory(res);
const bool isBuyPos = (ptype == (long)POSITION_TYPE_BUY);
r.action = TRADE_ACTION_DEAL;
r.position = ticket;
r.symbol = _Symbol;
r.volume = PositionGetDouble(POSITION_VOLUME);
r.type = isBuyPos ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
r.price = isBuyPos ? t.bid : t.ask;
r.deviation = (ulong)InpSlippage;
r.type_filling = g_Filling;
r.magic = (ulong)InpMagicNumber;
r.comment = InpTradeComment;
if(OrderSendAsync(r, res)) sent++;
else Print("Close send failed #", ticket, " err=", GetLastError());
}
return sent;
}
//+------------------------------------------------------------------+
//| Class: CManualPanel |
//+------------------------------------------------------------------+
class CManualPanel : public CAppDialog
{
private:
// Labels
CLabel m_lblTitle;
CLabel m_lblTradeHeader;
CLabel m_lblSingleHeader;
CLabel m_lblBasketHeader;
CLabel m_lblStatusHeader;
CLabel m_lblLot;
CLabel m_lblTP, m_lblSL, m_lblTrailStart, m_lblTrailStep;
CLabel m_lblBasketTP, m_lblBasketTrailStart, m_lblBasketTrailStep;
CLabel m_lblTimer;
CLabel m_lblStatus;
CLabel m_lblAvgBuy;
CLabel m_lblAvgSell;
CLabel m_lblProfit;
CLabel m_lblTpMoney; // expected $ at TP
CLabel m_lblSlMoney; // expected $ at SL (hidden in basket w/o SL)
CLabel m_lblChkSingle; // bilingual text next to checkbox
CLabel m_lblChkBasket;
CLabel m_lblChkBTP;
CLabel m_lblMartin; // "Martin x:" caption next to the multiplier field
CLabel m_lblMartinMode; // "Martin Mode" caption
CLabel m_lblAutoDist; // "Next Gap (pts):" caption (Auto only)
CLabel m_lblAutoMax; // "Max Trades:" caption (Auto only)
CLabel m_lblLotHeader; // Lot capacity section (live)
CLabel m_lblLotAcc;
CLabel m_lblLotCap;
CLabel m_lblLotMax;
CLabel m_lblLotNext;
// Edits
CEdit m_editLot;
CEdit m_editTP, m_editSL, m_editTrailStart, m_editTrailStep;
CEdit m_editBasketTP, m_editBasketTrailStart, m_editBasketTrailStep;
CEdit m_editMartin;
CEdit m_editAutoDist, m_editAutoMax;
// Toggle buttons
CButton m_btnSingleTrail;
CButton m_btnBasketTrail;
CButton m_btnBasketTP;
CButton m_btnMartinMode; // Manual / Auto selector
// Buttons
CButton m_btnBuy;
CButton m_btnSell;
CButton m_btnApply;
CButton m_btnCloseAllBuy;
CButton m_btnCloseAllSell;
CButton m_btnCloseAll;
CButton m_btnRemoveSL;
CButton m_btnDeleteOrders; // delete all pending orders (limit/stop)
// UI update caches
ulong m_lastSig;
int m_hardRefresh; // >0 → force full recompute + unconditional label write (after (re)init)
ulong m_lastHeavyMs;
ulong m_lastPLms;
ulong m_lastLotMs;
string m_tfName;
public:
CManualPanel(void) { m_hardRefresh = 0; m_lastSig = 0; m_lastHeavyMs = 0; m_lastPLms = 0; m_lastLotMs = 0; m_tfName = ""; }
~CManualPanel(void) {}
virtual bool Create(const long chart, const string name, const int subwin,
const int x1, const int y1, const int x2, const int y2);
virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
void UpdateStatus(const bool force = false);
void UpdateFloatingPL(const bool force = false);
void UpdateLotInfo(const bool force = false);
void UpdateTimer();
void ReadValuesFromPanel();
void WriteValuesToPanel();
void ApplyDarkTheme();
void StyleToggle(CButton &btn, bool on);
void StyleMartinMode(const bool autoMode);
void SetAutoFieldsEnabled(const bool en);
void RequestHardRefresh(const int n = 12) { m_hardRefresh = n; m_lastSig = 0; m_lastHeavyMs = 0; }
void KillMinimize(); // public – called from OnInit/OnChartEvent
// Override CAppDialog minimize – do absolutely nothing
virtual void Minimize(void) { /* intentionally empty – panel never minimizes */ }
protected:
bool CreateLabels();
bool CreateEdits();
bool CreateCheckBoxes();
bool CreateButtons();
void StyleEdit(CEdit &edit);
void StyleLabel(CLabel &lbl, color clr);
void SetLabel(CLabel &lbl, const string txt, const color clr);
void OnClickBuy();
void OnClickSell();
void OnClickApply();
void OnClickCloseAllBuy();
void OnClickCloseAllSell();
void OnClickCloseAll();
void OnClickRemoveSL();
void OnClickDeleteOrders();
void OnClickSingleTrail();
void OnClickBasketTrail();
void OnClickBasketTP();
void OnClickMartinMode();
};
//+------------------------------------------------------------------+
//| Event Map |
//| Buy/Sell are normally executed by the fast path in OnChartEvent; |
//| these entries are only a safety net (see *FastPending counters). |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CManualPanel)
ON_EVENT(ON_CLICK, m_btnBuy, OnClickBuy)
ON_EVENT(ON_CLICK, m_btnSell, OnClickSell)
ON_EVENT(ON_CLICK, m_btnApply, OnClickApply)
ON_EVENT(ON_CLICK, m_btnCloseAllBuy, OnClickCloseAllBuy)
ON_EVENT(ON_CLICK, m_btnCloseAllSell, OnClickCloseAllSell)
ON_EVENT(ON_CLICK, m_btnCloseAll, OnClickCloseAll)
ON_EVENT(ON_CLICK, m_btnRemoveSL, OnClickRemoveSL)
ON_EVENT(ON_CLICK, m_btnDeleteOrders, OnClickDeleteOrders)
ON_EVENT(ON_CLICK, m_btnSingleTrail, OnClickSingleTrail)
ON_EVENT(ON_CLICK, m_btnBasketTrail, OnClickBasketTrail)
ON_EVENT(ON_CLICK, m_btnBasketTP, OnClickBasketTP)
ON_EVENT(ON_CLICK, m_btnMartinMode, OnClickMartinMode)
EVENT_MAP_END(CAppDialog)
//+------------------------------------------------------------------+
//| Create Panel |
//+------------------------------------------------------------------+
bool CManualPanel::Create(const long chart, const string name, const int subwin,
const int x1, const int y1, const int x2, const int y2)
{
if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2))
return false;
Caption("Manual Trade Manager");
m_tfName = EnumToString(PERIOD_CURRENT);
StringReplace(m_tfName, "PERIOD_", "");
ApplyDarkTheme();
if(!CreateLabels()) return false;
if(!CreateEdits()) return false;
if(!CreateCheckBoxes()) return false;
if(!CreateButtons()) return false;
// Re-apply dark theme after all controls exist
ApplyDarkTheme();
WriteValuesToPanel();
// Explicitly enable all edit fields for keyboard input
m_editLot.ReadOnly(false);
m_editTP.ReadOnly(false);
m_editSL.ReadOnly(false);
m_editTrailStart.ReadOnly(false);
m_editTrailStep.ReadOnly(false);
m_editBasketTP.ReadOnly(false);
m_editBasketTrailStart.ReadOnly(false);
m_editBasketTrailStep.ReadOnly(false);
m_editMartin.ReadOnly(false);
m_editAutoDist.ReadOnly(false);
m_editAutoMax.ReadOnly(false);
SetAutoFieldsEnabled(g_PendingMartinAuto); // Auto fields: enabled only in Auto mode
// After a timeframe change the panel is rebuilt: recompute TP$/SL$ immediately
RequestHardRefresh();
return true;
}
//+------------------------------------------------------------------+
//| Style helpers |
//+------------------------------------------------------------------+
void CManualPanel::StyleEdit(CEdit &edit)
{
edit.Color(InpEditText);
edit.ColorBackground(InpEditBg);
edit.ColorBorder(C'70,75,90');
edit.Font("Consolas");
edit.FontSize(9);
edit.ReadOnly(false);
edit.TextAlign(ALIGN_CENTER);
}
void CManualPanel::StyleLabel(CLabel &lbl, color clr)
{
lbl.Color(clr);
lbl.Font("Arial");
lbl.FontSize(8);
}
// Touch the chart object only when something really changed
void CManualPanel::SetLabel(CLabel &lbl, const string txt, const color clr)
{
if(m_hardRefresh > 0)
{
lbl.Color(clr);
lbl.Text(txt);
return;
}
if(lbl.Color() != clr) lbl.Color(clr);
if(lbl.Text() != txt) lbl.Text(txt);
}
//+------------------------------------------------------------------+
//| Apply dark theme to panel |
//+------------------------------------------------------------------+
void CManualPanel::ApplyDarkTheme()
{
// Do NOT touch ZORDER/SELECTABLE/READONLY (breaks CEdit)
string prefix = Name();
color navy = InpPanelBg; // black
color capBg = C'10,10,14';
color border = C'40,40,55';
int total = ObjectsTotal(0, -1, -1);
for(int i = total - 1; i >= 0; i--)
{
string nm = ObjectName(0, i, -1, -1);
if(StringFind(nm, prefix) < 0) continue;
// Never touch interactive controls
if(StringFind(nm, "edit") >= 0 || StringFind(nm, "Edit") >= 0) continue;
if(StringFind(nm, "btn") >= 0 || StringFind(nm, "Button") >= 0) continue;
long otype = ObjectGetInteger(0, nm, OBJPROP_TYPE);
if(otype == OBJ_RECTANGLE_LABEL || otype == OBJ_BITMAP_LABEL ||
otype == OBJ_RECTANGLE || otype == OBJ_EDIT)
{
if(StringFind(nm, "Caption") >= 0)
{
ObjectSetInteger(0, nm, OBJPROP_BGCOLOR, capBg);
ObjectSetInteger(0, nm, OBJPROP_COLOR, InpAccentColor);
ObjectSetInteger(0, nm, OBJPROP_BORDER_COLOR, InpAccentColor);
}
else
{
ObjectSetInteger(0, nm, OBJPROP_BGCOLOR, navy);
ObjectSetInteger(0, nm, OBJPROP_COLOR, navy);
ObjectSetInteger(0, nm, OBJPROP_BORDER_COLOR, border);
}
}
}
// Main dialog container
if(ObjectFind(0, prefix) >= 0)
{
ObjectSetInteger(0, prefix, OBJPROP_BGCOLOR, navy);
ObjectSetInteger(0, prefix, OBJPROP_COLOR, navy);
ObjectSetInteger(0, prefix, OBJPROP_BORDER_COLOR, border);
}
}
//+------------------------------------------------------------------+
//| Create Labels |
//+------------------------------------------------------------------+
bool CManualPanel::CreateLabels()
{
int x = 12;
int y = 6;
int w = ClientAreaWidth() - 24;
// Title
if(!m_lblTitle.Create(m_chart_id, m_name+"Title", m_subwin, x, y, x+w, y+20)) return false;
m_lblTitle.Text("◆ Manual Trade Manager");
m_lblTitle.Font("Arial Bold");
m_lblTitle.FontSize(10);
m_lblTitle.Color(InpAccentColor);
if(!Add(m_lblTitle)) return false;
// --- Trade Section ---
y = 32;
if(!m_lblTradeHeader.Create(m_chart_id, m_name+"TradeH", m_subwin, x, y, x+w, y+16)) return false;
m_lblTradeHeader.Text("▶ Open Trade / باز کردن معامله");
m_lblTradeHeader.Font("Arial Bold");
m_lblTradeHeader.FontSize(9);
m_lblTradeHeader.Color(InpHeaderColor);
if(!Add(m_lblTradeHeader)) return false;
y = 52;
if(!m_lblLot.Create(m_chart_id, m_name+"lblLot", m_subwin, x, y, x+70, y+18)) return false;
m_lblLot.Text("Lot / لات:");
StyleLabel(m_lblLot, InpLabelColor);
if(!Add(m_lblLot)) return false;
// --- Single Section ---
y = 110 + Y_SHIFT;
if(!m_lblSingleHeader.Create(m_chart_id, m_name+"SingleH", m_subwin, x, y, x+w, y+16)) return false;
m_lblSingleHeader.Text("▶ Single Trade Settings / تنظیمات معامله تکی");
m_lblSingleHeader.Font("Arial Bold");
m_lblSingleHeader.FontSize(9);
m_lblSingleHeader.Color(InpHeaderColor);
if(!Add(m_lblSingleHeader)) return false;
y = 132 + Y_SHIFT;
if(!m_lblTP.Create(m_chart_id, m_name+"lblTP", m_subwin, x, y, x+95, y+18)) return false;
m_lblTP.Text("TP (points):");
StyleLabel(m_lblTP, InpLabelColor);
if(!Add(m_lblTP)) return false;
if(!m_lblSL.Create(m_chart_id, m_name+"lblSL", m_subwin, x+175, y, x+270, y+18)) return false;
m_lblSL.Text("SL (points):");
StyleLabel(m_lblSL, InpLabelColor);
if(!Add(m_lblSL)) return false;
y = 160 + Y_SHIFT;
if(!m_lblTrailStart.Create(m_chart_id, m_name+"lblTS", m_subwin, x, y, x+110, y+18)) return false;
m_lblTrailStart.Text("Trail Start:");
StyleLabel(m_lblTrailStart, InpLabelColor);
if(!Add(m_lblTrailStart)) return false;
if(!m_lblTrailStep.Create(m_chart_id, m_name+"lblTStep", m_subwin, x+175, y, x+280, y+18)) return false;
m_lblTrailStep.Text("Trail Step:");
StyleLabel(m_lblTrailStep, InpLabelColor);
if(!Add(m_lblTrailStep)) return false;
// --- Basket Section ---
y = 215 + Y_SHIFT;
if(!m_lblBasketHeader.Create(m_chart_id, m_name+"BasketH", m_subwin, x, y, x+w, y+16)) return false;
m_lblBasketHeader.Text("▶ Martingale Basket / تنظیمات سبد مارتینگل");
m_lblBasketHeader.Font("Arial Bold");
m_lblBasketHeader.FontSize(9);
m_lblBasketHeader.Color(C'255,150,40');
if(!Add(m_lblBasketHeader)) return false;
y = 237 + Y_SHIFT;
if(!m_lblBasketTP.Create(m_chart_id, m_name+"lblBTP", m_subwin, x, y, x+145, y+18)) return false;
m_lblBasketTP.Text("TP from Avg:");
StyleLabel(m_lblBasketTP, InpLabelColor);
if(!Add(m_lblBasketTP)) return false;
if(!m_lblMartin.Create(m_chart_id, m_name+"lblMartin", m_subwin, x+205, y, x+290, y+18)) return false;
m_lblMartin.Text("Martin x:");
StyleLabel(m_lblMartin, InpLabelColor);
if(!Add(m_lblMartin)) return false;
y = 265 + Y_SHIFT;
if(!m_lblBasketTrailStart.Create(m_chart_id, m_name+"lblBTS", m_subwin, x, y, x+120, y+18)) return false;
m_lblBasketTrailStart.Text("Basket Trail Start:");
StyleLabel(m_lblBasketTrailStart, InpLabelColor);
if(!Add(m_lblBasketTrailStart)) return false;
if(!m_lblBasketTrailStep.Create(m_chart_id, m_name+"lblBTStep", m_subwin, x+185, y, x+300, y+18)) return false;
m_lblBasketTrailStep.Text("Basket Step:");
StyleLabel(m_lblBasketTrailStep, InpLabelColor);
if(!Add(m_lblBasketTrailStep)) return false;
// --- Martingale mode rows (Manual / Auto) ---
y = 325 + Y_SHIFT;
if(!m_lblMartinMode.Create(m_chart_id, m_name+"lblMMode", m_subwin, x, y, x+165, y+18)) return false;
m_lblMartinMode.Text("Martin Mode / نوع مارتینگل:");
StyleLabel(m_lblMartinMode, InpLabelColor);
if(!Add(m_lblMartinMode)) return false;
y = 352 + Y_SHIFT;
if(!m_lblAutoDist.Create(m_chart_id, m_name+"lblADist", m_subwin, x, y, x+125, y+18)) return false;
m_lblAutoDist.Text("Next Gap (pts):");
StyleLabel(m_lblAutoDist, InpLabelColor);
if(!Add(m_lblAutoDist)) return false;
if(!m_lblAutoMax.Create(m_chart_id, m_name+"lblAMax", m_subwin, x+205, y, x+290, y+18)) return false;
m_lblAutoMax.Text("Max Trades:");
StyleLabel(m_lblAutoMax, InpLabelColor);
if(!Add(m_lblAutoMax)) return false;
// --- Status Section ---
y = 330 + Y_SHIFT + MA_SHIFT;
if(!m_lblStatusHeader.Create(m_chart_id, m_name+"StatusH", m_subwin, x, y, x+w, y+16)) return false;
m_lblStatusHeader.Text("▶ Status & Timer / وضعیت و تایمر");
m_lblStatusHeader.Font("Arial Bold");
m_lblStatusHeader.FontSize(9);
m_lblStatusHeader.Color(InpStatusColor);
if(!Add(m_lblStatusHeader)) return false;
y = 352 + Y_SHIFT + MA_SHIFT;
if(!m_lblTimer.Create(m_chart_id, m_name+"Timer", m_subwin, x, y, x+w, y+18)) return false;
m_lblTimer.Text("Candle Timer: --:--");
m_lblTimer.Font("Consolas");
m_lblTimer.FontSize(9);
m_lblTimer.Color(C'0,230,255');
if(!Add(m_lblTimer)) return false;
y = 374 + Y_SHIFT + MA_SHIFT;
if(!m_lblStatus.Create(m_chart_id, m_name+"Status", m_subwin, x, y, x+w, y+16)) return false;
m_lblStatus.Text("Trades: Buy 0 | Sell 0");
StyleLabel(m_lblStatus, InpTextColor);
if(!Add(m_lblStatus)) return false;
y = 394 + Y_SHIFT + MA_SHIFT;
if(!m_lblAvgBuy.Create(m_chart_id, m_name+"AvgBuy", m_subwin, x, y, x+w, y+16)) return false;
m_lblAvgBuy.Text("Buy Avg: -");
m_lblAvgBuy.Font("Arial Bold");
m_lblAvgBuy.FontSize(9);
m_lblAvgBuy.Color(InpBuyColor);
if(!Add(m_lblAvgBuy)) return false;
y = 412 + Y_SHIFT + MA_SHIFT;
if(!m_lblAvgSell.Create(m_chart_id, m_name+"AvgSell", m_subwin, x, y, x+w, y+16)) return false;
m_lblAvgSell.Text("Sell Avg: -");
m_lblAvgSell.Font("Arial Bold");
m_lblAvgSell.FontSize(9);
m_lblAvgSell.Color(InpSellColor);
if(!Add(m_lblAvgSell)) return false;
y = 432 + Y_SHIFT + MA_SHIFT;
if(!m_lblProfit.Create(m_chart_id, m_name+"Profit", m_subwin, x, y, x+w, y+18)) return false;
m_lblProfit.Text("Floating P/L: 0.00");
m_lblProfit.Font("Arial Bold");
m_lblProfit.FontSize(9);
m_lblProfit.Color(clrLime);
if(!Add(m_lblProfit)) return false;
// Expected money at TP / SL
y = 452 + Y_SHIFT + MA_SHIFT;
if(!m_lblTpMoney.Create(m_chart_id, m_name+"TpMoney", m_subwin, x, y, x+w, y+16)) return false;
m_lblTpMoney.Text("TP $: -");
m_lblTpMoney.Font("Arial Bold");
m_lblTpMoney.FontSize(9);
m_lblTpMoney.Color(C'0,220,120');
if(!Add(m_lblTpMoney)) return false;
y = 470 + Y_SHIFT + MA_SHIFT;
if(!m_lblSlMoney.Create(m_chart_id, m_name+"SlMoney", m_subwin, x, y, x+w, y+16)) return false;
m_lblSlMoney.Text("SL $: -");
m_lblSlMoney.Font("Arial Bold");
m_lblSlMoney.FontSize(9);
m_lblSlMoney.Color(C'255,90,70');
if(!Add(m_lblSlMoney)) return false;
// --- Lot capacity section (live) ---
const int ly = 494 + Y_SHIFT + MA_SHIFT;
if(!m_lblLotHeader.Create(m_chart_id, m_name+"LotH", m_subwin, x, ly, x+w, ly+16)) return false;
m_lblLotHeader.Text("▶ Lot Capacity (Live) / ظرفیت لات (زنده)");
m_lblLotHeader.Font("Arial Bold");
m_lblLotHeader.FontSize(9);
m_lblLotHeader.Color(C'190,150,255');
if(!Add(m_lblLotHeader)) return false;
if(!m_lblLotAcc.Create(m_chart_id, m_name+"LotAcc", m_subwin, x, ly+20, x+w, ly+36)) return false;
m_lblLotAcc.Text("Bal - | Eq - | Float - | 1:-");
m_lblLotAcc.Font("Consolas");
m_lblLotAcc.FontSize(8);
m_lblLotAcc.Color(InpLabelColor);
if(!Add(m_lblLotAcc)) return false;
if(!m_lblLotCap.Create(m_chart_id, m_name+"LotCap", m_subwin, x, ly+38, x+w, ly+54)) return false;
m_lblLotCap.Text("Free - | Max by Bal - | Used -");
m_lblLotCap.Font("Consolas");
m_lblLotCap.FontSize(8);
m_lblLotCap.Color(InpLabelColor);
if(!Add(m_lblLotCap)) return false;
if(!m_lblLotMax.Create(m_chart_id, m_name+"LotMax", m_subwin, x, ly+56, x+w, ly+74)) return false;
m_lblLotMax.Text("MAX LOT NOW Buy: - | Sell: -");
m_lblLotMax.Font("Arial Bold");
m_lblLotMax.FontSize(9);
m_lblLotMax.Color(C'0,230,100');
if(!Add(m_lblLotMax)) return false;
if(!m_lblLotNext.Create(m_chart_id, m_name+"LotNext", m_subwin, x, ly+76, x+w, ly+92)) return false;
m_lblLotNext.Text("Next Lot Buy: - | Sell: -");
m_lblLotNext.Font("Consolas");
m_lblLotNext.FontSize(8);
m_lblLotNext.Color(C'120,200,255');
if(!Add(m_lblLotNext)) return false;
return true;
}
//+------------------------------------------------------------------+
//| Create Edits |
//+------------------------------------------------------------------+
bool CManualPanel::CreateEdits()
{
// Lot
if(!m_editLot.Create(m_chart_id, m_name+"editLot", m_subwin, 80, 50, 150, 70)) return false;
m_editLot.Text(DoubleToString(g_LotSize, MathMax(2, g_VolDigits)));
StyleEdit(m_editLot);
if(!Add(m_editLot)) return false;
// TP / SL
if(!m_editTP.Create(m_chart_id, m_name+"editTP", m_subwin, 100, 130+Y_SHIFT, 160, 150+Y_SHIFT)) return false;
m_editTP.Text(IntegerToString(g_SingleTP));
StyleEdit(m_editTP);
if(!Add(m_editTP)) return false;
if(!m_editSL.Create(m_chart_id, m_name+"editSL", m_subwin, 275, 130+Y_SHIFT, 335, 150+Y_SHIFT)) return false;
m_editSL.Text(IntegerToString(g_SingleSL));
StyleEdit(m_editSL);
if(!Add(m_editSL)) return false;
// Trail Start / Step
if(!m_editTrailStart.Create(m_chart_id, m_name+"editTS", m_subwin, 115, 158+Y_SHIFT, 175, 178+Y_SHIFT)) return false;
m_editTrailStart.Text(IntegerToString(g_TrailStart));
StyleEdit(m_editTrailStart);
if(!Add(m_editTrailStart)) return false;
if(!m_editTrailStep.Create(m_chart_id, m_name+"editTStep", m_subwin, 285, 158+Y_SHIFT, 345, 178+Y_SHIFT)) return false;
m_editTrailStep.Text(IntegerToString(g_TrailStep));
StyleEdit(m_editTrailStep);
if(!Add(m_editTrailStep)) return false;
// Basket TP
if(!m_editBasketTP.Create(m_chart_id, m_name+"editBTP", m_subwin, 150, 235+Y_SHIFT, 210, 255+Y_SHIFT)) return false;
m_editBasketTP.Text(IntegerToString(g_BasketTP));
StyleEdit(m_editBasketTP);
if(!Add(m_editBasketTP)) return false;
// Martingale multiplier (same row as Basket TP)
if(!m_editMartin.Create(m_chart_id, m_name+"editMartin", m_subwin, 295, 235+Y_SHIFT, 355, 255+Y_SHIFT)) return false;
m_editMartin.Text(DoubleToString(g_Martin, 2));
StyleEdit(m_editMartin);
if(!Add(m_editMartin)) return false;
// Basket Trail Start / Step
if(!m_editBasketTrailStart.Create(m_chart_id, m_name+"editBTS", m_subwin, 130, 263+Y_SHIFT, 190, 283+Y_SHIFT)) return false;
m_editBasketTrailStart.Text(IntegerToString(g_BasketTrailStart));
StyleEdit(m_editBasketTrailStart);
if(!Add(m_editBasketTrailStart)) return false;
if(!m_editBasketTrailStep.Create(m_chart_id, m_name+"editBTStep", m_subwin, 295, 263+Y_SHIFT, 355, 283+Y_SHIFT)) return false;
m_editBasketTrailStep.Text(IntegerToString(g_BasketTrailStep));
StyleEdit(m_editBasketTrailStep);
if(!Add(m_editBasketTrailStep)) return false;
// Auto martingale: gap (points) from the last trade, and max trades per side
if(!m_editAutoDist.Create(m_chart_id, m_name+"editADist", m_subwin, 130, 350+Y_SHIFT, 190, 370+Y_SHIFT)) return false;
m_editAutoDist.Text(IntegerToString(g_AutoDist));
StyleEdit(m_editAutoDist);
if(!Add(m_editAutoDist)) return false;
if(!m_editAutoMax.Create(m_chart_id, m_name+"editAMax", m_subwin, 295, 350+Y_SHIFT, 355, 370+Y_SHIFT)) return false;
m_editAutoMax.Text(IntegerToString(g_AutoMax));
StyleEdit(m_editAutoMax);
if(!Add(m_editAutoMax)) return false;
return true;
}
//+------------------------------------------------------------------+
//| Create toggle "checkboxes" |
//+------------------------------------------------------------------+
bool CManualPanel::CreateCheckBoxes()
{
// --- Single Trail toggle ---
if(!m_btnSingleTrail.Create(m_chart_id, m_name+"btnSingleTrail", m_subwin, 12, 186+Y_SHIFT, 32, 206+Y_SHIFT)) return false;
StyleToggle(m_btnSingleTrail, g_EnableSingleTrail);
if(!Add(m_btnSingleTrail)) return false;
if(!m_lblChkSingle.Create(m_chart_id, m_name+"lblChkSingle", m_subwin, 38, 188+Y_SHIFT, 350, 206+Y_SHIFT)) return false;
m_lblChkSingle.Text("Single Trail ON | تریل معامله تکی");
m_lblChkSingle.Color(InpEditText);
m_lblChkSingle.Font("Arial");
m_lblChkSingle.FontSize(8);
if(!Add(m_lblChkSingle)) return false;
// --- Basket Trail toggle ---
if(!m_btnBasketTrail.Create(m_chart_id, m_name+"btnBasketTrail", m_subwin, 12, 296+Y_SHIFT, 32, 316+Y_SHIFT)) return false;
StyleToggle(m_btnBasketTrail, g_EnableBasketTrail);
if(!Add(m_btnBasketTrail)) return false;
if(!m_lblChkBasket.Create(m_chart_id, m_name+"lblChkBasket", m_subwin, 38, 298+Y_SHIFT, 175, 316+Y_SHIFT)) return false;
m_lblChkBasket.Text("Basket Trail | تریل سبد");
m_lblChkBasket.Color(InpEditText);
m_lblChkBasket.Font("Arial");
m_lblChkBasket.FontSize(8);
if(!Add(m_lblChkBasket)) return false;
// --- Basket TP toggle ---
if(!m_btnBasketTP.Create(m_chart_id, m_name+"btnBasketTP", m_subwin, 185, 296+Y_SHIFT, 205, 316+Y_SHIFT)) return false;
StyleToggle(m_btnBasketTP, g_EnableBasketTP);
if(!Add(m_btnBasketTP)) return false;
if(!m_lblChkBTP.Create(m_chart_id, m_name+"lblChkBTP", m_subwin, 210, 298+Y_SHIFT, 360, 316+Y_SHIFT)) return false;
m_lblChkBTP.Text("Basket TP | تیک‌پروفیت سبد");
m_lblChkBTP.Color(InpEditText);
m_lblChkBTP.Font("Arial");
m_lblChkBTP.FontSize(8);
if(!Add(m_lblChkBTP)) return false;
// --- Martingale mode selector (Manual / Auto) ---
if(!m_btnMartinMode.Create(m_chart_id, m_name+"btnMartinMode", m_subwin, 185, 322+Y_SHIFT, 355, 344+Y_SHIFT)) return false;
StyleMartinMode(g_PendingMartinAuto);
if(!Add(m_btnMartinMode)) return false;
return true;
}
//+------------------------------------------------------------------+
//| Style a toggle button (ON = green check, OFF = dark empty) |
//+------------------------------------------------------------------+
void CManualPanel::StyleToggle(CButton &btn, bool on)
{
if(on)
{
btn.Text("✓");
btn.ColorBackground(C'0,160,80'); // green fill
btn.Color(clrWhite);
btn.ColorBorder(C'0,220,120');
}
else
{
btn.Text(" ");
btn.ColorBackground(C'30,45,75'); // dark navy fill
btn.Color(InpAccentColor);
btn.ColorBorder(InpAccentColor); // cyan border – clearly visible
}
btn.Font("Arial Bold");
btn.FontSize(11);
}
//+------------------------------------------------------------------+
//| Martingale mode button look (Manual = grey-blue, Auto = green) |
//+------------------------------------------------------------------+
void CManualPanel::StyleMartinMode(const bool autoMode)
{
if(autoMode)
{
m_btnMartinMode.Text("AUTO / اتوماتیک");
m_btnMartinMode.ColorBackground(C'0,160,80');
m_btnMartinMode.Color(clrWhite);
m_btnMartinMode.ColorBorder(C'0,220,120');
}
else
{
m_btnMartinMode.Text("MANUAL / دستی");
m_btnMartinMode.ColorBackground(C'60,70,95');
m_btnMartinMode.Color(clrWhite);
m_btnMartinMode.ColorBorder(InpAccentColor);
}
m_btnMartinMode.Font("Arial Bold");
m_btnMartinMode.FontSize(9);
}
//+------------------------------------------------------------------+
//| Enable / grey-out the Auto-martingale fields |
//+------------------------------------------------------------------+
void CManualPanel::SetAutoFieldsEnabled(const bool en)
{
const color dimTxt = C'105,105,112';
m_editAutoDist.ReadOnly(!en);
m_editAutoMax.ReadOnly(!en);
if(en)
{
m_editAutoDist.Color(InpEditText); m_editAutoDist.ColorBackground(InpEditBg); m_editAutoDist.ColorBorder(C'70,75,90');
m_editAutoMax.Color(InpEditText); m_editAutoMax.ColorBackground(InpEditBg); m_editAutoMax.ColorBorder(C'70,75,90');
m_lblAutoDist.Color(InpLabelColor);
m_lblAutoMax.Color(InpLabelColor);
}
else
{
m_editAutoDist.Color(dimTxt); m_editAutoDist.ColorBackground(C'38,38,42'); m_editAutoDist.ColorBorder(C'55,55,60');
m_editAutoMax.Color(dimTxt); m_editAutoMax.ColorBackground(C'38,38,42'); m_editAutoMax.ColorBorder(C'55,55,60');
m_lblAutoDist.Color(dimTxt);
m_lblAutoMax.Color(dimTxt);
}
}
//+------------------------------------------------------------------+
//| Create Buttons |
//+------------------------------------------------------------------+
bool CManualPanel::CreateButtons()
{
// SELL (left) / BUY (right)
if(!m_btnSell.Create(m_chart_id, m_name+"btnSell", m_subwin, 160, 48, 245, 80)) return false;
m_btnSell.Text("SELL");
m_btnSell.ColorBackground(InpSellColor);
m_btnSell.Color(clrWhite);
m_btnSell.ColorBorder(C'160,30,30');
m_btnSell.Font("Arial Bold");
m_btnSell.FontSize(12);
if(!Add(m_btnSell)) return false;
g_ObjSell = m_btnSell.Name();
if(!m_btnBuy.Create(m_chart_id, m_name+"btnBuy", m_subwin, 265, 48, 350, 80)) return false;
m_btnBuy.Text("BUY");
m_btnBuy.ColorBackground(InpBuyColor);
m_btnBuy.Color(clrWhite);
m_btnBuy.ColorBorder(C'0,140,60');
m_btnBuy.Font("Arial Bold");
m_btnBuy.FontSize(12);
if(!Add(m_btnBuy)) return false;
g_ObjBuy = m_btnBuy.Name();
// Apply Settings – directly under the Lot row
if(!m_btnApply.Create(m_chart_id, m_name+"btnApply", m_subwin, 12, 86, 410, 112)) return false;
m_btnApply.Text("Apply Settings / اعمال تنظیمات");
m_btnApply.ColorBackground(InpAccentColor);
m_btnApply.Color(clrBlack);
m_btnApply.ColorBorder(C'0,160,145');
m_btnApply.Font("Arial Bold");
m_btnApply.FontSize(9);
if(!Add(m_btnApply)) return false;
// Close buttons
int y = 494 + Y_SHIFT + LOT_SHIFT;
int btnW = 128;
int gap = 8;
if(!m_btnCloseAllBuy.Create(m_chart_id, m_name+"btnCBuy", m_subwin, 12, y, 12+btnW, y+26)) return false;
m_btnCloseAllBuy.Text("Close Buys");
m_btnCloseAllBuy.ColorBackground(C'0,130,60');
m_btnCloseAllBuy.Color(clrWhite);
m_btnCloseAllBuy.ColorBorder(C'0,90,40');
m_btnCloseAllBuy.Font("Arial Bold");
m_btnCloseAllBuy.FontSize(8);
if(!Add(m_btnCloseAllBuy)) return false;
if(!m_btnCloseAllSell.Create(m_chart_id, m_name+"btnCSell", m_subwin, 12+btnW+gap, y, 12+2*btnW+gap, y+26)) return false;
m_btnCloseAllSell.Text("Close Sells");
m_btnCloseAllSell.ColorBackground(C'180,40,40');
m_btnCloseAllSell.Color(clrWhite);
m_btnCloseAllSell.ColorBorder(C'120,20,20');
m_btnCloseAllSell.Font("Arial Bold");
m_btnCloseAllSell.FontSize(8);
if(!Add(m_btnCloseAllSell)) return false;
if(!m_btnCloseAll.Create(m_chart_id, m_name+"btnCAll", m_subwin, 12+2*(btnW+gap), y, 12+3*btnW+2*gap, y+26)) return false;
m_btnCloseAll.Text("Close All");
m_btnCloseAll.ColorBackground(C'80,80,95');
m_btnCloseAll.Color(clrWhite);
m_btnCloseAll.ColorBorder(C'50,50,60');
m_btnCloseAll.Font("Arial Bold");
m_btnCloseAll.FontSize(8);
if(!Add(m_btnCloseAll)) return false;
// Remove SL button
y = 526 + Y_SHIFT + LOT_SHIFT;
if(!m_btnRemoveSL.Create(m_chart_id, m_name+"btnRemSL", m_subwin, 12, y, 410, y+26)) return false;
m_btnRemoveSL.Text("Remove SL / حذف حد ضرر");
m_btnRemoveSL.ColorBackground(C'180,100,20');
m_btnRemoveSL.Color(clrWhite);
m_btnRemoveSL.ColorBorder(C'140,70,10');
m_btnRemoveSL.Font("Arial Bold");
m_btnRemoveSL.FontSize(9);
if(!Add(m_btnRemoveSL)) return false;
// Delete all pending orders (Buy/Sell Limit & Stop)
y = 558 + Y_SHIFT + LOT_SHIFT;
if(!m_btnDeleteOrders.Create(m_chart_id, m_name+"btnDelOrd", m_subwin, 12, y, 410, y+26)) return false;
m_btnDeleteOrders.Text("Delete All Orders / حذف همه سفارش‌ها");
m_btnDeleteOrders.ColorBackground(C'140,40,80');
m_btnDeleteOrders.Color(clrWhite);
m_btnDeleteOrders.ColorBorder(C'100,20,50');
m_btnDeleteOrders.Font("Arial Bold");
m_btnDeleteOrders.FontSize(9);
if(!Add(m_btnDeleteOrders)) return false;
return true;
}
//+------------------------------------------------------------------+
//| Read / Write panel values |
//+------------------------------------------------------------------+
void CManualPanel::ReadValuesFromPanel()
{
// Lot: invalid / empty input keeps the previous lot instead of silently dropping to minimum
double lot = StringToDouble(CleanNum(m_editLot.Text()));
if(lot <= 0.0) lot = g_LotSize;
g_LotSize = NormalizeLot(lot);
// Martingale multiplier: invalid / empty input keeps the previous value; clamped to 1.0 .. 10.0
double mt = StringToDouble(CleanNum(m_editMartin.Text()));
if(mt <= 0.0) mt = g_Martin;
g_Martin = ClampMartin(mt);
g_SingleTP = ParseIntField(m_editTP.Text());
g_SingleSL = ParseIntField(m_editSL.Text());
g_TrailStart = ParseIntField(m_editTrailStart.Text());
g_TrailStep = ParseIntField(m_editTrailStep.Text());
g_BasketTP = ParseIntField(m_editBasketTP.Text());
g_BasketTrailStart = ParseIntField(m_editBasketTrailStart.Text());
g_BasketTrailStep = ParseIntField(m_editBasketTrailStep.Text());
// g_Enable* toggled only by their buttons
if(g_SingleTP < 0) g_SingleTP = 0;
if(g_SingleSL < 0) g_SingleSL = 0;
if(g_TrailStart < 0) g_TrailStart = 0;
if(g_TrailStep < 1) g_TrailStep = 1;
if(g_BasketTP < 0) g_BasketTP = 0;
if(g_BasketTrailStart < 0) g_BasketTrailStart = 0;
if(g_BasketTrailStep < 1) g_BasketTrailStep = 1;
// Auto martingale: gap in points (>=1) and max trades per side (2..100, incl. the first trade)
int ad = ParseIntField(m_editAutoDist.Text());
int am = ParseIntField(m_editAutoMax.Text());
if(ad < 1) ad = g_AutoDist;
if(am < 1) am = g_AutoMax;
g_AutoDist = MathMax(1, ad);
g_AutoMax = MathMin(100, MathMax(2, am));
}
void CManualPanel::WriteValuesToPanel()
{
m_editLot.Text(DoubleToString(g_LotSize, MathMax(2, g_VolDigits)));
m_editMartin.Text(DoubleToString(g_Martin, 2));
m_editTP.Text(IntegerToString(g_SingleTP));
m_editSL.Text(IntegerToString(g_SingleSL));
m_editTrailStart.Text(IntegerToString(g_TrailStart));
m_editTrailStep.Text(IntegerToString(g_TrailStep));
m_editBasketTP.Text(IntegerToString(g_BasketTP));
m_editBasketTrailStart.Text(IntegerToString(g_BasketTrailStart));
m_editBasketTrailStep.Text(IntegerToString(g_BasketTrailStep));
m_editAutoDist.Text(IntegerToString(g_AutoDist));
m_editAutoMax.Text(IntegerToString(g_AutoMax));
StyleMartinMode(g_PendingMartinAuto);
SetAutoFieldsEnabled(g_PendingMartinAuto);
// Show pending toggle state on panel (may differ from committed until Apply)
StyleToggle(m_btnSingleTrail, g_PendingSingleTrail);
StyleToggle(m_btnBasketTrail, g_PendingBasketTrail);
StyleToggle(m_btnBasketTP, g_PendingBasketTP);
}
//+------------------------------------------------------------------+
//| Button Handlers |
//+------------------------------------------------------------------+
void CManualPanel::OnClickBuy()
{
// Fast path in OnChartEvent already executed this click
if(g_BuyFastPending > 0) { g_BuyFastPending--; return; }
ExecuteMarket(ORDER_TYPE_BUY);
}
void CManualPanel::OnClickSell()
{
if(g_SellFastPending > 0) { g_SellFastPending--; return; }
ExecuteMarket(ORDER_TYPE_SELL);
}
void CManualPanel::OnClickApply()
{
// Commit numeric fields from panel → globals (normalised)
ReadValuesFromPanel();
// Commit pending toggle states → active management flags
g_EnableSingleTrail = g_PendingSingleTrail;
g_EnableBasketTrail = g_PendingBasketTrail;
g_EnableBasketTP = g_PendingBasketTP;
g_MartinAuto = g_PendingMartinAuto;
// Show the normalised values back in the fields
WriteValuesToPanel();
Print("Settings COMMITTED | Lot=", DoubleToString(g_LotSize, g_VolDigits),
" Martin=x", DoubleToString(g_Martin, 2),
" TP=", g_SingleTP, " SL=", g_SingleSL,
" TrailStart=", g_TrailStart, " TrailStep=", g_TrailStep,
" BasketTP=", g_BasketTP,
" BasketTrailStart=", g_BasketTrailStart, " BasketTrailStep=", g_BasketTrailStep,
" SingleTrail=", g_EnableSingleTrail,
" BasketTrail=", g_EnableBasketTrail,
" BasketTP_on=", g_EnableBasketTP,
" MartinMode=", g_MartinAuto ? "AUTO" : "MANUAL",
" AutoGap=", g_AutoDist, " AutoMaxTrades=", g_AutoMax);
// Invalidate stable TP caches so they are recomputed from new settings
g_LastBuyCount = 0;
g_LastSellCount = 0;
g_StableBuyTP = 0.0;
g_StableSellTP = 0.0;
// Push new TP/SL onto EVERY open managed position right now
MqlTick t;
if(SymbolInfoTick(_Symbol, t)) { g_Bid = t.bid; g_Ask = t.ask; }
int n = ForceApplySettingsToAll();
Print("Apply complete: sent ", n, " modify request(s)");
UpdateStatus(true);
ChartRedraw(0);
}
void CManualPanel::OnClickCloseAllBuy()
{
int sent = CloseManaged(1);
Print("Close Buys: ", sent, " request(s) sent");
g_LastBuyCount = 0;
g_StableBuyTP = 0.0;
ChartRedraw(0);
}
void CManualPanel::OnClickCloseAllSell()
{
int sent = CloseManaged(2);
Print("Close Sells: ", sent, " request(s) sent");
g_LastSellCount = 0;
g_StableSellTP = 0.0;
ChartRedraw(0);
}
void CManualPanel::OnClickCloseAll()
{
int sent = CloseManaged(0);
Print("Close All: ", sent, " request(s) sent");
g_LastBuyCount = 0;
g_LastSellCount = 0;
g_StableBuyTP = 0.0;
g_StableSellTP = 0.0;
ChartRedraw(0);
}
void CManualPanel::OnClickRemoveSL()
{
// ManagePositions() re-applies the fixed SL (g_SingleSL > 0) on every tick, so the
// fixed-SL setting must be zeroed or the removed SL is put straight back.
// Trailing is NOT touched: it keeps working independently of the fixed SL.
g_SingleSL = 0;
m_editSL.Text("0"); // only the SL field changes; other unapplied edits stay
m_lastSig = 0; // force TP$/SL$ labels to recompute
m_hardRefresh = MathMax(m_hardRefresh, 2);
int removed = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(!posInfo.SelectByIndex(i)) continue;
if(!IsManagedPosition()) continue;
if(posInfo.StopLoss() != 0.0)
{
if(SendSLTP(posInfo.Ticket(), 0.0, posInfo.TakeProfit()))
removed++;
}
}
Print("Remove SL: fixed SL setting=0 (trailing unchanged), clear request sent for ", removed, " position(s)");
UpdateStatus(true);
ChartRedraw(0);
}
void CManualPanel::OnClickDeleteOrders()
{
int sent = 0;
int failed = 0;
// Pending orders: Buy Limit/Stop, Sell Limit/Stop (+ StopLimit variants)
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0) continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
if(InpMagicNumber != 0)
{
long magic = OrderGetInteger(ORDER_MAGIC);
if(magic != InpMagicNumber && magic != 0) continue;
}
ENUM_ORDER_TYPE otype = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
if(otype != ORDER_TYPE_BUY_LIMIT && otype != ORDER_TYPE_BUY_STOP &&
otype != ORDER_TYPE_SELL_LIMIT && otype != ORDER_TYPE_SELL_STOP &&
otype != ORDER_TYPE_BUY_STOP_LIMIT && otype != ORDER_TYPE_SELL_STOP_LIMIT)
continue;
MqlTradeRequest r;
MqlTradeResult res;
ZeroMemory(r);
ZeroMemory(res);
r.action = TRADE_ACTION_REMOVE;
r.order = ticket;
if(OrderSendAsync(r, res)) sent++;
else
{
failed++;
Print("Order delete send FAIL #", ticket, " err=", GetLastError());
}
}
Print("Delete All Orders: sent=", sent, " failed=", failed);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Toggle handlers – UI / pending only until Apply is clicked |
//+------------------------------------------------------------------+
void CManualPanel::OnClickSingleTrail()
{
g_PendingSingleTrail = !g_PendingSingleTrail;
StyleToggle(m_btnSingleTrail, g_PendingSingleTrail);
Print("Single Trail (pending) = ", g_PendingSingleTrail ? "ON" : "OFF",
" → click Apply to commit");
}
void CManualPanel::OnClickBasketTrail()
{
g_PendingBasketTrail = !g_PendingBasketTrail;
StyleToggle(m_btnBasketTrail, g_PendingBasketTrail);
Print("Basket Trail (pending) = ", g_PendingBasketTrail ? "ON" : "OFF",
" → click Apply to commit");
}
void CManualPanel::OnClickBasketTP()
{
g_PendingBasketTP = !g_PendingBasketTP;
StyleToggle(m_btnBasketTP, g_PendingBasketTP);
Print("Basket TP (pending) = ", g_PendingBasketTP ? "ON" : "OFF",
" → click Apply to commit");
}
void CManualPanel::OnClickMartinMode()
{
g_PendingMartinAuto = !g_PendingMartinAuto;
StyleMartinMode(g_PendingMartinAuto);
SetAutoFieldsEnabled(g_PendingMartinAuto); // grey / active immediately
Print("Martingale mode (pending) = ", g_PendingMartinAuto ? "AUTO" : "MANUAL",
" → click Apply to commit");
}
//+------------------------------------------------------------------+
//| Permanently destroy the Minimize button |
//+------------------------------------------------------------------+
void CManualPanel::KillMinimize()
{
string panelName = Name();
int total = ObjectsTotal(0, -1, -1);
for(int i = total - 1; i >= 0; i--)
{
string nm = ObjectName(0, i, -1, -1);
if(StringFind(nm, panelName) < 0) continue;
// ONLY exact system caption buttons – never touch our controls
if(StringFind(nm, "Minimize") >= 0 ||
StringFind(nm, "Maximize") >= 0 ||
StringFind(nm, "Restore") >= 0)
{
ObjectDelete(0, nm);
}
}
}
//+------------------------------------------------------------------+
//| Profit in account currency between two prices |
//+------------------------------------------------------------------+
double CalcMoneyAtPrice(ENUM_POSITION_TYPE type, double openPrice, double closePrice, double lots)
{
// Prefer broker's OrderCalcProfit – matches MT5 Trade tab exactly
double profit = 0.0;
ENUM_ORDER_TYPE ot = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
if(OrderCalcProfit(ot, _Symbol, lots, openPrice, closePrice, profit))
return NormalizeDouble(profit, 2);
// Fallback: tick-value method
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
if(tickSize <= 0.0 || tickValue <= 0.0)
return 0.0;
double ticks = (closePrice - openPrice) / tickSize;
if(type == POSITION_TYPE_SELL) ticks = -ticks;
return NormalizeDouble(ticks * tickValue * lots, 2);
}
//+------------------------------------------------------------------+
//| One pass over all positions: counts, sums, floating P/L, tickets |
//+------------------------------------------------------------------+
void ScanPositions(const bool wantSig)
{
g_Buy.count = 0; g_Buy.lots = 0.0; g_Buy.sum = 0.0; g_Buy.lastLot = 0.0; g_Buy.lastPrice = 0.0; g_Buy.lastMs = 0;
g_Sell.count = 0; g_Sell.lots = 0.0; g_Sell.sum = 0.0; g_Sell.lastLot = 0.0; g_Sell.lastPrice = 0.0; g_Sell.lastMs = 0;
g_FloatPL = 0.0;
g_PosSig = 0;
int total = PositionsTotal();
if(total <= 0) return;
if(ArraySize(g_BuyTk) < total)
{
ArrayResize(g_BuyTk, total + 8);
ArrayResize(g_SellTk, total + 8);
}
ulong sig = 1469598103;
for(int i = total - 1; i >= 0; i--)
{
if(!posInfo.SelectByIndex(i)) continue;
if(!IsManagedPosition()) continue;
const double vol = posInfo.Volume();
const double op = posInfo.PriceOpen();
const ulong tms = (ulong)posInfo.TimeMsc();
g_FloatPL += posInfo.Profit() + posInfo.Swap() + posInfo.Commission();
if(posInfo.PositionType() == POSITION_TYPE_BUY)
{
g_BuyTk[g_Buy.count] = posInfo.Ticket();
g_Buy.count++;
g_Buy.lots += vol;
g_Buy.sum += op * vol;
if(tms >= g_Buy.lastMs) { g_Buy.lastMs = tms; g_Buy.lastLot = vol; g_Buy.lastPrice = op; }
}
else
{
g_SellTk[g_Sell.count] = posInfo.Ticket();
g_Sell.count++;
g_Sell.lots += vol;
g_Sell.sum += op * vol;
if(tms >= g_Sell.lastMs) { g_Sell.lastMs = tms; g_Sell.lastLot = vol; g_Sell.lastPrice = op; }
}
if(wantSig)
{
sig = HashMix(sig, (double)posInfo.Ticket());
sig = HashMix(sig, vol);
sig = HashMix(sig, op);
sig = HashMix(sig, posInfo.StopLoss());
sig = HashMix(sig, posInfo.TakeProfit());
}
}
g_PosSig = sig;
}
//+------------------------------------------------------------------+
//| Real-time Floating P/L label (throttled, only on change) |
//+------------------------------------------------------------------+
void CManualPanel::UpdateFloatingPL(const bool force)
{
const ulong now = GetTickCount64();
if(!force && now - m_lastPLms < 80) return;
m_lastPLms = now;
SetLabel(m_lblProfit,
StringFormat("Floating P/L: %.2f %s", g_FloatPL, g_Ccy),
(g_FloatPL >= 0.0) ? C'0,230,100' : C'255,90,70');
}
//+------------------------------------------------------------------+
//| Real-time Lot Capacity block (throttled 100 ms, from OnTick/Timer)|
//| Uses g_Buy / g_Sell from the scan done just before by the caller |
//+------------------------------------------------------------------+
void CManualPanel::UpdateLotInfo(const bool force)
{
const ulong now = GetTickCount64();
if(!force && now - m_lastLotMs < 100) return;
m_lastLotMs = now;
const double bal = AccountInfoDouble(ACCOUNT_BALANCE);
const double eq = AccountInfoDouble(ACCOUNT_EQUITY);
const double flt = AccountInfoDouble(ACCOUNT_PROFIT);
const double freeM = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
const int lev = (int)AccountInfoInteger(ACCOUNT_LEVERAGE);
const double usedLots = TotalVolumeAllSymbols(); // all symbols
// ---- Max total lot capacity -------------------------------------------------
// * Open trades exist : "Max by Eq" -> REMAINING lots = (Equity capacity - margin used by all symbols), live
// (equity moves with floating P/L, margin/lot moves with price)
// * No open trades : "Max by Bal" -> uses BALANCE, FROZEN. It is recalculated
// only when balance / leverage / symbol / margin-% change
// (or when the last trade has just been closed).
static bool s_wasOpen = false;
static bool s_cached = false;
static double s_cBal = 0.0;
static int s_cLev = 0;
static double s_cPct = 0.0;
static string s_cSym = "";
static double s_frozen = 0.0;
const bool hasTrades = (PositionsTotal() > 0);
double maxCap;
string capName;
if(hasTrades)
{
// Remaining capacity (in lots of THIS symbol) = Equity capacity - margin already used by
// ALL open positions (all symbols), converted to lots of this symbol via margin-per-lot.
// (Plain lot subtraction would mix symbols with different margin per lot.)
const double mplNow = MarginPerLotNow();
const double usedEq = (mplNow > 0.0) ? AccountInfoDouble(ACCOUNT_MARGIN) / mplNow : 0.0;
maxCap = FloorToStep(MathMax(0.0, MaxLotByBalance(eq) - usedEq));
capName = "Max by Eq";
s_wasOpen = true;
}
else
{
const bool changed = (!s_cached || s_wasOpen ||
MathAbs(bal - s_cBal) > 0.005 || lev != s_cLev ||
InpMaxMarginUsePct != s_cPct || _Symbol != s_cSym);
if(changed)
{
const double v = MaxLotByBalance(bal);
if(v > 0.0 || !s_cached) // do not cache a failed (0) calc when a good value exists
{
s_frozen = v;
s_cBal = bal;
s_cLev = lev;
s_cPct = InpMaxMarginUsePct;
s_cSym = _Symbol;
s_cached = true;
}
s_wasOpen = false;
}
maxCap = s_frozen;
capName = "Max by Bal";
}
const double maxBuy = MaxLotByMargin(ORDER_TYPE_BUY, freeM);
const double maxSell = MaxLotByMargin(ORDER_TYPE_SELL, freeM);
bool cappedB = false, cappedS = false;
const double nextB = ApplyCap(PlannedLot(true), maxBuy, cappedB);
const double nextS = ApplyCap(PlannedLot(false), maxSell, cappedS);
SetLabel(m_lblLotAcc,
StringFormat("Bal %.2f | Eq %.2f | Float %+.2f | 1:%d", bal, eq, flt, lev),
(flt >= 0.0) ? C'0,230,100' : C'255,110,80');
SetLabel(m_lblLotCap,
StringFormat("Free %.2f | %s %s | Used %s lot",
freeM, capName, LotStr(maxCap), LotStr(usedLots)),
InpLabelColor);
color maxClr = C'0,230,100';
if(maxBuy <= 0.0 && maxSell <= 0.0) maxClr = C'255,70,70';
else if(cappedB || cappedS) maxClr = C'255,170,40';
SetLabel(m_lblLotMax,
StringFormat("MAX LOT NOW Buy: %s | Sell: %s", LotStr(maxBuy), LotStr(maxSell)),
maxClr);
const string mtxt = (g_Martin > 1.0) ? StringFormat("Martin x%.2f", g_Martin) : "fixed lot";
SetLabel(m_lblLotNext,
StringFormat("Next Lot Buy: %s | Sell: %s (%s)%s",
LotStr(nextB), LotStr(nextS), mtxt,
(cappedB || cappedS) ? " CAPPED" : ""),
C'120,200,255');
}
//+------------------------------------------------------------------+
//| Update Status (cheap part every call, heavy part only on change) |
//+------------------------------------------------------------------+
void CManualPanel::UpdateStatus(const bool forceIn)
{
bool force = forceIn;
if(m_hardRefresh > 0) force = true;
ScanPositions(true);
const int buyCount = g_Buy.count;
const int sellCount = g_Sell.count;
const double buyLots = g_Buy.lots;
const double sellLots = g_Sell.lots;
const double buySum = g_Buy.sum;
const double sellSum = g_Sell.sum;
SetLabel(m_lblStatus,
StringFormat("Trades: Buy %d (%.2f) | Sell %d (%.2f)",
buyCount, buyLots, sellCount, sellLots),
InpTextColor);
if(buyCount > 0 && buyLots > 0.0)
SetLabel(m_lblAvgBuy,
StringFormat("Buy Avg: %s | Count: %d",
DoubleToString(buySum / buyLots, _Digits), buyCount),
InpBuyColor);
else
SetLabel(m_lblAvgBuy, "Buy Avg: -", InpBuyColor);
if(sellCount > 0 && sellLots > 0.0)
SetLabel(m_lblAvgSell,
StringFormat("Sell Avg: %s | Count: %d",
DoubleToString(sellSum / sellLots, _Digits), sellCount),
InpSellColor);
else
SetLabel(m_lblAvgSell, "Sell Avg: -", InpSellColor);
UpdateFloatingPL(true);
UpdateLotInfo(true);
// ---- Heavy part (OrderCalcProfit etc.) only when inputs changed ----
ulong sig = g_PosSig;
sig = HashMix(sig, g_LotSize);
sig = HashMix(sig, (double)g_SingleTP);
sig = HashMix(sig, (double)g_SingleSL);
sig = HashMix(sig, (double)g_BasketTP);
sig = HashMix(sig, g_EnableBasketTP ? 1.0 : 0.0);
const ulong nowMs = GetTickCount64();
const bool idle = (buyCount + sellCount == 0);
if(!force && sig == m_lastSig && !(idle && nowMs - m_lastHeavyMs > 3000))
return;
m_lastSig = sig;
m_lastHeavyMs = nowMs;
double buyTpMoney = 0.0, sellTpMoney = 0.0;
double buySlMoney = 0.0, sellSlMoney = 0.0;
bool anyBuyTp = false, anySellTp = false;
bool anyBuySl = false, anySellSl = false;
const bool buyBasket = (buyCount > 1);
const bool sellBasket = (sellCount > 1);
bool buyHasSlOnPos = false, sellHasSlOnPos = false;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(!posInfo.SelectByIndex(i)) continue;
if(!IsManagedPosition()) continue;
ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)posInfo.PositionType();
double vol = posInfo.Volume();
double op = posInfo.PriceOpen();
double tp = posInfo.TakeProfit();
double sl = posInfo.StopLoss();
bool isBuy = (ptype == POSITION_TYPE_BUY);
bool isBasketSide = isBuy ? buyBasket : sellBasket;
// --- TP $ ---
double thisTp = 0.0;
bool hasTp = false;
if(tp > 0.0)
{
thisTp = CalcMoneyAtPrice(ptype, op, tp, vol);
hasTp = true;
}
else if(!isBasketSide && g_SingleTP > 0)
{
double estTp = isBuy
? NormalizeDouble(op + g_SingleTP * _Point, _Digits)
: NormalizeDouble(op - g_SingleTP * _Point, _Digits);
thisTp = CalcMoneyAtPrice(ptype, op, estTp, vol);
hasTp = true;
}
else if(isBasketSide && g_EnableBasketTP && g_BasketTP > 0)
{
double avg = isBuy
? (buyLots > 0 ? buySum / buyLots : op)
: (sellLots > 0 ? sellSum / sellLots : op);
double estTp = isBuy
? NormalizeDouble(avg + g_BasketTP * _Point, _Digits)
: NormalizeDouble(avg - g_BasketTP * _Point, _Digits);
thisTp = CalcMoneyAtPrice(ptype, op, estTp, vol);
hasTp = true;
}
if(hasTp)
{
if(isBuy) { buyTpMoney += thisTp; anyBuyTp = true; }
else { sellTpMoney += thisTp; anySellTp = true; }
}
// --- SL $ ---
double thisSl = 0.0;
bool hasSl = false;
if(sl > 0.0)
{
if(isBuy) buyHasSlOnPos = true; else sellHasSlOnPos = true;
thisSl = CalcMoneyAtPrice(ptype, op, sl, vol);
hasSl = true;
}
else if(!isBasketSide && g_SingleSL > 0)
{
double estSl = isBuy
? NormalizeDouble(op - g_SingleSL * _Point, _Digits)
: NormalizeDouble(op + g_SingleSL * _Point, _Digits);
thisSl = CalcMoneyAtPrice(ptype, op, estSl, vol);
hasSl = true;
}
// Basket without SL → skip SL $ for that side
if(hasSl)
{
if(isBuy) { buySlMoney += thisSl; anyBuySl = true; }
else { sellSlMoney += thisSl; anySellSl = true; }
}
}
// --- Separate Buy / Sell TP $ display ---
if(idle)
{
// No positions: estimate next order for both sides
double lot = g_LotSize;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Price may be 0 right after a chart/timeframe change → fall back to last known tick
if(ask <= 0.0 || bid <= 0.0)
{
MqlTick lt;
if(SymbolInfoTick(_Symbol, lt)) { ask = lt.ask; bid = lt.bid; }
if(ask <= 0.0) ask = g_Ask;
if(bid <= 0.0) bid = g_Bid;
}
if((ask <= 0.0 || bid <= 0.0) && m_hardRefresh < 2) m_hardRefresh = 2; // keep retrying
string tpLine = "TP $ Buy: - | Sell: -";
string slLine = "SL $ Buy: - | Sell: -";
if(g_SingleTP > 0 && lot > 0.0 && ask > 0.0 && bid > 0.0)
{
double estTpBuy = NormalizeDouble(ask + g_SingleTP * _Point, _Digits);
double estTpSell = NormalizeDouble(bid - g_SingleTP * _Point, _Digits);
double mBuy = CalcMoneyAtPrice(POSITION_TYPE_BUY, ask, estTpBuy, lot);
double mSell = CalcMoneyAtPrice(POSITION_TYPE_SELL, bid, estTpSell, lot);
tpLine = StringFormat("TP $ Buy: +%.2f | Sell: +%.2f (%s)", mBuy, mSell, g_Ccy);
}
if(g_SingleSL > 0 && lot > 0.0 && ask > 0.0 && bid > 0.0)
{
double estSlBuy = NormalizeDouble(ask - g_SingleSL * _Point, _Digits);
double estSlSell = NormalizeDouble(bid + g_SingleSL * _Point, _Digits);
double mBuy = CalcMoneyAtPrice(POSITION_TYPE_BUY, ask, estSlBuy, lot);
double mSell = CalcMoneyAtPrice(POSITION_TYPE_SELL, bid, estSlSell, lot);
slLine = StringFormat("SL $ Buy: %.2f | Sell: %.2f (%s)", mBuy, mSell, g_Ccy);
}
SetLabel(m_lblTpMoney, tpLine, C'0,220,120');
SetLabel(m_lblSlMoney, slLine, C'255,90,70');
}
else
{
string tpBuyStr = anyBuyTp ? StringFormat("%+.2f", buyTpMoney) : "-";
string tpSellStr = anySellTp ? StringFormat("%+.2f", sellTpMoney) : "-";
SetLabel(m_lblTpMoney,
StringFormat("TP $ Buy: %s | Sell: %s (%s)", tpBuyStr, tpSellStr, g_Ccy),
C'0,220,120');
string slBuyStr = "-";
string slSellStr = "-";
if(buyCount > 0)
{
if(buyBasket && !buyHasSlOnPos)
slBuyStr = "basket";
else if(anyBuySl)
slBuyStr = StringFormat("%.2f", buySlMoney);
}
if(sellCount > 0)
{
if(sellBasket && !sellHasSlOnPos)
slSellStr = "basket";
else if(anySellSl)
slSellStr = StringFormat("%.2f", sellSlMoney);
}
SetLabel(m_lblSlMoney,
StringFormat("SL $ Buy: %s | Sell: %s (%s)", slBuyStr, slSellStr, g_Ccy),
C'255,90,70');
}
if(m_hardRefresh > 0) m_hardRefresh--;
}
//+------------------------------------------------------------------+
//| Update Candle Timer |
//+------------------------------------------------------------------+
void CManualPanel::UpdateTimer()
{
// TimeTradeServer keeps ticking even when no new quotes arrive
datetime now = TimeTradeServer();
datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 0);
int periodSec = PeriodSeconds(PERIOD_CURRENT);
int remaining = (int)(barTime + periodSec - now);
if(remaining < 0) remaining = 0;
int mins = remaining / 60;
int secs = remaining % 60;
SetLabel(m_lblTimer,
StringFormat("Candle %s : %02d:%02d", m_tfName, mins, secs),
C'0,230,255');
}
//+------------------------------------------------------------------+
//| Global Panel |
//+------------------------------------------------------------------+
CManualPanel ExtPanel;
//+------------------------------------------------------------------+
//| Settings persistence across timeframe change |
//| Committed (Applied) settings are stored in terminal global |
//| variables on REASON_CHARTCHANGE and restored in OnInit. They are |
//| deleted when the EA is removed / chart closed. |
//+------------------------------------------------------------------+
string GvKey(const string n)
{
return "MTM_" + (string)ChartID() + "_" + n;
}
void GvPut(const string n, const double v)
{
GlobalVariableSet(GvKey(n), v);
}
void GvGetInt(const string n, int &v)
{
if(GlobalVariableCheck(GvKey(n))) v = (int)MathRound(GlobalVariableGet(GvKey(n)));
}
void GvGetBool(const string n, bool &v)
{
if(GlobalVariableCheck(GvKey(n))) v = (GlobalVariableGet(GvKey(n)) > 0.5);
}
void SaveSettings()
{
GvPut("lot", g_LotSize);
GvPut("mtn", g_Martin);
GvPut("stp", g_SingleTP);
GvPut("ssl", g_SingleSL);
GvPut("tst", g_TrailStart);
GvPut("tsp", g_TrailStep);
GvPut("btp", g_BasketTP);
GvPut("bts", g_BasketTrailStart);
GvPut("bsp", g_BasketTrailStep);
GvPut("esT", g_EnableSingleTrail ? 1.0 : 0.0);
GvPut("ebT", g_EnableBasketTrail ? 1.0 : 0.0);
GvPut("ebP", g_EnableBasketTP ? 1.0 : 0.0);
GvPut("mau", g_MartinAuto ? 1.0 : 0.0);
GvPut("adt", g_AutoDist);
GvPut("amx", g_AutoMax);
GvPut("flag", 1.0);
}
void ClearSettings()
{
string names[] = {"lot","mtn","stp","ssl","tst","tsp","btp","bts","bsp","esT","ebT","ebP","mau","adt","amx","flag"};
for(int i = 0; i < ArraySize(names); i++)
GlobalVariableDel(GvKey(names[i]));
}
// Returns true when settings saved by a timeframe change were restored
bool LoadSettings()
{
if(!GlobalVariableCheck(GvKey("flag")))
{
ClearSettings(); // drop stale leftovers
return false;
}
if(GlobalVariableCheck(GvKey("lot"))) g_LotSize = GlobalVariableGet(GvKey("lot"));
if(GlobalVariableCheck(GvKey("mtn"))) g_Martin = GlobalVariableGet(GvKey("mtn"));
GvGetInt("stp", g_SingleTP);
GvGetInt("ssl", g_SingleSL);
GvGetInt("tst", g_TrailStart);
GvGetInt("tsp", g_TrailStep);
GvGetInt("btp", g_BasketTP);
GvGetInt("bts", g_BasketTrailStart);
GvGetInt("bsp", g_BasketTrailStep);
GvGetBool("esT", g_EnableSingleTrail);
GvGetBool("ebT", g_EnableBasketTrail);
GvGetBool("ebP", g_EnableBasketTP);
GvGetBool("mau", g_MartinAuto);
GvGetInt("adt", g_AutoDist);
GvGetInt("amx", g_AutoMax);
ClearSettings(); // consumed
return true;
}
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
CacheSymbolData();
if(_Point <= 0.0)
{
Print("Symbol info error");
return INIT_FAILED;
}
// Fresh attach → defaults; after a timeframe change → restore the last Applied settings
if(LoadSettings())
{
g_LotSize = NormalizeLot(g_LotSize);
g_Martin = ClampMartin(g_Martin);
g_AutoDist = MathMax(1, g_AutoDist);
g_AutoMax = MathMin(100, MathMax(2, g_AutoMax));
Print("Settings restored after timeframe change");
}
else
{
g_LotSize = NormalizeLot(InpDefaultLot);
g_Martin = ClampMartin(InpDefaultMartin);
g_MartinAuto = (InpMartinMode == MARTIN_AUTO);
g_AutoDist = MathMax(1, InpAutoMartinDistance);
g_AutoMax = MathMin(100, MathMax(2, InpAutoMartinMaxTrades));
}
g_PendingMartinAuto = g_MartinAuto;
g_LastBarTime = iTime(_Symbol, _Period, 0); // no auto trade on the bar that is already running
g_PendingSingleTrail = g_EnableSingleTrail;
g_PendingBasketTrail = g_EnableBasketTrail;
g_PendingBasketTP = g_EnableBasketTP;
BuildOpenRequest();
int width = 430;
int height = 650 + LOT_SHIFT; // + Lot Capacity section
if(!ExtPanel.Create(0, "ManualTradeManager", 0, 15, 25, 15 + width, 25 + height))
{
Print("Panel create failed");
return INIT_FAILED;
}
ExtPanel.Run();
ExtPanel.ApplyDarkTheme();
ExtPanel.KillMinimize();
// Fill TP$/SL$ immediately with the restored (Applied) settings – no Apply click needed
ExtPanel.UpdateStatus(true);
// Light timer: candle countdown + status refresh (all updates are change-guarded)
EventSetMillisecondTimer(250);
ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
Print("Manual Trade Manager v1.62 | Direct-click Buy/Sell | ",
InpAsyncOrders ? "ASYNC" : "SYNC", " orders | filling=", EnumToString(g_Filling));
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
EventKillTimer();
if(reason == REASON_CHARTCHANGE)
SaveSettings(); // keep Applied settings for the re-init
else
ClearSettings(); // EA removed / chart closed → forget them
ExtPanel.Destroy(reason);
Comment("");
}
//+------------------------------------------------------------------+
//| AUTO MARTINGALE |
//| Runs once per NEW candle (= the previous candle just closed). |
//| For each side that ALREADY has a trade (the first one is yours): |
//| Buy : open next when lastOpen - Close[1] >= gap |
//| Sell: open next when Close[1] - lastOpen >= gap |
//| and the side has fewer than "Max Trades" positions. |
//| The order is a normal market order (price right after the close),|
//| lot = last lot x Martin multiplier (same rule as manual mode). |
//+------------------------------------------------------------------+
void AutoMartinOnNewBar()
{
const datetime bt = iTime(_Symbol, _Period, 0);
if(bt <= 0) return;
if(g_LastBarTime == 0) { g_LastBarTime = bt; return; }
if(bt == g_LastBarTime) return; // same candle – nothing to do
g_LastBarTime = bt; // a candle has just closed
if(!g_MartinAuto) return; // Manual mode: behaviour unchanged
const double c1 = iClose(_Symbol, _Period, 1); // close of the candle that just finished
if(c1 <= 0.0) return;
ScanPositions(false);
const double gap = (double)g_AutoDist * _Point;
const double eps = _Point * 0.001;
if(g_Buy.count > 0 && g_Buy.count < g_AutoMax && g_Buy.lastPrice > 0.0)
{
if((g_Buy.lastPrice - c1) >= gap - eps)
{
Print("AUTO MARTIN BUY #", g_Buy.count + 1, "/", g_AutoMax,
" | last=", DoubleToString(g_Buy.lastPrice, _Digits),
" close=", DoubleToString(c1, _Digits),
" gap=", DoubleToString((g_Buy.lastPrice - c1) / _Point, 0), " pts");
ExecuteMarket(ORDER_TYPE_BUY);
}
}
ScanPositions(false); // refresh (a Buy may just have been sent)
if(g_Sell.count > 0 && g_Sell.count < g_AutoMax && g_Sell.lastPrice > 0.0)
{
if((c1 - g_Sell.lastPrice) >= gap - eps)
{
Print("AUTO MARTIN SELL #", g_Sell.count + 1, "/", g_AutoMax,
" | last=", DoubleToString(g_Sell.lastPrice, _Digits),
" close=", DoubleToString(c1, _Digits),
" gap=", DoubleToString((c1 - g_Sell.lastPrice) / _Point, 0), " pts");
ExecuteMarket(ORDER_TYPE_SELL);
}
}
}
//+------------------------------------------------------------------+
//| Expert tick |
//+------------------------------------------------------------------+
void OnTick()
{
AutoMartinOnNewBar(); // Auto martingale: acts only at candle close (new bar)
ManagePositions(); // single position scan, non-blocking
ExtPanel.UpdateFloatingPL(false); // throttled + only when text changes
ExtPanel.UpdateLotInfo(false); // live max lot / next lot (throttled 100 ms)
}
//+------------------------------------------------------------------+
//| Timer |
//+------------------------------------------------------------------+
void OnTimer()
{
RefreshStopLevel();
ExtPanel.UpdateStatus(false);
ExtPanel.UpdateTimer();
}
//+------------------------------------------------------------------+
//| Trade transaction – reports async results, recovers rejections |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type != TRADE_TRANSACTION_REQUEST) return;
const uint rc = result.retcode;
if(rc == TRADE_RETCODE_DONE || rc == TRADE_RETCODE_DONE_PARTIAL ||
rc == TRADE_RETCODE_PLACED || rc == TRADE_RETCODE_NO_CHANGES)
return;
Print("Trade request failed: ret=", rc, " ", result.comment,
" | action=", EnumToString(request.action));
// Only our own market requests are retried
if(request.action == TRADE_ACTION_DEAL &&
request.symbol == _Symbol &&
request.magic == (ulong)InpMagicNumber &&
request.comment == InpTradeComment)
{
RecoverRequest(request, rc);
}
}
//+------------------------------------------------------------------+
//| Chart Event |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_CHART_CHANGE)
ExtPanel.RequestHardRefresh(4); // resize / property change → redraw TP$/SL$
if(id == CHARTEVENT_OBJECT_CLICK)
{
// ===== FAST PATH =====
// Execute Buy/Sell the instant the click arrives – no custom-event round trip.
if(sparam == g_ObjBuy)
{
ExecuteMarket(ORDER_TYPE_BUY);
g_BuyFastPending++;
}
else if(sparam == g_ObjSell)
{
ExecuteMarket(ORDER_TYPE_SELL);
g_SellFastPending++;
}
else if(StringFind(sparam, "Minimize") >= 0 || StringFind(sparam, "Maximize") >= 0)
{
ExtPanel.KillMinimize();
}
}
// MUST forward all events (click, key, endedit) to CAppDialog or CEdit won't work
ExtPanel.ChartEvent(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+
//| Push panel settings to ALL open managed positions |
//| Called only from Apply button – ignores soft tolerances |
//| Returns number of modify requests sent |
//+------------------------------------------------------------------+
int ForceApplySettingsToAll()
{
const double bid = g_Bid;
const double ask = g_Ask;
const double minDist = g_MinStopDist;
ScanPositions(false);
const int buyCount = g_Buy.count;
const int sellCount = g_Sell.count;
const double buyLots = g_Buy.lots;
const double sellLots = g_Sell.lots;
const double buySum = g_Buy.sum;
const double sellSum = g_Sell.sum;
int updated = 0;
// ----- BUY side -----
if(buyCount == 1)
{
if(posInfo.SelectByTicket(g_BuyTk[0]))
{
double open = posInfo.PriceOpen();
double newSL = 0.0;
double newTP = 0.0;
if(g_SingleSL > 0)
newSL = NormalizeDouble(open - g_SingleSL * _Point, _Digits);
if(g_SingleTP > 0)
newTP = NormalizeDouble(open + g_SingleTP * _Point, _Digits);
// If single trail is ON and already in profit → prefer trail SL
if(g_EnableSingleTrail && g_TrailStart > 0 && g_TrailStep > 0)
{
double profitPts = (bid - open) / _Point;
if(profitPts >= g_TrailStart)
{
double tsl = NormalizeDouble(bid - g_TrailStep * _Point, _Digits);
double maxSL = NormalizeDouble(bid - minDist, _Digits);
if(tsl > maxSL) tsl = maxSL;
if(tsl > open)
newSL = tsl;
}
}
if(SendSLTP(g_BuyTk[0], newSL, newTP))
{
updated++;
Print("Apply SINGLE BUY #", g_BuyTk[0], " SL=", newSL, " TP=", newTP);
}
}
}
else if(buyCount > 1)
{
double avg = buySum / buyLots;
double commonTP = 0.0;
if(g_EnableBasketTP && g_BasketTP > 0)
commonTP = NormalizeDouble(avg + g_BasketTP * _Point, _Digits);
g_StableBuyTP = commonTP;
g_LastBuyAvg = avg;
g_LastBuyCount = buyCount;
double commonSL = 0.0;
bool useTrail = false;
if(g_EnableBasketTrail && g_BasketTrailStart > 0 && g_BasketTrailStep > 0)
{
double profitPts = (bid - avg) / _Point;
if(profitPts >= g_BasketTrailStart)
{
commonSL = NormalizeDouble(bid - g_BasketTrailStep * _Point, _Digits);
double maxSL = NormalizeDouble(bid - minDist, _Digits);
if(commonSL > maxSL) commonSL = maxSL;
if(commonSL > avg)
useTrail = true;
}
}
for(int i = 0; i < buyCount; i++)
{
double newSL = useTrail ? commonSL : 0.0;
if(SendSLTP(g_BuyTk[i], newSL, commonTP))
updated++;
}
Print("Apply BASKET BUY n=", buyCount, " avg=", DoubleToString(avg, _Digits),
" SL=", commonSL, " TP=", commonTP);
}
// ----- SELL side -----
if(sellCount == 1)
{
if(posInfo.SelectByTicket(g_SellTk[0]))
{
double open = posInfo.PriceOpen();
double newSL = 0.0;
double newTP = 0.0;
if(g_SingleSL > 0)
newSL = NormalizeDouble(open + g_SingleSL * _Point, _Digits);
if(g_SingleTP > 0)
newTP = NormalizeDouble(open - g_SingleTP * _Point, _Digits);
if(g_EnableSingleTrail && g_TrailStart > 0 && g_TrailStep > 0)
{
double profitPts = (open - ask) / _Point;
if(profitPts >= g_TrailStart)
{
double tsl = NormalizeDouble(ask + g_TrailStep * _Point, _Digits);
double minSL = NormalizeDouble(ask + minDist, _Digits);
if(tsl < minSL) tsl = minSL;
if(tsl < open)
newSL = tsl;
}
}
if(SendSLTP(g_SellTk[0], newSL, newTP))
{
updated++;
Print("Apply SINGLE SELL #", g_SellTk[0], " SL=", newSL, " TP=", newTP);
}
}
}
else if(sellCount > 1)
{
double avg = sellSum / sellLots;
double commonTP = 0.0;
if(g_EnableBasketTP && g_BasketTP > 0)
commonTP = NormalizeDouble(avg - g_BasketTP * _Point, _Digits);
g_StableSellTP = commonTP;
g_LastSellAvg = avg;
g_LastSellCount = sellCount;
double commonSL = 0.0;
bool useTrail = false;
if(g_EnableBasketTrail && g_BasketTrailStart > 0 && g_BasketTrailStep > 0)
{
double profitPts = (avg - ask) / _Point;
if(profitPts >= g_BasketTrailStart)
{
commonSL = NormalizeDouble(ask + g_BasketTrailStep * _Point, _Digits);
double minSL = NormalizeDouble(ask + minDist, _Digits);
if(commonSL < minSL) commonSL = minSL;
if(commonSL < avg)
useTrail = true;
}
}
for(int i = 0; i < sellCount; i++)
{
double newSL = useTrail ? commonSL : 0.0;
if(SendSLTP(g_SellTk[i], newSL, commonTP))
updated++;
}
Print("Apply BASKET SELL n=", sellCount, " avg=", DoubleToString(avg, _Digits),
" SL=", commonSL, " TP=", commonTP);
}
// Give the broker time to confirm before ManagePositions looks again
if(buyCount > 0) StartCooldown(POSITION_TYPE_BUY);
if(sellCount > 0) StartCooldown(POSITION_TYPE_SELL);
return updated;
}
//+------------------------------------------------------------------+
//| Manage Positions (called every tick – non-blocking) |
//+------------------------------------------------------------------+
void ManagePositions()
{
MqlTick t;
if(!SymbolInfoTick(_Symbol, t)) return;
g_Bid = t.bid;
g_Ask = t.ask;
ScanPositions(false);
const ulong now = GetTickCount64();
if(g_Buy.count == 0)
{
g_StableBuyTP = 0.0; // ready for next basket
g_LastBuyCount = 0;
}
else if(now >= g_CoolUntil[0])
{
if(g_Buy.count == 1)
ManageSinglePosition(g_BuyTk[0], POSITION_TYPE_BUY);
else
ManageBasket(g_BuyTk, g_Buy.count, POSITION_TYPE_BUY, g_Buy.sum / g_Buy.lots);
}
if(g_Sell.count == 0)
{
g_StableSellTP = 0.0;
g_LastSellCount = 0;
}
else if(now >= g_CoolUntil[1])
{
if(g_Sell.count == 1)
ManageSinglePosition(g_SellTk[0], POSITION_TYPE_SELL);
else
ManageBasket(g_SellTk, g_Sell.count, POSITION_TYPE_SELL, g_Sell.sum / g_Sell.lots);
}
}
//+------------------------------------------------------------------+
//| Manage Single Position |
//+------------------------------------------------------------------+
void ManageSinglePosition(const ulong ticket, const ENUM_POSITION_TYPE type)
{
if(!posInfo.SelectByTicket(ticket)) return;
const double openPrice = posInfo.PriceOpen();
const double currentSL = posInfo.StopLoss();
const double currentTP = posInfo.TakeProfit();
const double tolerance = _Point * 0.51;
const double bid = g_Bid;
const double ask = g_Ask;
const double minDist = g_MinStopDist;
const bool isBuy = (type == POSITION_TYPE_BUY);
// ---- Desired fixed TP from open ----
double desiredTP = 0.0;
if(g_SingleTP > 0)
{
desiredTP = isBuy ? NormalizeDouble(openPrice + g_SingleTP * _Point, _Digits)
: NormalizeDouble(openPrice - g_SingleTP * _Point, _Digits);
}
// ---- Trailing SL ----
// TrailStart = profit (in points) needed to ACTIVATE trailing
// TrailStep = distance of SL behind current price once active
// Once active: SL only moves in favorable direction, never removed
bool trailActive = false;
double trailSL = 0.0;
if(g_EnableSingleTrail && g_TrailStart > 0 && g_TrailStep > 0)
{
if(isBuy)
{
double profitPts = (bid - openPrice) / _Point;
bool alreadyLocked = (currentSL != 0.0 && currentSL >= openPrice - tolerance);
if(profitPts >= (double)g_TrailStart || alreadyLocked)
{
trailSL = NormalizeDouble(bid - (double)g_TrailStep * _Point, _Digits);
double maxSL = NormalizeDouble(bid - minDist, _Digits);
if(trailSL > maxSL) trailSL = maxSL;
if(trailSL >= openPrice - tolerance || alreadyLocked)
trailActive = true;
}
}
else // SELL
{
double profitPts = (openPrice - ask) / _Point;
bool alreadyLocked = (currentSL != 0.0 && currentSL <= openPrice + tolerance);
if(profitPts >= (double)g_TrailStart || alreadyLocked)
{
trailSL = NormalizeDouble(ask + (double)g_TrailStep * _Point, _Digits);
double minSL = NormalizeDouble(ask + minDist, _Digits);
if(trailSL < minSL) trailSL = minSL;
if(trailSL <= openPrice + tolerance || alreadyLocked)
trailActive = true;
}
}
}
// ---- Decide SL to write ----
double writeSL = currentSL;
bool doModify = false;
if(trailActive)
{
// Move SL only in favorable direction
if(isBuy)
{
if(currentSL == 0.0 || trailSL > currentSL + tolerance)
{
writeSL = trailSL;
doModify = true;
}
}
else
{
if(currentSL == 0.0 || trailSL < currentSL - tolerance)
{
writeSL = trailSL;
doModify = true;
}
}
}
else if(g_SingleSL > 0)
{
// Fixed SL from panel (only if trail not active)
double desiredSL = isBuy
? NormalizeDouble(openPrice - g_SingleSL * _Point, _Digits)
: NormalizeDouble(openPrice + g_SingleSL * _Point, _Digits);
if(currentSL == 0.0 || MathAbs(currentSL - desiredSL) > tolerance)
{
writeSL = desiredSL;
doModify = true;
}
}
else
{
// No SL wanted and trail not active
if(currentSL != 0.0)
{
writeSL = 0.0;
doModify = true;
}
}
// ---- Decide TP ----
double writeTP = currentTP;
if(desiredTP > 0.0)
{
if(currentTP == 0.0 || MathAbs(currentTP - desiredTP) > tolerance)
{
writeTP = desiredTP;
doModify = true;
}
}
if(!doModify) return;
// Fire-and-forget; cooldown prevents re-sending before the broker confirms
SendSLTP(ticket, writeSL, writeTP);
StartCooldown(type);
if(trailActive)
{
double pp = (isBuy ? (bid - openPrice) : (openPrice - ask)) / _Point;
PrintThrottled("Single TRAIL #" + (string)ticket + " SL→" + DoubleToString(writeSL, _Digits) +
" profitPts=" + DoubleToString(pp, 1) +
" start=" + (string)g_TrailStart + " step=" + (string)g_TrailStep);
}
}
//+------------------------------------------------------------------+
//| Manage Basket (2+ positions) |
//| - Force ONE identical TP on ALL positions from weighted avg |
//| - Common trailing SL from average (never remove once locked) |
//+------------------------------------------------------------------+
void ManageBasket(ulong &tickets[], const int count, const ENUM_POSITION_TYPE type, const double avgPrice)
{
const double bid = g_Bid;
const double ask = g_Ask;
const double minDist = g_MinStopDist;
const double tolerance = _Point * 0.51;
const bool isBuy = (type == POSITION_TYPE_BUY);
// =====================================================================
// A) Common TP = average ± BasketTP points (all legs identical)
// =====================================================================
double commonTP = 0.0;
if(g_EnableBasketTP && g_BasketTP > 0)
{
commonTP = isBuy ? NormalizeDouble(avgPrice + (double)g_BasketTP * _Point, _Digits)
: NormalizeDouble(avgPrice - (double)g_BasketTP * _Point, _Digits);
if(isBuy) { g_StableBuyTP = commonTP; g_LastBuyAvg = avgPrice; g_LastBuyCount = count; }
else { g_StableSellTP = commonTP; g_LastSellAvg = avgPrice; g_LastSellCount = count; }
}
else
{
if(isBuy) { g_StableBuyTP = 0.0; g_LastBuyCount = count; }
else { g_StableSellTP = 0.0; g_LastSellCount = count; }
}
// =====================================================================
// B) Basket trailing decision (from average price)
// BasketTrailStart = profit pts from AVG to activate
// BasketTrailStep = SL distance behind bid/ask once active
// =====================================================================
bool trailOn = false;
double trailSL = 0.0;
if(g_EnableBasketTrail && g_BasketTrailStart > 0 && g_BasketTrailStep > 0)
{
// Already locking profit?
bool anyLocked = false;
for(int i = 0; i < count; i++)
{
if(!posInfo.SelectByTicket(tickets[i])) continue;
double s = posInfo.StopLoss();
if(isBuy && s != 0.0 && s >= avgPrice - tolerance) { anyLocked = true; break; }
if(!isBuy && s != 0.0 && s <= avgPrice + tolerance) { anyLocked = true; break; }
}
if(isBuy)
{
double profitPts = (bid - avgPrice) / _Point;
if(profitPts >= (double)g_BasketTrailStart || anyLocked)
{
trailSL = NormalizeDouble(bid - (double)g_BasketTrailStep * _Point, _Digits);
double maxSL = NormalizeDouble(bid - minDist, _Digits);
if(trailSL > maxSL) trailSL = maxSL;
if(trailSL >= avgPrice - tolerance || anyLocked)
trailOn = true;
}
}
else
{
double profitPts = (avgPrice - ask) / _Point;
if(profitPts >= (double)g_BasketTrailStart || anyLocked)
{
trailSL = NormalizeDouble(ask + (double)g_BasketTrailStep * _Point, _Digits);
double minSL = NormalizeDouble(ask + minDist, _Digits);
if(trailSL < minSL) trailSL = minSL;
if(trailSL <= avgPrice + tolerance || anyLocked)
trailOn = true;
}
}
}
// =====================================================================
// C) Apply to every leg: same TP + correct SL
// - Trail ON → SL = trailSL (only move favorably)
// - Trail OFF → SL = 0 (martingale: no individual SL)
// =====================================================================
const double wantTP = (g_EnableBasketTP && commonTP > 0.0) ? NormalizeDouble(commonTP, _Digits) : 0.0;
int sent = 0;
for(int i = 0; i < count; i++)
{
if(!posInfo.SelectByTicket(tickets[i])) continue;
const double curTP = NormalizeDouble(posInfo.TakeProfit(), _Digits);
const double curSL = posInfo.StopLoss();
// Target SL
double wantSL = 0.0;
if(trailOn)
{
wantSL = trailSL;
// Only improve, never worsen
if(isBuy)
{
if(curSL != 0.0 && curSL > wantSL + tolerance)
wantSL = curSL; // keep better existing SL
}
else
{
if(curSL != 0.0 && curSL < wantSL - tolerance && curSL > 0.0)
wantSL = curSL;
}
}
// Target TP – keep the existing TP whenever we have no TP target
// (previously a trailing SL update could wipe the TP when BasketTP = 0)
const double setTP = (g_EnableBasketTP && wantTP > 0.0) ? wantTP : curTP;
const bool tpNeed = (g_EnableBasketTP && wantTP > 0.0 && MathAbs(curTP - wantTP) >= tolerance);
bool slNeed = false;
if(trailOn)
{
if(isBuy)
slNeed = (curSL == 0.0 || curSL < wantSL - tolerance);
else
slNeed = (curSL == 0.0 || curSL > wantSL + tolerance);
}
else
{
// Martingale without trail → SL must be zero
slNeed = (curSL != 0.0);
wantSL = 0.0;
}
if(!tpNeed && !slNeed) continue;
if(SendSLTP(tickets[i], wantSL, setTP))
sent++;
}
if(sent > 0)
{
StartCooldown(type);
string msg = "BASKET-UNIFY " + (isBuy ? "BUY" : "SELL") + " n=" + (string)count +
" sent=" + (string)sent + " trail=" + (trailOn ? "ON" : "OFF") +
" SL=" + DoubleToString(trailOn ? trailSL : 0.0, _Digits) +
" TP=" + DoubleToString(wantTP, _Digits) +
" avg=" + DoubleToString(avgPrice, _Digits);
if(trailOn) PrintThrottled(msg);
else Print(msg);
}
}
//+------------------------------------------------------------------+