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