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