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