Copy_Trade_MT4_to_MT5/CopySlave_MT5.mq5
1197 lines
37 KiB
MQL5
meisamd
2026-10-27 07:36:57 +00:00
//+------------------------------------------------------------------+
//| CopySlave_MT5.mq5 |
//| Fast Local Copy Trade - SLAVE (MT4 -> MT5) v5.00 |
//| File based | No DLL | Hedging account required |
//+------------------------------------------------------------------+
#property copyright "Fast Copy Trade System"
#property version "5.00"
#property description "SLAVE EA - fast local copy from MT4 Master (file, no DLL)"
#property description "Hedging account + AutoTrading required. Attach to ONE chart."
input group "============== FILE & CONNECTION =============="
input string InpFileName = "CopyTrade_MT4_to_MT5.csv"; // Shared file name (same as Master)
input long InpMasterAccount = 0; // Master account (0 = any) - SET on real
input int InpStaleMs = 3000; // Master lost if no heartbeat for (ms)
input group "============== SYMBOL MAPPING =============="
input string InpReplaceFrom = ""; // Replace this in symbol
input string InpReplaceTo = ""; // With this
input string InpSymbolPrefix = ""; // Then PREFIX
input string InpSymbolSuffix = "+"; // Then SUFFIX (your broker: +)
input string InpPreloadSymbols = ""; // Preload (master names, comma sep) e.g. EURUSD,XAUUSD
input group "============== LOT SETTINGS =============="
input double InpLotMultiplier = 1.0; // Lot multiplier
input double InpFixedLot = 0.0; // Fixed lot (0 = use multiplier)
input double InpMaxLot = 50.0; // Max lot (0 = off)
input double InpMinLot = 0.01; // Min lot (0 = off)
input group "============== TRADE SETTINGS =============="
input int InpSlippage = 50; // Slippage (points)
input int InpMagicNumber = 20241014; // Magic for copied trades
input bool InpReverseTrade = false; // Reverse Buy/Sell (SL<->TP swapped)
input bool InpCopySL = true; // Copy SL (also follows trailing)
input bool InpCopyTP = true; // Copy TP
input bool InpOnlyNewTrades = false; // Ignore trades already open at EA start
input int InpMaxOpenAgeSec = 0; // Skip trades older than N sec (0 = off)
input bool InpAsyncMode = true; // Async orders (fastest)
input int InpPendingMs = 3000; // Async: wait before re-sending (ms)
input group "============== ACCOUNT PROTECTION =============="
input int InpMaxPositions = 0; // Max copied positions (0 = off)
input double InpMinMarginLevel = 0.0; // Min margin level % after open (0 = off)
input int InpMaxRetries = 6; // Max open retries per trade
input int InpHistoryDays = 14; // History scan to avoid re-opening (days)
input group "============== SPEED =============="
input int InpTimerMs = 1; // Timer ms (1 = fastest)
input int InpFullSyncMs = 250; // Periodic full re-check (ms)
input group "============== PANEL / LOG =============="
input bool InpShowPanel = true; // Show panel
input int InpPanelMs = 500; // Panel refresh ms
input bool InpShowLogs = true; // Log copies
#define P "CTS5_"
#define CMT "CT#"
#define K_OPEN 1
#define K_MOD 2
#define K_CLOSE 3
#define K_WARN 9
struct MTrade
{
ulong ticket;
ulong parent;
string symbol;
int type;
double lots;
double sl;
double tp;
datetime openTime;
};
struct SPos
{
ulong ticket;
ulong ref;
double vol;
double sl;
double tp;
string sym;
int ptype;
};
struct RState
{
int kind;
ulong key;
uint nextMs;
int fails;
bool dead;
};
struct SymC
{
string name;
int digits;
double point;
double tick;
double vmin;
double vmax;
double vstep;
int vdig;
int fillOpen;
int fillClose;
};
struct AliasT
{
ulong o;
ulong n;
};
MTrade g_list[];
int g_mCount = 0;
SPos g_sp[];
int g_spN = 0;
RState g_rs[];
SymC g_sc[];
AliasT g_al[];
ulong g_done[];
ulong g_lastVer = 0;
bool g_haveList = false;
bool g_verChanged = false;
bool g_baselineDone = false;
bool g_mReady = false;
long g_masterAcc = 0;
datetime g_mst = 0;
uint g_changeTick = 0;
uint g_lastGoodMs = 0;
bool g_everGood = false;
bool g_connected = false;
int g_lat = -1;
double g_latSum = 0;
int g_latN = 0;
int g_latMax = 0;
ulong g_copied = 0;
string g_lastError = "";
bool g_tradeBlocked = false;
uint g_lastFullMs = 0;
uint g_lastSyncMs = 0;
bool g_needSync = false;
uint g_lastPanel = 0;
//+------------------------------------------------------------------+
int OnInit()
{
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE) != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
{
Print("FATAL: this EA needs a HEDGING account (netting would merge copied trades).");
return(INIT_FAILED);
}
if(InpLotMultiplier <= 0 && InpFixedLot <= 0)
{
Print("FATAL: lot multiplier / fixed lot invalid");
return(INIT_PARAMETERS_INCORRECT);
}
if(!EventSetMillisecondTimer(MathMax(1, InpTimerMs)))
{
Print("FATAL timer: ", GetLastError());
return(INIT_FAILED);
}
LoadHistoryDone();
PreloadSymbols();
if(InpShowPanel) CreatePanel();
Print("======================================================");
Print("CopySlave_MT5 v5.00 (File, comment-mapped, ", (InpAsyncMode ? "ASYNC" : "SYNC"), ")");
Print("File : Common\\Files\\", InpFileName);
Print("Suffix : [", InpSymbolSuffix, "] Magic: ", InpMagicNumber);
Print("Timer : ", MathMax(1, InpTimerMs), " ms done-set from history: ", ArraySize(g_done));
Print("======================================================");
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
EventKillTimer();
ObjectsDeleteAll(0, P);
Print("Slave stopped | Copied: ", g_copied);
}
void OnTimer()
{
Poll();
if(InpShowPanel) UpdatePanel();
}
//+------------------------------------------------------------------+
//| Failures of async requests + detection of executed entries |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
{
if(!HistoryDealSelect(trans.deal)) return;
if(HistoryDealGetInteger(trans.deal, DEAL_MAGIC) != InpMagicNumber) return;
if(HistoryDealGetInteger(trans.deal, DEAL_ENTRY) != DEAL_ENTRY_IN) return;
ulong ref = ParseRef(HistoryDealGetString(trans.deal, DEAL_COMMENT));
if(ref > 0)
{
AddDone(ref);
RClear(K_OPEN, ref);
g_copied++;
g_needSync = true;
}
return;
}
if(!InpAsyncMode) return;
if(trans.type != TRADE_TRANSACTION_REQUEST) return;
if(request.magic != (ulong)InpMagicNumber) return;
uint rc = result.retcode;
if(RcOK(rc)) return;
uint now = GetTickCount();
if(request.action == TRADE_ACTION_DEAL && request.position == 0)
{
ulong ref = ParseRef(request.comment);
if(ref > 0)
{
if(rc == TRADE_RETCODE_INVALID_FILL) RotateFill(request.symbol);
RFail(K_OPEN, ref, rc, now);
Print("ASYNC OPEN FAIL [", request.symbol, "] #", ref, " rc=", rc, " ", RcText(rc));
g_lastError = RcText(rc);
g_needSync = true;
}
}
else if(request.action == TRADE_ACTION_DEAL)
{
if(rc == TRADE_RETCODE_POSITION_CLOSED) return;
if(rc == TRADE_RETCODE_INVALID_FILL) RotateFill(request.symbol);
RFail(K_CLOSE, request.position, rc, now);
Print("ASYNC CLOSE FAIL #", request.position, " rc=", rc, " ", RcText(rc));
g_lastError = RcText(rc);
}
else if(request.action == TRADE_ACTION_SLTP)
{
if(rc == TRADE_RETCODE_NO_CHANGES) return;
RFail(K_MOD, request.position, rc, now);
}
}
//+------------------------------------------------------------------+
//| Helpers |
//+------------------------------------------------------------------+
void Lg(string s) { if(InpShowLogs) Print(s); }
bool RcOK(uint rc)
{
return (rc == TRADE_RETCODE_DONE || rc == TRADE_RETCODE_DONE_PARTIAL || rc == TRADE_RETCODE_PLACED);
}
string RcText(uint rc)
{
switch(rc)
{
case TRADE_RETCODE_REQUOTE: return "Requote";
case TRADE_RETCODE_REJECT: return "Rejected";
case TRADE_RETCODE_TIMEOUT: return "Timeout";
case TRADE_RETCODE_INVALID_VOLUME: return "Invalid volume";
case TRADE_RETCODE_INVALID_PRICE: return "Invalid price";
case TRADE_RETCODE_INVALID_STOPS: return "Invalid stops";
case TRADE_RETCODE_TRADE_DISABLED: return "Trade disabled";
case TRADE_RETCODE_MARKET_CLOSED: return "Market closed";
case TRADE_RETCODE_NO_MONEY: return "No money";
case TRADE_RETCODE_PRICE_CHANGED: return "Price changed";
case TRADE_RETCODE_PRICE_OFF: return "No quotes";
case TRADE_RETCODE_TOO_MANY_REQUESTS: return "Too many requests";
case TRADE_RETCODE_CONNECTION: return "No connection";
case TRADE_RETCODE_INVALID_FILL: return "Invalid fill mode";
case TRADE_RETCODE_LIMIT_VOLUME: return "Volume limit";
case TRADE_RETCODE_LIMIT_POSITIONS: return "Positions limit";
case TRADE_RETCODE_HEDGE_PROHIBITED: return "Hedge prohibited";
case TRADE_RETCODE_FIFO_CLOSE: return "FIFO rule";
case TRADE_RETCODE_CLIENT_DISABLES_AT: return "AutoTrading off (client)";
case TRADE_RETCODE_SERVER_DISABLES_AT: return "AutoTrading off (server)";
}
return "retcode " + IntegerToString((long)rc);
}
string MapSymbol(string src)
{
string s = src;
if(StringLen(InpReplaceFrom) > 0)
StringReplace(s, InpReplaceFrom, InpReplaceTo);
return InpSymbolPrefix + s + InpSymbolSuffix;
}
ulong ParseRef(const string c)
{
if(StringFind(c, CMT) != 0) return 0;
return (ulong)StringToInteger(StringSubstr(c, StringLen(CMT)));
}
bool TradingAllowed()
{
if(!TerminalInfoInteger(TERMINAL_CONNECTED)) return false;
if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return false;
if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) return false;
if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return false;
if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) return false;
return true;
}
bool SendReq(MqlTradeRequest &rq, MqlTradeResult &rs)
{
if(InpAsyncMode) return OrderSendAsync(rq, rs);
return OrderSend(rq, rs);
}
//--- alias table (master partial-close chain: old ticket -> new ticket)
ulong AliasGet(ulong o)
{
for(int i = ArraySize(g_al) - 1; i >= 0; i--)
if(g_al[i].o == o) return g_al[i].n;
return 0;
}
void AliasSet(ulong o, ulong n)
{
int k = ArraySize(g_al);
for(int i = 0; i < k; i++)
if(g_al[i].o == o) { g_al[i].n = n; return; }
if(k >= 256)
{
for(int j = 1; j < k; j++) g_al[j-1] = g_al[j];
k--;
}
else
ArrayResize(g_al, k + 1);
g_al[k].o = o;
g_al[k].n = n;
}
//--- "done" set: master tickets already copied / deliberately skipped
bool DoneHas(ulong t)
{
for(int i = ArraySize(g_done) - 1; i >= 0; i--)
if(g_done[i] == t) return true;
return false;
}
void AddDone(ulong t)
{
if(t == 0 || DoneHas(t)) return;
int n = ArraySize(g_done);
ArrayResize(g_done, n + 1);
g_done[n] = t;
}
bool IsDone(const MTrade &t)
{
if(DoneHas(t.ticket)) return true;
if(t.parent > 0 && DoneHas(t.parent)) { AddDone(t.ticket); return true; }
return false;
}
void PruneDone()
{
int n = ArraySize(g_done), k = 0;
for(int i = 0; i < n; i++)
if(FindMasterIdx(g_done[i]) >= 0) g_done[k++] = g_done[i];
ArrayResize(g_done, k);
}
//--- retry / pending state
int RFind(int kind, ulong key)
{
for(int i = ArraySize(g_rs) - 1; i >= 0; i--)
if(g_rs[i].kind == kind && g_rs[i].key == key) return i;
return -1;
}
int RGet(int kind, ulong key, uint now)
{
int i = RFind(kind, key);
if(i >= 0) return i;
i = ArraySize(g_rs);
ArrayResize(g_rs, i + 1);
g_rs[i].kind = kind;
g_rs[i].key = key;
g_rs[i].nextMs = now;
g_rs[i].fails = 0;
g_rs[i].dead = false;
return i;
}
bool RReady(int kind, ulong key, uint now)
{
int i = RFind(kind, key);
if(i < 0) return true;
if(g_rs[i].dead) return false;
return ((int)(now - g_rs[i].nextMs) >= 0);
}
void RHold(int kind, ulong key, uint now, uint ms)
{
int i = RGet(kind, key, now);
g_rs[i].nextMs = now + ms;
}
void RSuccess(int kind, ulong key, uint now, uint holdMs)
{
int i = RGet(kind, key, now);
g_rs[i].fails = 0;
g_rs[i].dead = false;
g_rs[i].nextMs = now + holdMs;
}
void RClear(int kind, ulong key)
{
int i = RFind(kind, key);
if(i < 0) return;
int n = ArraySize(g_rs);
g_rs[i] = g_rs[n - 1];
ArrayResize(g_rs, n - 1);
}
uint RetryDelay(uint rc, int fails, int kind)
{
uint d;
switch(rc)
{
case TRADE_RETCODE_REQUOTE:
case TRADE_RETCODE_PRICE_CHANGED:
case TRADE_RETCODE_PRICE_OFF:
case TRADE_RETCODE_REJECT:
case TRADE_RETCODE_INVALID_FILL:
d = (uint)MathMin(20 * fails, 300);
break;
case TRADE_RETCODE_TIMEOUT:
case TRADE_RETCODE_CONNECTION:
d = 1500;
break;
case TRADE_RETCODE_TOO_MANY_REQUESTS:
d = 1000;
break;
default:
d = (uint)MathMin(1000 * fails, 10000);
break;
}
if(kind != K_OPEN && d > 2000) d = 2000;
return d;
}
void RFail(int kind, ulong key, uint rc, uint now)
{
int i = RGet(kind, key, now);
g_rs[i].fails++;
g_rs[i].nextMs = now + RetryDelay(rc, g_rs[i].fails, kind);
if(kind == K_OPEN && g_rs[i].fails >= InpMaxRetries)
{
g_rs[i].dead = true;
Print("GIVE UP opening master #", key, " after ", g_rs[i].fails, " tries: ", RcText(rc));
}
}
bool SlaveHas(ulong tk)
{
for(int i = 0; i < g_spN; i++)
if(g_sp[i].ticket == tk) return true;
return false;
}
void PruneRS(uint now)
{
for(int i = ArraySize(g_rs) - 1; i >= 0; i--)
{
bool rm = false;
int kd = g_rs[i].kind;
ulong key = g_rs[i].key;
bool expired = ((int)(now - g_rs[i].nextMs) >= 0);
if(kd == K_OPEN)
{
if(FindMasterIdx(key) < 0) rm = true;
else if(g_rs[i].fails == 0 && !g_rs[i].dead && expired) rm = true;
}
else if(kd == K_WARN)
{
if(!SlaveHas(key)) rm = true;
}
else
{
if(!SlaveHas(key)) rm = true;
else if(g_rs[i].fails == 0 && expired) rm = true;
}
if(rm)
{
int n = ArraySize(g_rs);
g_rs[i] = g_rs[n - 1];
ArrayResize(g_rs, n - 1);
}
}
}
bool HasActiveRS()
{
for(int i = ArraySize(g_rs) - 1; i >= 0; i--)
if(!g_rs[i].dead) return true;
return false;
}
//--- symbol cache
int VolDigits(double step)
{
int d = 0;
double s = step;
while(d < 8 && MathAbs(s - MathRound(s)) > 1e-9) { s *= 10.0; d++; }
return d;
}
void DetectFills(int i)
{
long fm = SymbolInfoInteger(g_sc[i].name, SYMBOL_FILLING_MODE);
if((fm & SYMBOL_FILLING_FOK) != 0) g_sc[i].fillOpen = ORDER_FILLING_FOK;
else if((fm & SYMBOL_FILLING_IOC) != 0) g_sc[i].fillOpen = ORDER_FILLING_IOC;
else g_sc[i].fillOpen = ORDER_FILLING_RETURN;
if((fm & SYMBOL_FILLING_IOC) != 0) g_sc[i].fillClose = ORDER_FILLING_IOC;
else if((fm & SYMBOL_FILLING_FOK) != 0) g_sc[i].fillClose = ORDER_FILLING_FOK;
else g_sc[i].fillClose = ORDER_FILLING_RETURN;
}
int NextFill(int f)
{
if(f == ORDER_FILLING_FOK) return ORDER_FILLING_IOC;
if(f == ORDER_FILLING_IOC) return ORDER_FILLING_RETURN;
return ORDER_FILLING_FOK;
}
void RotateFill(string sym)
{
for(int i = 0; i < ArraySize(g_sc); i++)
if(g_sc[i].name == sym)
{
g_sc[i].fillOpen = NextFill(g_sc[i].fillOpen);
g_sc[i].fillClose = NextFill(g_sc[i].fillClose);
return;
}
}
int SymIdx(const string name)
{
for(int i = ArraySize(g_sc) - 1; i >= 0; i--)
if(g_sc[i].name == name) return i;
if(!SymbolSelect(name, true)) return -1;
// first use: give the terminal a moment to receive the first quote
for(int w = 0; w < 60; w++)
{
MqlTick x;
if(SymbolInfoTick(name, x) && x.bid > 0) break;
Sleep(5);
}
double pt = SymbolInfoDouble(name, SYMBOL_POINT);
if(pt <= 0) return -1;
int n = ArraySize(g_sc);
ArrayResize(g_sc, n + 1);
g_sc[n].name = name;
g_sc[n].digits = (int)SymbolInfoInteger(name, SYMBOL_DIGITS);
g_sc[n].point = pt;
double ts = SymbolInfoDouble(name, SYMBOL_TRADE_TICK_SIZE);
g_sc[n].tick = (ts > 0) ? ts : pt;
g_sc[n].vmin = SymbolInfoDouble(name, SYMBOL_VOLUME_MIN);
g_sc[n].vmax = SymbolInfoDouble(name, SYMBOL_VOLUME_MAX);
g_sc[n].vstep = SymbolInfoDouble(name, SYMBOL_VOLUME_STEP);
if(g_sc[n].vstep <= 0) g_sc[n].vstep = 0.01;
if(g_sc[n].vmin <= 0) g_sc[n].vmin = g_sc[n].vstep;
if(g_sc[n].vmax <= 0) g_sc[n].vmax = 1000.0;
g_sc[n].vdig = VolDigits(g_sc[n].vstep);
DetectFills(n);
return n;
}
void PreloadSymbols()
{
if(StringLen(InpPreloadSymbols) == 0) return;
string arr[];
int c = StringSplit(InpPreloadSymbols, ',', arr);
for(int i = 0; i < c; i++)
{
string s = arr[i];
StringTrimLeft(s);
StringTrimRight(s);
if(StringLen(s) == 0) continue;
string m = MapSymbol(s);
if(SymIdx(m) < 0) Print("Preload: symbol not found [", m, "]");
}
}
double Snap(double p, int si)
{
if(p <= 0) return 0;
double ts = g_sc[si].tick;
if(ts > 0) p = MathRound(p / ts) * ts;
return NormalizeDouble(p, g_sc[si].digits);
}
double CalcLots(double mLots, int si)
{
double lots = (InpFixedLot > 0) ? InpFixedLot : mLots * InpLotMultiplier;
if(InpMinLot > 0 && lots < InpMinLot) lots = InpMinLot;
if(InpMaxLot > 0 && lots > InpMaxLot) lots = InpMaxLot;
double step = g_sc[si].vstep;
lots = MathFloor(lots / step + 1e-9) * step;
if(lots < g_sc[si].vmin) lots = g_sc[si].vmin;
if(lots > g_sc[si].vmax) lots = g_sc[si].vmax;
return NormalizeDouble(lots, g_sc[si].vdig);
}
void LoadHistoryDone()
{
if(InpHistoryDays <= 0) return;
datetime to = TimeCurrent() + 86400;
datetime from = to - (datetime)(InpHistoryDays * 86400);
if(!HistorySelect(from, to)) return;
int n = HistoryDealsTotal();
for(int i = 0; i < n; i++)
{
ulong d = HistoryDealGetTicket(i);
if(d == 0) continue;
if(HistoryDealGetInteger(d, DEAL_MAGIC) != InpMagicNumber) continue;
if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_IN) continue;
ulong ref = ParseRef(HistoryDealGetString(d, DEAL_COMMENT));
if(ref > 0) AddDone(ref);
}
}
//+------------------------------------------------------------------+
//| Snapshot reading (validated: header + count + end marker) |
//+------------------------------------------------------------------+
int FindMasterIdx(ulong tk)
{
for(int i = 0; i < g_mCount; i++)
if(g_list[i].ticket == tk) return i;
return -1;
}
int FindChildIdx(ulong parent)
{
for(int i = 0; i < g_mCount; i++)
if(g_list[i].parent == parent) return i;
return -1;
}
int ResolveIdx(ulong ref)
{
ulong cur = ref;
for(int hop = 0; hop < 12; hop++)
{
int idx = FindMasterIdx(cur);
if(idx >= 0)
{
if(cur != ref) AliasSet(ref, cur);
return idx;
}
ulong a = AliasGet(cur);
if(a > 0 && a != cur) { cur = a; continue; }
int c = FindChildIdx(cur);
if(c >= 0)
{
AliasSet(cur, g_list[c].ticket);
cur = g_list[c].ticket;
continue;
}
return -1;
}
return -1;
}
bool ReadSnapshot(uint now)
{
g_verChanged = false;
int h = FileOpen(InpFileName, FILE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE);
if(h == INVALID_HANDLE) return false;
string hp[];
int hn = StringSplit(FileReadString(h), ',', hp);
if(hn < 9 || hp[0] != "H" || hp[1] != "5") { FileClose(h); return false; }
long acc = StringToInteger(hp[2]);
if(InpMasterAccount != 0 && acc != InpMasterAccount)
{
FileClose(h);
g_lastError = "Master account mismatch";
return false;
}
ulong seq = (ulong)StringToInteger(hp[3]);
ulong ver = (ulong)StringToInteger(hp[4]);
uint mTick = (uint)StringToInteger(hp[5]);
int hcnt = (int)StringToInteger(hp[6]);
bool mrdy = (hp[7] == "1");
datetime mst = (datetime)StringToInteger(hp[8]);
uint d = now - mTick;
int lat = (d <= 0x7FFFFFFF) ? (int)d : 0;
g_lat = lat;
if(lat > InpStaleMs) { FileClose(h); return false; } // stale file = master gone
if(hcnt < 0 || hcnt > 5000) { FileClose(h); return false; }
g_masterAcc = acc;
g_mReady = mrdy;
g_mst = mst;
g_lastGoodMs = now;
g_everGood = true;
if(g_haveList && ver == g_lastVer) { FileClose(h); return true; } // nothing changed: cheap exit
MTrade tmp[];
ArrayResize(tmp, hcnt);
int n = 0;
bool tailOK = false;
while(!FileIsEnding(h))
{
string line = FileReadString(h);
if(StringLen(line) < 3) continue;
string p[];
int c = StringSplit(line, ',', p);
if(c < 1) continue;
if(p[0] == "T")
{
if(c < 11 || n >= hcnt) { n = -1; break; }
tmp[n].ticket = (ulong)StringToInteger(p[1]);
tmp[n].symbol = MapSymbol(p[2]);
tmp[n].type = (int)StringToInteger(p[3]);
tmp[n].lots = StringToDouble(p[4]);
tmp[n].sl = StringToDouble(p[6]);
tmp[n].tp = StringToDouble(p[7]);
tmp[n].openTime = (datetime)StringToInteger(p[9]);
tmp[n].parent = (ulong)StringToInteger(p[10]);
n++;
}
else if(p[0] == "E")
{
tailOK = (c >= 3 && StringToInteger(p[1]) == (long)seq && StringToInteger(p[2]) == hcnt);
break;
}
}
FileClose(h);
if(n != hcnt || !tailOK) return false; // torn / partial read -> ignore
ArrayResize(g_list, n);
for(int i = 0; i < n; i++) g_list[i] = tmp[i];
g_mCount = n;
g_lastVer = ver;
g_haveList = true;
g_verChanged = true;
g_changeTick = mTick;
if(g_baselineDone)
{
g_latSum += lat;
g_latN++;
if(lat > g_latMax) g_latMax = lat;
}
return true;
}
//+------------------------------------------------------------------+
void Poll()
{
uint now = GetTickCount();
bool ok = ReadSnapshot(now);
g_connected = g_everGood && ((int)(now - g_lastGoodMs) < InpStaleMs);
if(!ok) return;
if(!g_mReady) return;
bool due = g_verChanged
|| (g_needSync && (int)(now - g_lastSyncMs) >= 5)
|| ((int)(now - g_lastFullMs) >= MathMax(50, InpFullSyncMs));
if(!due) return;
Sync(now);
g_lastFullMs = now;
}
void ScanSlavePositions()
{
int pt = PositionsTotal();
ArrayResize(g_sp, pt);
g_spN = 0;
for(int i = 0; i < pt; i++)
{
ulong tk = PositionGetTicket(i);
if(tk == 0) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
int k = g_spN++;
g_sp[k].ticket = tk;
g_sp[k].ref = ParseRef(PositionGetString(POSITION_COMMENT));
g_sp[k].vol = PositionGetDouble(POSITION_VOLUME);
g_sp[k].sl = PositionGetDouble(POSITION_SL);
g_sp[k].tp = PositionGetDouble(POSITION_TP);
g_sp[k].sym = PositionGetString(POSITION_SYMBOL);
g_sp[k].ptype = (int)PositionGetInteger(POSITION_TYPE);
}
}
//+------------------------------------------------------------------+
void Sync(uint now)
{
g_lastSyncMs = now;
if(!TradingAllowed()) { g_tradeBlocked = true; return; }
g_tradeBlocked = false;
if(!g_baselineDone)
{
if(InpOnlyNewTrades)
for(int i = 0; i < g_mCount; i++) AddDone(g_list[i].ticket);
g_baselineDone = true;
}
ScanSlavePositions();
bool covered[];
ArrayResize(covered, g_mCount);
ArrayInitialize(covered, false);
//--- 1) existing slave positions: close / partial / stops
for(int k = 0; k < g_spN; k++)
{
if(g_sp[k].ref == 0)
{
if(RFind(K_WARN, g_sp[k].ticket) < 0)
{
int w = RGet(K_WARN, g_sp[k].ticket, now);
g_rs[w].dead = true;
Print("WARNING: position #", g_sp[k].ticket, " has our magic but no CT# comment -> ignored");
}
continue;
}
AddDone(g_sp[k].ref);
int mi = ResolveIdx(g_sp[k].ref);
if(mi < 0)
{
if(RReady(K_CLOSE, g_sp[k].ticket, now))
CloseVol(g_sp[k].ticket, g_sp[k].sym, g_sp[k].ptype, g_sp[k].vol, now, false);
continue;
}
covered[mi] = true;
AddDone(g_list[mi].ticket);
if(g_list[mi].ticket != g_sp[k].ref) PartialSync(k, mi, now);
SyncStops(k, mi, now);
}
//--- 2) new master trades
int openCnt = g_spN;
for(int i = 0; i < g_mCount; i++)
{
if(covered[i]) continue;
if(IsDone(g_list[i])) continue;
if(!RReady(K_OPEN, g_list[i].ticket, now)) continue;
if(InpMaxOpenAgeSec > 0 && g_mst > 0 && g_list[i].openTime > 0 &&
(long)(g_mst - g_list[i].openTime) > InpMaxOpenAgeSec)
{
AddDone(g_list[i].ticket);
Lg("SKIP old master trade #" + IntegerToString((long)g_list[i].ticket));
continue;
}
if(InpMaxPositions > 0 && openCnt >= InpMaxPositions) continue;
if(OpenTrade(g_list[i], now)) openCnt++;
}
//--- 3) housekeeping
if(g_verChanged) PruneDone();
PruneRS(now);
g_needSync = HasActiveRS();
}
//+------------------------------------------------------------------+
void SyncStops(int k, int mi, uint now)
{
if(!InpCopySL && !InpCopyTP) return;
if(!RReady(K_MOD, g_sp[k].ticket, now)) return;
string sym = g_sp[k].sym;
int si = SymIdx(sym);
if(si < 0) return;
double wsl = InpReverseTrade ? g_list[mi].tp : g_list[mi].sl;
double wtp = InpReverseTrade ? g_list[mi].sl : g_list[mi].tp;
double curSL = g_sp[k].sl, curTP = g_sp[k].tp;
double nsl = InpCopySL ? Snap(wsl, si) : curSL;
double ntp = InpCopyTP ? Snap(wtp, si) : curTP;
double half = g_sc[si].tick * 0.5;
bool dSL = (MathAbs(nsl - curSL) > half);
bool dTP = (MathAbs(ntp - curTP) > half);
if(!dSL && !dTP) return;
MqlTick tk;
if(!SymbolInfoTick(sym, tk)) return;
double lvl = (double)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL) * g_sc[si].point;
bool buy = (g_sp[k].ptype == POSITION_TYPE_BUY);
if(dSL && nsl > 0)
{
bool okS = buy ? (nsl < tk.bid - lvl) : (nsl > tk.ask + lvl);
if(!okS) { nsl = curSL; dSL = false; }
}
if(dTP && ntp > 0)
{
bool okT = buy ? (ntp > tk.bid + lvl) : (ntp < tk.ask - lvl);
if(!okT) { ntp = curTP; dTP = false; }
}
if(!dSL && !dTP) return; // will be re-checked on next full sync
MqlTradeRequest rq;
MqlTradeResult rs;
ZeroMemory(rq);
ZeroMemory(rs);
rq.action = TRADE_ACTION_SLTP;
rq.position = g_sp[k].ticket;
rq.symbol = sym;
rq.sl = nsl;
rq.tp = ntp;
rq.magic = InpMagicNumber;
bool sent = SendReq(rq, rs);
if(sent && (RcOK(rs.retcode) || rs.retcode == TRADE_RETCODE_NO_CHANGES))
RSuccess(K_MOD, g_sp[k].ticket, now, InpAsyncMode ? 120 : 30);
else
{
uint rc = (rs.retcode == 0) ? TRADE_RETCODE_REJECT : rs.retcode;
RFail(K_MOD, g_sp[k].ticket, rc, now);
}
}
void PartialSync(int k, int mi, uint now)
{
if(InpFixedLot > 0) return;
if(!RReady(K_CLOSE, g_sp[k].ticket, now)) return;
int si = SymIdx(g_sp[k].sym);
if(si < 0) return;
double target = CalcLots(g_list[mi].lots, si);
double step = g_sc[si].vstep;
double diff = g_sp[k].vol - target;
if(diff < step * 0.999) return;
diff = MathFloor(diff / step + 1e-9) * step;
if(diff < g_sc[si].vmin) return;
if(g_sp[k].vol - diff < g_sc[si].vmin - 1e-9) return;
CloseVol(g_sp[k].ticket, g_sp[k].sym, g_sp[k].ptype, NormalizeDouble(diff, g_sc[si].vdig), now, true);
}
//+------------------------------------------------------------------+
void CloseVol(ulong ticket, string sym, int ptype, double vol, uint now, bool partial)
{
int si = SymIdx(sym);
if(si < 0) return;
MqlTick tk;
if(!SymbolInfoTick(sym, tk) || tk.bid <= 0 || tk.ask <= 0)
{
RHold(K_CLOSE, ticket, now, 20);
return;
}
MqlTradeRequest rq;
MqlTradeResult rs;
ZeroMemory(rq);
ZeroMemory(rs);
bool buy = (ptype == POSITION_TYPE_BUY);
rq.action = TRADE_ACTION_DEAL;
rq.position = ticket;
rq.symbol = sym;
rq.volume = vol;
rq.type = buy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
rq.price = buy ? tk.bid : tk.ask;
rq.deviation = InpSlippage;
rq.magic = InpMagicNumber;
rq.type_filling = (ENUM_ORDER_TYPE_FILLING)g_sc[si].fillClose;
rq.comment = partial ? "CT-part" : "CT-close";
uint t0 = GetTickCount();
int detect = (int)(t0 - g_changeTick);
bool sent = SendReq(rq, rs);
uint t1 = GetTickCount();
if(sent && (RcOK(rs.retcode) || rs.retcode == TRADE_RETCODE_POSITION_CLOSED))
{
RSuccess(K_CLOSE, ticket, now, InpAsyncMode ? 400 : 150);
Lg((partial ? "PARTIAL " : "CLOSE ") + sym + " #" + IntegerToString((long)ticket) +
" vol=" + DoubleToString(vol, g_sc[si].vdig) +
" master->send=" + IntegerToString(detect) + "ms call=" + IntegerToString((int)(t1 - t0)) + "ms");
return;
}
uint rc = (rs.retcode == 0) ? TRADE_RETCODE_REJECT : rs.retcode;
if(rc == TRADE_RETCODE_INVALID_FILL) RotateFill(sym);
RFail(K_CLOSE, ticket, rc, now);
g_lastError = RcText(rc);
Print("CLOSE FAIL [", sym, "] #", ticket, " ", RcText(rc));
}
//+------------------------------------------------------------------+
bool OpenTrade(const MTrade &t, uint now)
{
string sym = t.symbol;
int si = SymIdx(sym);
if(si < 0)
{
g_lastError = "Not found: " + sym;
RFail(K_OPEN, t.ticket, 0, now);
Print("Symbol not found: [", sym, "] - check symbol mapping");
return false;
}
bool wantBuy = ((t.type == 0) != InpReverseTrade);
long tm = SymbolInfoInteger(sym, SYMBOL_TRADE_MODE);
if(tm == SYMBOL_TRADE_MODE_DISABLED || tm == SYMBOL_TRADE_MODE_CLOSEONLY ||
(wantBuy && tm == SYMBOL_TRADE_MODE_SHORTONLY) || (!wantBuy && tm == SYMBOL_TRADE_MODE_LONGONLY))
{
g_lastError = "Trade mode: " + sym;
RFail(K_OPEN, t.ticket, TRADE_RETCODE_TRADE_DISABLED, now);
return false;
}
MqlTick tk;
if(!SymbolInfoTick(sym, tk) || tk.ask <= 0 || tk.bid <= 0)
{
RFail(K_OPEN, t.ticket, TRADE_RETCODE_PRICE_OFF, now);
return false;
}
double lots = CalcLots(t.lots, si);
if(lots <= 0) return false;
ENUM_ORDER_TYPE otype = wantBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
double price = wantBuy ? tk.ask : tk.bid;
double margin = 0;
if(OrderCalcMargin(otype, sym, lots, price, margin))
{
bool bad = (margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE));
if(!bad && InpMinMarginLevel > 0)
{
double used = AccountInfoDouble(ACCOUNT_MARGIN) + margin;
if(used > 0 && AccountInfoDouble(ACCOUNT_EQUITY) / used * 100.0 < InpMinMarginLevel) bad = true;
}
if(bad)
{
g_lastError = "Margin too low";
RFail(K_OPEN, t.ticket, TRADE_RETCODE_NO_MONEY, now);
Print("OPEN SKIP [", sym, "] margin too low (need ", DoubleToString(margin, 2), ")");
return false;
}
}
double sl = 0, tp = 0;
if(InpCopySL || InpCopyTP)
{
double msl = InpReverseTrade ? t.tp : t.sl;
double mtp = InpReverseTrade ? t.sl : t.tp;
double lvl = (double)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL) * g_sc[si].point;
if(InpCopySL && msl > 0)
{
double v = Snap(msl, si);
if(wantBuy ? (v < tk.bid - lvl) : (v > tk.ask + lvl)) sl = v;
}
if(InpCopyTP && mtp > 0)
{
double v = Snap(mtp, si);
if(wantBuy ? (v > tk.bid + lvl) : (v < tk.ask - lvl)) tp = v;
}
}
MqlTradeRequest rq;
MqlTradeResult rs;
ZeroMemory(rq);
ZeroMemory(rs);
rq.action = TRADE_ACTION_DEAL;
rq.symbol = sym;
rq.volume = lots;
rq.type = otype;
rq.price = price;
rq.sl = sl;
rq.tp = tp;
rq.deviation = InpSlippage;
rq.magic = InpMagicNumber;
rq.comment = CMT + IntegerToString((long)t.ticket);
rq.type_filling = (ENUM_ORDER_TYPE_FILLING)g_sc[si].fillOpen;
uint t0 = GetTickCount();
int detect = (int)(t0 - g_changeTick);
bool sent = SendReq(rq, rs);
uint t1 = GetTickCount();
if(sent && RcOK(rs.retcode))
{
RSuccess(K_OPEN, t.ticket, now, InpAsyncMode ? (uint)MathMax(500, InpPendingMs) : 500);
if(!InpAsyncMode) AddDone(t.ticket);
g_lastError = "";
Lg("OPEN " + sym + " " + EnumToString(otype) + " lot=" + DoubleToString(lots, g_sc[si].vdig) +
" #" + IntegerToString((long)t.ticket) +
" ipc=" + IntegerToString(g_lat) + "ms master->send=" + IntegerToString(detect) +
"ms call=" + IntegerToString((int)(t1 - t0)) + "ms");
return true;
}
uint rc = (rs.retcode == 0) ? TRADE_RETCODE_REJECT : rs.retcode;
if(rc == TRADE_RETCODE_INVALID_FILL) RotateFill(sym);
RFail(K_OPEN, t.ticket, rc, now);
g_lastError = RcText(rc);
Print("OPEN FAIL [", sym, "] #", t.ticket, " ", RcText(rc));
return false;
}
//+------------------------------------------------------------------+
//| Panel |
//+------------------------------------------------------------------+
void CreatePanel()
{
ObjectsDeleteAll(0, P);
ObjectCreate(0, P+"bg", OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, P+"bg", OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, P+"bg", OBJPROP_XDISTANCE, 8);
ObjectSetInteger(0, P+"bg", OBJPROP_YDISTANCE, 18);
ObjectSetInteger(0, P+"bg", OBJPROP_XSIZE, 300);
ObjectSetInteger(0, P+"bg", OBJPROP_YSIZE, 150);
ObjectSetInteger(0, P+"bg", OBJPROP_BGCOLOR, C'12,12,18');
ObjectSetInteger(0, P+"bg", OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, P+"bg", OBJPROP_COLOR, clrDodgerBlue);
ObjectSetInteger(0, P+"bg", OBJPROP_WIDTH, 1);
ObjectSetInteger(0, P+"bg", OBJPROP_BACK, false);
ObjectSetInteger(0, P+"bg", OBJPROP_SELECTABLE, false);
Lbl(P+"t", 16, 26, "SLAVE v5.00", 10, clrDodgerBlue);
Lbl(P+"s", 16, 48, "Status: ...", 9, clrOrange);
Lbl(P+"a", 16, 66, "Master: --", 9, clrWhite);
Lbl(P+"l", 16, 86, "IPC: -- ms", 9, clrAqua);
Lbl(P+"m", 16, 106, "M:0 S:0", 9, clrWhite);
Lbl(P+"c", 16, 124, "Copied: 0", 9, clrSilver);
Lbl(P+"e", 16, 140, "", 8, clrOrange);
}
void Lbl(string n, int x, int y, string t, int sz, color c)
{
ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
ObjectSetString(0, n, OBJPROP_TEXT, t);
ObjectSetString(0, n, OBJPROP_FONT, "Arial");
ObjectSetInteger(0, n, OBJPROP_FONTSIZE, sz);
ObjectSetInteger(0, n, OBJPROP_COLOR, c);
ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
}
void UpdatePanel()
{
uint n = GetTickCount();
if((int)(n - g_lastPanel) < InpPanelMs) return;
g_lastPanel = n;
string st; color sc;
if(!g_connected) { st = "Status: MASTER LOST"; sc = clrRed; }
else if(!g_mReady) { st = "Status: MASTER WARM-UP (idle)"; sc = clrOrange; }
else if(g_tradeBlocked) { st = "Status: AUTOTRADING OFF"; sc = clrRed; }
else { st = "Status: CONNECTED"; sc = clrLime; }
ObjectSetString (0, P+"s", OBJPROP_TEXT, st);
ObjectSetInteger(0, P+"s", OBJPROP_COLOR, sc);
ObjectSetString (0, P+"a", OBJPROP_TEXT, "Master: " + (g_masterAcc > 0 ? IntegerToString(g_masterAcc) : "--"));
string ls = "IPC: " + (g_lat >= 0 ? IntegerToString(g_lat) : "--") + " ms";
if(g_latN > 0) ls += " avg " + DoubleToString(g_latSum / g_latN, 1) + " max " + IntegerToString(g_latMax);
ObjectSetString(0, P+"l", OBJPROP_TEXT, ls);
ObjectSetString(0, P+"m", OBJPROP_TEXT, "M:" + IntegerToString(g_mCount) + " S:" + IntegerToString(g_spN) +
(InpAsyncMode ? " ASYNC" : " SYNC"));
ObjectSetString(0, P+"c", OBJPROP_TEXT, "Copied: " + IntegerToString((long)g_copied));
ObjectSetString(0, P+"e", OBJPROP_TEXT, g_lastError);
ChartRedraw(0);
}
//+------------------------------------------------------------------+