#property strict
#property version   "1.70"
#property description "BADZO MT5 copier - follower executor"

#include <Trade/Trade.mqh>

enum BADZO_LOT_MODE { BADZO_FIXED=0, BADZO_MULTIPLIER=1, BADZO_SAME=2 };

// Embedded BADZO_Emergency.mqh
#ifndef BADZO_EMERGENCY_MQH
#define BADZO_EMERGENCY_MQH
#include <Trade/Trade.mqh>

// Remote close requests are accepted only through an authenticated telemetry
// response, for the account that made that exact request. No opening orders.
CTrade bzEmergencyTrade;
string bzPauseFile="",bzCommandId="",bzEmergencyState="idle",bzEmergencyMessage="";
long bzCommandLogin=0;
string bzCommandServer="";
bool bzEmergencyEnabled=false,bzEmergencyActive=false,bzCancelPending=false,bzPauseWriteFailed=false,bzPausePending=false,bzOrdersTurn=true;
bool bzResumeEnabled=false,bzResumeActive=false;
string bzResumeId="",bzResumePauseId="",bzResumeState="idle",bzResumeMessage="",bzResumeServer="";
long bzResumeLogin=0;
ulong bzResumeDeadline=0;
ulong bzEmergencyDeadline=0,bzLastAttempt=0,bzEmergencyDeviation=100;
uint bzEmergencyRetcode=0;

string BzEscape(string value)
  {
   StringReplace(value,"\\","\\\\"); StringReplace(value,"\"","\\\"");
   StringReplace(value,"\r","\\r"); StringReplace(value,"\n","\\n"); StringReplace(value,"\t","\\t");
   return(value);
  }

void BzEmergencyInit(const string stateFile,const bool enabled,const ulong deviation,const bool resumeEnabled)
  {
   bzPauseFile=stateFile+".emergency.pause";
   bzEmergencyEnabled=enabled; bzEmergencyDeviation=deviation;
   bzResumeEnabled=resumeEnabled;
   bzEmergencyTrade.SetAsyncMode(false);
   bzEmergencyTrade.SetDeviationInPoints(deviation);
   if(FileIsExist(bzPauseFile,FILE_COMMON)) Print("BADZO COPIER PAUSED: emergency pause is still active.");
  }

bool BzEmergencyPaused()
  {
   return(bzPauseWriteFailed || bzEmergencyActive || FileIsExist(bzPauseFile,FILE_COMMON));
  }

bool BzTradeAllowed()
  {
   return(TerminalInfoInteger(TERMINAL_CONNECTED)!=0 &&
          TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)!=0 &&
          MQLInfoInteger(MQL_TRADE_ALLOWED)!=0 &&
          AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)!=0 &&
          AccountInfoInteger(ACCOUNT_TRADE_EXPERT)!=0);
  }

bool BzReadPause(string &pauseId,datetime &until)
  {
   int file=FileOpen(bzPauseFile,FILE_READ|FILE_CSV|FILE_COMMON|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE,';');
   if(file==INVALID_HANDLE) return(false);
   string header=FileReadString(file); until=(datetime)StringToInteger(FileReadString(file)); pauseId=FileReadString(file);
   FileClose(file); return(header=="BZE1" && until>0 && StringLen(pauseId)==36);
  }

bool BzReadResumeReceipt(string &resumeId,string &pauseId)
  {
   int file=FileOpen(bzPauseFile+".resumed",FILE_READ|FILE_CSV|FILE_COMMON|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE,';');
   if(file==INVALID_HANDLE) return(false);
   string header=FileReadString(file); resumeId=FileReadString(file); pauseId=FileReadString(file); FileClose(file);
   return(header=="BZR1" && StringLen(resumeId)==36 && StringLen(pauseId)==36);
  }

int BzWritePause(const int ttl)
  {
   // Exclusive common-file lock coordinates close, resume and the manual script.
   int lock=FileOpen(bzPauseFile+".lock",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(lock==INVALID_HANDLE) return(0); // Retry next timer tick; never block closes.
   datetime until=TimeGMT()+ttl+10;
   string previousId=""; datetime oldUntil=0;
   if(BzReadPause(previousId,oldUntil) && oldUntil>until)
     {if(previousId!=bzCommandId) {FileClose(lock);return(-1);} until=oldUntil;}
   int file=FileOpen(bzPauseFile,FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE,';');
   if(file==INVALID_HANDLE) {FileClose(lock);return(-1);}
   uint written=FileWrite(file,"BZE1",(long)until,bzCommandId);
   FileFlush(file); FileClose(file);
   bool ok=written>0 && FileIsExist(bzPauseFile,FILE_COMMON); FileClose(lock);
   return(ok?1:-1);
  }

void BzEmergencyHandle(const string responseHeaders,const long requestLogin,const string requestServer,const ulong elapsedMs)
  {
   if(!bzEmergencyEnabled || AccountInfoInteger(ACCOUNT_LOGIN)!=requestLogin || AccountInfoString(ACCOUNT_SERVER)!=requestServer) return;
   string lines[]; int total=StringSplit(responseHeaders,'\n',lines);
   for(int i=0;i<total;i++)
     {
      string line=lines[i],lower=line; StringToLower(lower);
      if(StringFind(lower,"x-badzo-emergency:")!=0) continue;
      string directive=StringSubstr(line,18); StringTrimLeft(directive); StringTrimRight(directive);
      string parts[]; if(StringSplit(directive,'|',parts)!=5 || parts[0]!="BZE1") return;
      if(StringLen(parts[1])!=36 || parts[4]!=(string)requestLogin) return;
      for(int c=0;c<36;c++)
        {
         ushort ch=StringGetCharacter(parts[1],c);
         if(!((ch>='0' && ch<='9') || (ch>='a' && ch<='f') || ch=='-')) return;
        }
      int ttl=(int)StringToInteger(parts[2]);
      if(ttl<1 || ttl>120 || (parts[3]!="0" && parts[3]!="1")) return;
      ulong lifetime=(ulong)ttl*1000;
      if(lifetime<=elapsedMs) return;
      if(parts[1]==bzCommandId) return; // Retries never extend the deadline.
      bzCommandId=parts[1]; bzCommandLogin=requestLogin; bzCommandServer=requestServer;
      bzCancelPending=(parts[3]=="1"); bzOrdersTurn=true;
      bzEmergencyDeadline=GetTickCount64()+lifetime-elapsedMs;
      bzLastAttempt=0; bzEmergencyRetcode=0; bzEmergencyMessage="";
      bzResumeActive=false; bzResumeState="failed"; bzResumeMessage="emergency_priority";
      bzPauseWriteFailed=false; bzPausePending=true;
      bzEmergencyActive=true; bzEmergencyState="working";
      Print("BADZO EMERGENCY RECEIVED: ",bzCommandId,". All symbols and all magic numbers on this account are included.");
      return;
     }
  }

void BzEmergencyResult(const bool sent)
  {
   bzEmergencyRetcode=bzEmergencyTrade.ResultRetcode();
   if(!sent || (bzEmergencyRetcode!=TRADE_RETCODE_DONE && bzEmergencyRetcode!=TRADE_RETCODE_DONE_PARTIAL))
     {
      bzEmergencyMessage="broker_rejected: "+StringSubstr(bzEmergencyTrade.ResultRetcodeDescription(),0,180);
      Print("BADZO EMERGENCY RETRY: ",bzEmergencyRetcode," ",bzEmergencyMessage);
     }
   else if(bzEmergencyRetcode==TRADE_RETCODE_DONE_PARTIAL) bzEmergencyMessage="partial_close";
  }

void BzEmergencyTick()
  {
   if(!bzEmergencyActive) return;
   if(AccountInfoInteger(ACCOUNT_LOGIN)!=bzCommandLogin || AccountInfoString(ACCOUNT_SERVER)!=bzCommandServer)
     {
      bzEmergencyActive=false; bzEmergencyState="failed"; bzEmergencyMessage="account_changed";
      Print("BADZO EMERGENCY STOPPED: account changed. Copier remains paused.");
      return;
     }
   ulong now=GetTickCount64();
   if(now>=bzEmergencyDeadline)
     {
      if(bzPausePending) bzPauseWriteFailed=true;
      bzEmergencyActive=false; bzEmergencyState="expired"; bzEmergencyMessage="request_expired";
      Print("BADZO EMERGENCY EXPIRED: check remaining positions in MT5. Copier remains paused.");
      return;
     }
   if(bzLastAttempt>0 && now-bzLastAttempt<700) return;
   bzLastAttempt=now;
   if(bzPausePending)
     {
      int pauseResult=BzWritePause((int)((bzEmergencyDeadline-now+999)/1000));
      if(pauseResult==0) return;
      if(pauseResult<0)
        {bzPauseWriteFailed=true;bzEmergencyActive=false;bzEmergencyState="failed";bzEmergencyMessage="pause_file_error";return;}
      bzPausePending=false;
     }
   if(PositionsTotal()==0 && (!bzCancelPending || OrdersTotal()==0))
     {
      bzEmergencyActive=false; bzEmergencyState="closed"; bzEmergencyMessage=""; return;
     }
   if(!BzTradeAllowed())
     {
      bzEmergencyMessage=TerminalInfoInteger(TERMINAL_CONNECTED)==0?"terminal_offline":"algo_disabled";
      return;
     }
   bzEmergencyMessage="";
   int actions=0;
   // Delete waiting orders first, only when explicitly selected on the site.
   if(bzCancelPending && bzOrdersTurn)
     {
      for(int i=OrdersTotal()-1;i>=0;i--)
        {
         if(GetTickCount64()>=bzEmergencyDeadline || GetTickCount64()-now>=1500 || actions>=12) break;
         ulong ticket=OrderGetTicket(i); if(ticket==0 || !OrderSelect(ticket)) continue;
         ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
         if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL) continue;
         bool sent=bzEmergencyTrade.OrderDelete(ticket); actions++; BzEmergencyResult(sent);
        }
     }
   bzOrdersTurn=!bzOrdersTurn; // Rejected pending orders cannot starve position closes.
   for(int i=PositionsTotal()-1;i>=0;i--)
     {
      if(GetTickCount64()>=bzEmergencyDeadline || GetTickCount64()-now>=1500 || actions>=12) break;
      ulong ticket=PositionGetTicket(i); if(ticket==0 || !PositionSelectByTicket(ticket)) continue;
      string symbol=PositionGetString(POSITION_SYMBOL);
      bzEmergencyTrade.SetTypeFillingBySymbol(symbol);
      bool sent=bzEmergencyTrade.PositionClose(ticket,bzEmergencyDeviation); actions++; BzEmergencyResult(sent);
     }
   // A successful function call alone is not execution confirmation. Check
   // broker return codes above and the actual remaining positions/orders here.
   if(PositionsTotal()==0 && (!bzCancelPending || OrdersTotal()==0))
     {
      bzEmergencyActive=false; bzEmergencyState="closed"; bzEmergencyMessage="";
      Print("BADZO EMERGENCY CLOSED: ",bzCommandId,". Copier remains paused.");
     }
  }

string BzEmergencyJson()
  {
   return(StringFormat("{\"version\":1,\"enabled\":%s,\"allowed\":%s,\"paused\":%s,\"commandId\":\"%s\",\"state\":\"%s\",\"remaining\":%d,\"ordersRemaining\":%d,\"retcode\":%u,\"message\":\"%s\"}",
          bzEmergencyEnabled?"true":"false",BzTradeAllowed()?"true":"false",BzEmergencyPaused()?"true":"false",
          bzCommandId,bzEmergencyState,PositionsTotal(),OrdersTotal(),bzEmergencyRetcode,BzEscape(bzEmergencyMessage)));
  }
bool BzUuid(const string value)
  {
   if(StringLen(value)!=36) return(false);
   for(int i=0;i<36;i++)
     {
      ushort c=StringGetCharacter(value,i);
      if(i==8 || i==13 || i==18 || i==23) {if(c!='-') return(false);}
      else if(!((c>='0' && c<='9') || (c>='a' && c<='f'))) return(false);
     }
   return(true);
  }

void BzResumeHandle(const string responseHeaders,const long requestLogin,const string requestServer,const ulong elapsedMs)
  {
   if(!bzResumeEnabled || AccountInfoInteger(ACCOUNT_LOGIN)!=requestLogin || AccountInfoString(ACCOUNT_SERVER)!=requestServer) return;
   string lowerHeaders=responseHeaders;StringToLower(lowerHeaders);
   if(StringFind(lowerHeaders,"x-badzo-emergency:")>=0) return;
   string lines[];int count=StringSplit(responseHeaders,'\n',lines);
   for(int i=0;i<count;i++)
     {
      string lower=lines[i];StringToLower(lower);
      if(StringFind(lower,"x-badzo-resume:")!=0) continue;
      string value=StringSubstr(lines[i],15);StringTrimLeft(value);StringTrimRight(value);
      string parts[];if(StringSplit(value,'|',parts)!=5 || parts[0]!="BZR1" || !BzUuid(parts[1]) || !BzUuid(parts[3]) || parts[4]!=(string)requestLogin) return;
      int ttl=(int)StringToInteger(parts[2]);if(ttl<1 || ttl>60 || (ulong)ttl*1000<=elapsedMs) return;
      if(parts[1]==bzResumeId) return;
      bzResumeId=parts[1];bzResumePauseId=parts[3];bzResumeLogin=requestLogin;bzResumeServer=requestServer;
      bzResumeDeadline=GetTickCount64()+(ulong)ttl*1000-elapsedMs;bzResumeActive=true;bzResumeState="working";bzResumeMessage="";
      return;
     }
  }

void BzResumeTick()
  {
   if(!bzResumeActive) return;
   if(GetTickCount64()>=bzResumeDeadline) {bzResumeActive=false;bzResumeState="expired";bzResumeMessage="request_expired";return;}
   if(AccountInfoInteger(ACCOUNT_LOGIN)!=bzResumeLogin || AccountInfoString(ACCOUNT_SERVER)!=bzResumeServer)
     {bzResumeActive=false;bzResumeState="failed";bzResumeMessage="account_changed";return;}
   if(bzEmergencyActive || bzPauseWriteFailed)
     {bzResumeActive=false;bzResumeState="failed";bzResumeMessage="emergency_priority";return;}
   int lock=FileOpen(bzPauseFile+".lock",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(lock==INVALID_HANDLE) {bzResumeMessage="lock_busy";return;}
   if(FileIsExist(bzPauseFile,FILE_COMMON))
     {
      string pauseId="";datetime until=0;
      if(!BzReadPause(pauseId,until) || pauseId!=bzResumePauseId)
        {FileClose(lock);bzResumeActive=false;bzResumeState="failed";bzResumeMessage="pause_changed";return;}
      if(TimeGMT()<until) {FileClose(lock);bzResumeMessage="cooldown";return;}
      if(!BzTradeAllowed() || PositionsTotal()>0 || OrdersTotal()>0)
        {FileClose(lock);bzResumeActive=false;bzResumeState="failed";bzResumeMessage="terminal_not_ready";return;}
      if(GetTickCount64()>=bzResumeDeadline) {FileClose(lock);return;}
      // Persist the receipt before removal. Every member can acknowledge the
      // same group action even if a terminal restarts before its next POST.
      int receipt=FileOpen(bzPauseFile+".resumed",FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE,';');
      if(receipt==INVALID_HANDLE) {FileClose(lock);bzResumeMessage="file_error";return;}
      uint written=FileWrite(receipt,"BZR1",bzResumeId,bzResumePauseId);FileFlush(receipt);FileClose(receipt);
      if(written==0 || GetTickCount64()>=bzResumeDeadline || !FileDelete(bzPauseFile,FILE_COMMON)) {FileClose(lock);bzResumeMessage="file_error";return;}
     }
   string savedId="",savedPause="";
   bool verified=BzReadResumeReceipt(savedId,savedPause) && savedId==bzResumeId && savedPause==bzResumePauseId && !FileIsExist(bzPauseFile,FILE_COMMON);
   FileClose(lock);bzResumeActive=false;bzResumeState=verified?"resumed":"failed";bzResumeMessage=verified?"":"pause_changed";
   if(verified) Print("BADZO COPIER RESUMED FROM SITE: ",bzResumeId);
  }

string BzResumeJson()
  {
   string pauseId="";datetime until=0;BzReadPause(pauseId,until);
   string reportId=bzResumeId,reportPause=bzResumePauseId,state=bzResumeState,message=bzResumeMessage;
   if(!BzEmergencyPaused())
     {
      string savedId="",savedPause="";
      if(BzReadResumeReceipt(savedId,savedPause) && BzUuid(savedId) && BzUuid(savedPause))
        {reportId=savedId;reportPause=savedPause;state="resumed";message="";}
     }
   return(StringFormat("{\"version\":1,\"enabled\":%s,\"pauseId\":\"%s\",\"commandId\":\"%s\",\"commandPauseId\":\"%s\",\"state\":\"%s\",\"message\":\"%s\"}",
          bzResumeEnabled?"true":"false",BzEscape(pauseId),reportId,reportPause,state,BzEscape(message)));
  }
#endif

// End embedded BADZO_Emergency.mqh


// Embedded BADZO_Monitor.mqh
#ifndef BADZO_MONITOR_MQH
#define BADZO_MONITOR_MQH

// Read-only telemetry helpers. No order requests are sent from this file.
string BzMonitorEscape(string value)
  {
   StringReplace(value,"\\","\\\\");
   StringReplace(value,"\"","\\\"");
   StringReplace(value,"\r","\\r");
   StringReplace(value,"\n","\\n");
   StringReplace(value,"\t","\\t");
   return(value);
  }

// Parse the entire numeric reference, so BZ:123 never matches BZ:1234.
ulong BzMonitorSourceTicket(const string comment)
  {
   if(StringFind(comment,"BZ:")!=0) return(0);
   int end=3,length=StringLen(comment);
   while(end<length)
     {
      ushort ch=StringGetCharacter(comment,end);
      if(ch<48 || ch>57) break;
      end++;
     }
   if(end==3) return(0);
   if(end<length)
     {
      ushort ch=StringGetCharacter(comment,end);
      if(ch!=32 && ch!=91) return(0);
     }
   return((ulong)StringToInteger(StringSubstr(comment,3,end-3)));
  }

string BzMonitorPosition(const bool managed,const ulong sourceTicket)
  {
   return(StringFormat(",\"identifier\":\"%I64d\",\"copyManaged\":%s,\"sourceTicket\":\"%s\"",
      PositionGetInteger(POSITION_IDENTIFIER),managed?"true":"false",sourceTicket>0?(string)sourceTicket:""));
  }

string BzMonitorSymbol(const string symbol)
  {
   return(StringFormat("\"point\":%.10f,\"volumeStep\":%.8f,\"volumeMin\":%.8f,\"volumeMax\":%.8f,\"hedging\":%s",
      SymbolInfoDouble(symbol,SYMBOL_POINT),SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP),
      SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN),SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX),
      AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING?"true":"false"));
  }
#endif

// End embedded BADZO_Monitor.mqh


// Embedded BADZO_HistorySync.mqh
#ifndef BADZO_HISTORY_SYNC_MQH
#define BADZO_HISTORY_SYNC_MQH

// A history batch travels with the existing telemetry request. No extra network
// request, trading action, chart or timer is created by this module.
int bzHistoryCursor=0,bzHistoryNext=0;
long bzHistoryLogin=0;
string bzHistoryServer="",bzHistoryPending="";
datetime bzHistoryFrom=0;
bool bzHistoryCatchingUp=true,bzHistoryComplete=false;
ulong bzHistoryLastComplete=0,bzHistoryLastScan=0,bzHistoryLastOffer=0,bzHistoryLastError=0;

void BzHistoryReset(const long login,const string server)
  {
   bzHistoryLogin=login; bzHistoryServer=server;
   bzHistoryCursor=0; bzHistoryNext=0; bzHistoryFrom=0; bzHistoryPending="";
   bzHistoryCatchingUp=true; bzHistoryComplete=false;
   bzHistoryLastComplete=0; bzHistoryLastScan=0; bzHistoryLastOffer=0;
  }

string BzHistoryPrepare(const bool enabled,const int days,const int refresh)
  {
   if(!enabled || !TerminalInfoInteger(TERMINAL_CONNECTED)) return("");
   long login=AccountInfoInteger(ACCOUNT_LOGIN); string server=AccountInfoString(ACCOUNT_SERVER);
   if(login!=bzHistoryLogin || server!=bzHistoryServer) BzHistoryReset(login,server);
   ulong tick=GetTickCount64();
   if(bzHistoryLastOffer>0 && tick-bzHistoryLastOffer<2000) return("");
   if(bzHistoryPending!="") {bzHistoryLastOffer=tick;return(bzHistoryPending);}
   int interval=MathMax(5,refresh),historyDays=MathMax(1,MathMin(3650,days));
   if(!bzHistoryCatchingUp && tick-bzHistoryLastComplete<(ulong)interval*1000) return("");
   datetime now=TimeCurrent(); if(now<=0) return("");
   if(bzHistoryFrom==0) {bzHistoryFrom=now-(long)historyDays*86400;bzHistoryLastScan=tick;}
   if(tick-bzHistoryLastScan>=900000) {bzHistoryCursor=0;bzHistoryCatchingUp=true;bzHistoryLastScan=tick;}
   if(!HistorySelect(bzHistoryFrom,now)) return("");
   int total=HistoryDealsTotal();
   if(bzHistoryCursor>total) bzHistoryCursor=0;
   int next=bzHistoryCatchingUp?bzHistoryCursor:MathMax(0,bzHistoryCursor-40);
   int sent=0,scanned=0; string list="";
   while(next<total && sent<40 && scanned<500)
     {
      ulong ticket=HistoryDealGetTicket(next); if(ticket==0) return("");
      next++;scanned++;
      int side=(int)HistoryDealGetInteger(ticket,DEAL_TYPE);
      string symbol=HistoryDealGetString(ticket,DEAL_SYMBOL),upper=symbol;StringToUpper(upper);
      if((side!=DEAL_TYPE_BUY && side!=DEAL_TYPE_SELL) || StringFind(upper,"XAUUSD")!=0) continue;
      if(sent>0) list+=",";
      list+=StringFormat("{\"ticket\":\"%I64u\",\"positionId\":\"%I64d\",\"symbol\":\"%s\",\"time\":%I64d,\"entry\":%d,\"side\":%d,\"volume\":%.8f,\"price\":%.8f,\"profit\":%.8f,\"commission\":%.8f,\"swap\":%.8f,\"fee\":%.8f}",ticket,HistoryDealGetInteger(ticket,DEAL_POSITION_ID),BzEscape(symbol),HistoryDealGetInteger(ticket,DEAL_TIME),(int)HistoryDealGetInteger(ticket,DEAL_ENTRY),side,HistoryDealGetDouble(ticket,DEAL_VOLUME),HistoryDealGetDouble(ticket,DEAL_PRICE),HistoryDealGetDouble(ticket,DEAL_PROFIT),HistoryDealGetDouble(ticket,DEAL_COMMISSION),HistoryDealGetDouble(ticket,DEAL_SWAP),HistoryDealGetDouble(ticket,DEAL_FEE));
      sent++;
     }
   bzHistoryNext=next; bzHistoryComplete=next>=total;
   bzHistoryPending=StringFormat("{\"account\":{\"login\":\"%I64d\",\"server\":\"%s\",\"currency\":\"%s\"},\"from\":%I64d,\"to\":%I64d,\"complete\":%s,\"deals\":[%s]}",login,BzEscape(server),BzEscape(AccountInfoString(ACCOUNT_CURRENCY)),(long)bzHistoryFrom,(long)now,bzHistoryComplete?"true":"false",list);
   bzHistoryLastOffer=tick;
   return(bzHistoryPending);
  }

void BzHistoryAcknowledge(const string responseHeaders,const long requestLogin,const string requestServer)
  {
   if(bzHistoryPending=="" || requestLogin!=bzHistoryLogin || requestServer!=bzHistoryServer) return;
   if(AccountInfoInteger(ACCOUNT_LOGIN)!=requestLogin || AccountInfoString(ACCOUNT_SERVER)!=requestServer) return;
   string lines[];int count=StringSplit(responseHeaders,'\n',lines);
   for(int i=0;i<count;i++)
     {
      string line=lines[i];StringTrimLeft(line);StringTrimRight(line);StringToLower(line);
      if(StringFind(line,"x-badzo-history:")!=0) continue;
      string value=StringSubstr(line,16);StringTrimLeft(value);StringTrimRight(value);
      if(value!="accepted")
        {
         if(GetTickCount64()-bzHistoryLastError>=30000) {Print("BADZO HISTORY: batch retained for retry: ",value);bzHistoryLastError=GetTickCount64();}
         return;
        }
      bzHistoryCursor=bzHistoryNext;bzHistoryPending="";
      if(bzHistoryComplete)
        {
         if(bzHistoryCatchingUp) Print("BADZO HISTORY SYNCED: history received through telemetry.");
         bzHistoryLastComplete=GetTickCount64();
        }
      bzHistoryCatchingUp=!bzHistoryComplete;
      return;
     }
  }
#endif

// End embedded BADZO_HistorySync.mqh


// Embedded BADZO_Runtime.mqh
#ifndef BADZO_RUNTIME_MQH
#define BADZO_RUNTIME_MQH

// Monitoring stays alive even when execution is blocked. Trade events only
// mark data dirty; network work never runs inside OnTradeTransaction.
bool bzTelemetryDirty=true;
ulong bzTelemetryLastAttempt=0,bzTelemetryRetryAt=0;
long bzRuntimeLogin=0;
string bzRuntimeServer="",bzExecutionCode="starting",bzExecutionMessage="Pokretanje EA-a.";
uint bzExecutionRetcode=0;
ulong bzExecutionTicket=0;
double bzObservedBalance=0.0;

void BzExecution(const string code,const string message,const uint retcode=0,const ulong ticket=0)
  {
   if(code!=bzExecutionCode || message!=bzExecutionMessage || retcode!=bzExecutionRetcode)
     {
      bzTelemetryDirty=true;
      if(code!="ready") Print("BADZO ",code,": ",message," retcode=",retcode);
     }
   bzExecutionCode=code; bzExecutionMessage=message; bzExecutionRetcode=retcode; bzExecutionTicket=ticket;
  }

bool BzObserveAccount()
  {
   long login=AccountInfoInteger(ACCOUNT_LOGIN);
   string server=AccountInfoString(ACCOUNT_SERVER);
   double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   bool changed=login!=bzRuntimeLogin || server!=bzRuntimeServer;
   if(changed)
     {
      bzRuntimeLogin=login; bzRuntimeServer=server;
      BzHistoryReset(login,server);
      bzTelemetryRetryAt=0; bzTelemetryLastAttempt=0;
     }
   if(changed || balance!=bzObservedBalance) bzTelemetryDirty=true;
   bzObservedBalance=balance;
   return(changed);
  }

string BzGoldSymbol(const string configured)
  {
   if(SymbolSelect(configured,true)) return(configured);
   string upper=_Symbol; StringToUpper(upper);
   // Only the generic default may resolve to the chart's broker suffix.
   // An explicitly configured but unavailable symbol is never substituted.
   if(configured=="XAUUSD" && StringFind(upper,"XAUUSD")==0 && SymbolSelect(_Symbol,true)) return(_Symbol);
   return(configured);
  }

bool BzExecutionAllowed(const string symbol,const bool demoOnly,const bool trading)
  {
   if(!TerminalInfoInteger(TERMINAL_CONNECTED) || AccountInfoInteger(ACCOUNT_LOGIN)<=0)
     {BzExecution("disconnected","MT5 nije povezan s brokerom; prikazan je posljednji poznati balans.");return(false);}
   if(demoOnly && AccountInfoInteger(ACCOUNT_TRADE_MODE)!=ACCOUNT_TRADE_MODE_DEMO)
     {BzExecution("demo_only","DemoOnly=true blokira live kopiranje. Podaci racuna se i dalje salju.");return(false);}
   if(!SymbolSelect(symbol,true) || SymbolInfoDouble(symbol,SYMBOL_POINT)<=0)
     {BzExecution("symbol_missing","Simbol nije dostupan: "+symbol+". Provjeri MT5 Inputs.");return(false);}
   if(BzEmergencyPaused()) {BzExecution("paused","Copier je pauziran; koristi Nastavi nakon provjere racuna.");return(false);}
   if(trading && !BzTradeAllowed())
     {BzExecution("trading_disabled","Provjeri Algo Trading, EA dozvolu i trgovacku (ne investor) prijavu.");return(false);}
   if(trading && AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     {BzExecution("netting","Ovaj copier zahtijeva hedging racun za odvojene kopije pozicija.");return(false);}
   return(true);
  }

string BzExecutionJson()
  {
   return(StringFormat("{\"code\":\"%s\",\"message\":\"%s\",\"retcode\":%u,\"sourceTicket\":\"%s\"}",BzEscape(bzExecutionCode),BzEscape(bzExecutionMessage),bzExecutionRetcode,bzExecutionTicket>0?(string)bzExecutionTicket:""));
  }

string BzAccountRuntimeJson()
  {
   return(StringFormat(",\"credit\":%.2f,\"freeMargin\":%.2f,\"terminal\":{\"connected\":%s,\"tradeAllowed\":%s}",AccountInfoDouble(ACCOUNT_CREDIT),AccountInfoDouble(ACCOUNT_MARGIN_FREE),TerminalInfoInteger(TERMINAL_CONNECTED)?"true":"false",BzTradeAllowed()?"true":"false"));
  }

bool BzTelemetryDue(const int seconds)
  {
   if(seconds<=0)return(false);
   ulong tick=GetTickCount64();
   return(tick>=bzTelemetryRetryAt && (bzTelemetryLastAttempt==0 || tick-bzTelemetryLastAttempt>=(ulong)(bzTelemetryDirty?500:MathMax(1,seconds)*1000)));
  }

void BzTelemetryResult(const int code)
  {
   bzTelemetryLastAttempt=GetTickCount64();
   if(code==200) {bzTelemetryDirty=false;bzTelemetryRetryAt=0;}
   else bzTelemetryRetryAt=bzTelemetryLastAttempt+5000;
  }

void BzAccountTransaction()
  {
   bzTelemetryDirty=true;
   bzHistoryLastComplete=0;
  }
#endif

// End embedded BADZO_Runtime.mqh

input bool   EnableEmergencyClose = true;
input bool   EnableRemoteResume = true;
input ulong  EmergencyDeviationPoints = 100;

input string StateFile = "BADZO_MASTER_STATE.csv";
input string MasterSymbol = "XAUUSD";
input string FollowerSymbol = "XAUUSD";
input BADZO_LOT_MODE LotMode = BADZO_FIXED;
input double FixedLot = 0.01;
input double LotMultiplier = 1.0;
input double MaxLot = 0.01;
input int    MaxSpreadPoints = 100;
input int    MagicNumber = 26092201;
input int    RefreshMilliseconds = 500;
input bool   CopySLTP = true;
input bool   CopyMasterClose = true;
input bool   CopyExistingPositionsOnStart = false;
input bool   DemoOnly = true;
input string TelemetryURL = "";
input string TelemetryKey = "";
input string SiteAccessKey = "";
input int    TelemetryIntervalSeconds = 1;
input bool   EnableHistorySync = true;
input int    HistoryDays = 90;
input int    HistoryRefreshSeconds = 30;

struct SourcePosition
  {
   ulong ticket;
   ENUM_POSITION_TYPE type;
   double volume;
   double sl;
   double tp;
  };

CTrade trade;
bool baselineDone=false;
string monitorSourceLogin="",monitorSourceServer="";
long monitorStateTime=0;
string activeFollowerSymbol="",activeMasterSymbol="",boundSource="",monitorReadError="";
ulong skippedTickets[],uncertainOpenTicket=0;
bool cycleFailed=false;

bool WasSkipped(const ulong ticket)
  {for(int i=0;i<ArraySize(skippedTickets);i++)if(skippedTickets[i]==ticket)return(true);return(false);}

int ManagedPositions()
  {
   int count=0;
   for(int i=0;i<PositionsTotal();i++)
     {ulong t=PositionGetTicket(i);if(t>0 && PositionSelectByTicket(t) && PositionGetString(POSITION_SYMBOL)==activeFollowerSymbol && PositionGetInteger(POSITION_MAGIC)==MagicNumber)count++;}
   return(count);
  }

void CopyFailure(const string code,const string message,const uint retcode=0,const ulong ticket=0)
  {cycleFailed=true;BzExecution(code,message,retcode,ticket);}


int OnInit()
  {
   baselineDone=false;boundSource="";uncertainOpenTicket=0;ArrayResize(skippedTickets,0);
   BzObserveAccount();
   activeFollowerSymbol=BzGoldSymbol(FollowerSymbol);activeMasterSymbol=MasterSymbol;
   BzEmergencyInit(StateFile,EnableEmergencyClose,EmergencyDeviationPoints,EnableRemoteResume);
   int interval=RefreshMilliseconds;
   if(interval<100) interval=100;
   trade.SetExpertMagicNumber(MagicNumber);
   trade.SetAsyncMode(false);
   EventSetMillisecondTimer(interval);
   Print("BADZO FOLLOWER READY: ",FollowerSymbol," waiting for master file");
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason) { EventKillTimer(); }

void OnTradeTransaction(const MqlTradeTransaction &trans,const MqlTradeRequest &request,const MqlTradeResult &result)
  { BzAccountTransaction(); }

string JsonSafe(string value) { return(BzMonitorEscape(value)); }

void SendTelemetry()
  {
   if(TelemetryURL=="" || TelemetryKey=="" || SiteAccessKey=="" || AccountInfoInteger(ACCOUNT_LOGIN)<=0) return;
   string payload=StringFormat("{\"source\":\"follower\",\"accounts\":[{\"role\":\"follower\",\"login\":\"%I64d\",\"server\":\"%s\",\"symbol\":\"%s\",\"balance\":%.2f,\"equity\":%.2f,\"positions\":[",AccountInfoInteger(ACCOUNT_LOGIN),JsonSafe(AccountInfoString(ACCOUNT_SERVER)),JsonSafe(activeFollowerSymbol),AccountInfoDouble(ACCOUNT_BALANCE),AccountInfoDouble(ACCOUNT_EQUITY));
   bool first=true;
   for(int i=0;i<PositionsTotal();i++)
     {
      ulong ticket=PositionGetTicket(i); if(ticket==0 || !PositionSelectByTicket(ticket)) continue;
      string symbol=PositionGetString(POSITION_SYMBOL);
      if(!first) payload+=","; first=false;
      payload+=StringFormat("{\"ticket\":\"%I64u\",\"symbol\":\"%s\",\"type\":%d,\"volume\":%.8f,\"price\":%.8f,\"sl\":%.8f,\"tp\":%.8f,\"profit\":%.2f%s}",ticket,JsonSafe(symbol),(int)PositionGetInteger(POSITION_TYPE),PositionGetDouble(POSITION_VOLUME),PositionGetDouble(POSITION_PRICE_OPEN),PositionGetDouble(POSITION_SL),PositionGetDouble(POSITION_TP),PositionGetDouble(POSITION_PROFIT),BzMonitorPosition(symbol==activeFollowerSymbol && PositionGetInteger(POSITION_MAGIC)==MagicNumber,(symbol==activeFollowerSymbol && PositionGetInteger(POSITION_MAGIC)==MagicNumber)?BzMonitorSourceTicket(PositionGetString(POSITION_COMMENT)):0));
     }
   string accountMode=AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL?"real":(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO?"demo":"contest");
   payload+=StringFormat("],\"currency\":\"%s\",\"accountMode\":\"%s\",\"group\":\"%s\"}]}",JsonSafe(AccountInfoString(ACCOUNT_CURRENCY)),accountMode,JsonSafe(StateFile));
   payload=StringSubstr(payload,0,StringLen(payload)-3)+BzAccountRuntimeJson()+",\"copier\":"+MonitorJson()+",\"emergency\":"+BzEmergencyJson()+",\"resume\":"+BzResumeJson()+"}]}";
   string historyBatch="";
   if(!bzEmergencyActive && !bzResumeActive) historyBatch=BzHistoryPrepare(EnableHistorySync,HistoryDays,HistoryRefreshSeconds);
   if(historyBatch!="") payload=StringSubstr(payload,0,StringLen(payload)-1)+",\"history\":"+historyBatch+"}";
   long requestLogin=AccountInfoInteger(ACCOUNT_LOGIN);
   string requestServer=AccountInfoString(ACCOUNT_SERVER);
   char post[],result[]; string responseHeaders;
   int bytes=StringToCharArray(payload,post,0,WHOLE_ARRAY,CP_UTF8);
   if(bytes>0) ArrayResize(post,bytes-1);
   ResetLastError();
   string headers="OAI-Sites-Authorization: Bearer "+SiteAccessKey+"\r\nContent-Type: application/json\r\nX-BADZO-TELEMETRY: "+TelemetryKey+"\r\n";
   ulong sentAt=GetTickCount64();
   int code=WebRequest("POST",TelemetryURL,headers,3000,post,result,responseHeaders);
   BzTelemetryResult(code);
   if(code==200) { BzEmergencyHandle(responseHeaders,requestLogin,requestServer,GetTickCount64()-sentAt); BzResumeHandle(responseHeaders,requestLogin,requestServer,GetTickCount64()-sentAt); }
   if(code==200 && historyBatch!="") BzHistoryAcknowledge(responseHeaders,requestLogin,requestServer);
   if(code==200) { static bool reported=false; if(!reported) { Print("BADZO TELEMETRY CONNECTED: HTTP 200"); reported=true; } }
   if(code<200 || code>=300) Print("BADZO TELEMETRY ERROR: HTTP ",code," ",GetLastError());
  }

string CommentFor(const ulong ticket) { return("BZ:"+(string)ticket); }

int VolumeDigits()
  {
   double step=SymbolInfoDouble(activeFollowerSymbol,SYMBOL_VOLUME_STEP);
   int digits=0;
   while(digits<8 && NormalizeDouble(step,digits)!=step) digits++;
   return(digits);
  }

double NormalizeVolume(double value)
  {
   double step=SymbolInfoDouble(activeFollowerSymbol,SYMBOL_VOLUME_STEP);
   double minimum=SymbolInfoDouble(activeFollowerSymbol,SYMBOL_VOLUME_MIN);
   double maximum=SymbolInfoDouble(activeFollowerSymbol,SYMBOL_VOLUME_MAX);
   if(step<=0 || minimum<=0 || maximum<=0) return(0.0);
   value=MathMin(value,MathMin(MaxLot,maximum));
   value=MathFloor((value+0.000000001)/step)*step;
   value=NormalizeDouble(value,VolumeDigits());
   if(value<minimum) return(0.0);
   return(value);
  }

bool HasSource(const SourcePosition &sources[],const ulong ticket)
  {
   for(int i=0;i<ArraySize(sources);i++) if(sources[i].ticket==ticket) return(true);
   return(false);
  }

bool ReadState(SourcePosition &sources[])
  {
   ArrayResize(sources,0);monitorSourceLogin="";monitorSourceServer="";monitorStateTime=0;monitorReadError="";
   int file=FileOpen(StateFile,FILE_READ|FILE_CSV|FILE_COMMON|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE,';');
   if(file==INVALID_HANDLE){monitorReadError="Nema master zapisa. Provjeri isti StateFile, VPS i Windows korisnika.";return(false);}
   string header=FileReadString(file),version=FileReadString(file);
   monitorStateTime=StringToInteger(FileReadString(file));
   if((header!="BADZO" && header!="BADZO_BLOCKED") || (version!="1" && version!="2"))
     {FileClose(file);monitorReadError="Neispravan format master zapisa.";return(false);}
   bool complete=version=="1",sourceAllowed=header=="BADZO",hasSource=false,invalid=false;
   int positionRows=0;
   while(!FileIsEnding(file))
     {
      string row=FileReadString(file);if(row=="")continue;
      string fields[7];
      for(int j=0;j<7;j++){if(FileIsEnding(file)){invalid=true;break;}fields[j]=FileReadString(file);}
      if(invalid)break;
      if(row=="SRC")
        {
         if(hasSource){invalid=true;break;}
         hasSource=true;monitorSourceLogin=fields[0];monitorSourceServer=fields[1];
         if(version=="2")
           {
            string upper=fields[3];StringToUpper(upper);
            if(MasterSymbol=="XAUUSD" && StringFind(upper,"XAUUSD")==0)activeMasterSymbol=fields[3];
            if(activeMasterSymbol!=fields[3]){invalid=true;monitorReadError="MasterSymbol ne odgovara simbolu mastera: "+fields[3];}
            if(fields[4]=="1" || fields[5]!="1")sourceAllowed=false;
           }
         continue;
        }
      if(row=="END")
        {complete=(int)StringToInteger(fields[0])==positionRows;break;}
      if(row!="POS"){invalid=true;break;}
      positionRows++;
      SourcePosition item;
      item.ticket=(ulong)StringToInteger(fields[0]);
      string symbol=fields[1];long type=StringToInteger(fields[2]);
      item.volume=StringToDouble(fields[3]);item.sl=StringToDouble(fields[5]);item.tp=StringToDouble(fields[6]);
      if(item.ticket==0 || item.volume<=0 || (type!=POSITION_TYPE_BUY && type!=POSITION_TYPE_SELL) || HasSource(sources,item.ticket)){invalid=true;break;}
      if(symbol!=activeMasterSymbol)
        {
         // Legacy masters do not advertise a symbol in SRC.
         string upper=symbol;StringToUpper(upper);
         if(version=="1" && MasterSymbol=="XAUUSD" && StringFind(upper,"XAUUSD")==0)activeMasterSymbol=symbol;
         else {invalid=true;monitorReadError="MasterSymbol ne odgovara zapisu: "+symbol;break;}
        }
      item.type=(ENUM_POSITION_TYPE)type;
      int n=ArraySize(sources);ArrayResize(sources,n+1);sources[n]=item;
     }
   FileClose(file);
   long age=(long)TimeLocal()-monitorStateTime;
   if(invalid || !complete || !hasSource || monitorSourceLogin=="" || monitorSourceServer=="")
     {if(monitorReadError=="")monitorReadError="Nepotpun master zapis; kopiranje i automatsko zatvaranje cekaju ispravan zapis.";return(false);}
   if(age<0 || age>10){monitorReadError="Master zapis je zastario. Provjeri radi li Master EA.";return(false);}
   if(!sourceAllowed){monitorReadError="Master je blokiran ili pauziran. Provjeri njegov status.";return(false);}
   return(true);
  }

string MonitorJson()
  {
   SourcePosition observed[];bool ready=ReadState(observed);
   long age=monitorStateTime>0?(long)TimeLocal()-monitorStateTime:-1;
   string value="{\"version\":1,\"eaVersion\":\"1.70\",\"masterSymbol\":\""+JsonSafe(activeMasterSymbol)+"\",\"followerSymbol\":\""+JsonSafe(activeFollowerSymbol)+"\","+BzMonitorSymbol(activeFollowerSymbol);
   value+=StringFormat(",\"lotMode\":%d,\"fixedLot\":%.8f,\"lotMultiplier\":%.8f,\"maxLot\":%.8f,\"copySLTP\":%s",(int)LotMode,FixedLot,LotMultiplier,MaxLot,CopySLTP?"true":"false");
   value+=",\"execution\":"+BzExecutionJson();
   value+=StringFormat(",\"source\":{\"ready\":%s,\"login\":\"%s\",\"server\":\"%s\",\"ageSeconds\":%I64d},\"sourcePositions\":[",ready?"true":"false",JsonSafe(monitorSourceLogin),JsonSafe(monitorSourceServer),age);
   for(int i=0;i<ArraySize(observed);i++)
     {
      if(i>0)value+=",";
      value+=StringFormat("{\"ticket\":\"%I64u\",\"type\":%d,\"volume\":%.8f,\"sl\":%.8f,\"tp\":%.8f,\"skippedOnStart\":%s}",observed[i].ticket,(int)observed[i].type,observed[i].volume,observed[i].sl,observed[i].tp,WasSkipped(observed[i].ticket)?"true":"false");
     }
   return(value+"]}");
  }

bool FindFollower(const ulong sourceTicket,ulong &followerTicket)
  {

   for(int i=0;i<PositionsTotal();i++)
     {
      ulong ticket=PositionGetTicket(i); if(ticket==0 || !PositionSelectByTicket(ticket)) continue;
      if(PositionGetString(POSITION_SYMBOL)!=activeFollowerSymbol) continue;
      if((long)PositionGetInteger(POSITION_MAGIC)!=MagicNumber) continue;
      if(BzMonitorSourceTicket(PositionGetString(POSITION_COMMENT))==sourceTicket) { followerTicket=ticket; return(true); }
     }
   return(false);
  }

void OpenFollower(const SourcePosition &source)
  {
   if(uncertainOpenTicket>0){CopyFailure("unconfirmed","Prethodni nalog nema potvrdu izvrsenja. Provjeri Trade/Experts prije ponovnog pokretanja.",0,uncertainOpenTicket);return;}
   double desired=LotMode==BADZO_FIXED?FixedLot:(LotMode==BADZO_MULTIPLIER?source.volume*LotMultiplier:source.volume);
   double volume=NormalizeVolume(desired);
   if(volume<=0){CopyFailure("lot_invalid","Lot ili MaxLot je ispod minimuma brokera.",0,source.ticket);return;}
   MqlTick tick;
   if(!SymbolInfoTick(activeFollowerSymbol,tick) || tick.ask<=0 || tick.bid<=0)
     {CopyFailure("no_price","Broker nije poslao cijenu za "+activeFollowerSymbol,0,source.ticket);return;}
   double point=SymbolInfoDouble(activeFollowerSymbol,SYMBOL_POINT);
   if(point<=0){CopyFailure("symbol_missing","Simbol nema ispravnu preciznost.");return;}
   double spread=(tick.ask-tick.bid)/point;
   if(spread>MaxSpreadPoints)
     {CopyFailure("spread","Spread "+DoubleToString(spread,0)+" prelazi MaxSpreadPoints "+(string)MaxSpreadPoints,0,source.ticket);return;}
   trade.SetTypeFillingBySymbol(activeFollowerSymbol);
   ENUM_ORDER_TYPE type=source.type==POSITION_TYPE_BUY?ORDER_TYPE_BUY:ORDER_TYPE_SELL;
   bool sent=trade.PositionOpen(activeFollowerSymbol,type,volume,type==ORDER_TYPE_BUY?tick.ask:tick.bid,CopySLTP?source.sl:0.0,CopySLTP?source.tp:0.0,CommentFor(source.ticket));
   uint code=trade.ResultRetcode();ulong ticket=0;
   if(sent && (code==TRADE_RETCODE_DONE || code==TRADE_RETCODE_DONE_PARTIAL) && FindFollower(source.ticket,ticket))
     {Print("BADZO COPY CONFIRMED: ",source.ticket," -> ",ticket);return;}
   if(code==TRADE_RETCODE_DONE || code==TRADE_RETCODE_DONE_PARTIAL || code==TRADE_RETCODE_PLACED || code==TRADE_RETCODE_TIMEOUT || code==TRADE_RETCODE_CONNECTION)
     {uncertainOpenTicket=source.ticket;CopyFailure("unconfirmed","Nalog ceka provjeru u MT5: "+trade.ResultRetcodeDescription(),code,source.ticket);return;}
   CopyFailure("broker_rejected",trade.ResultRetcodeDescription(),code,source.ticket);
  }

void CloseFollower(const ulong ticket)
  {
   if(!CopyMasterClose)return;
   trade.SetTypeFillingBySymbol(activeFollowerSymbol);
   bool sent=trade.PositionClose(ticket);uint code=trade.ResultRetcode();
   if(!sent || (code!=TRADE_RETCODE_DONE && code!=TRADE_RETCODE_DONE_PARTIAL))CopyFailure("close_rejected",trade.ResultRetcodeDescription(),code);
  }

void UpdateStops(const ulong followerTicket,const SourcePosition &source)
  {
   if(!CopySLTP || !PositionSelectByTicket(followerTicket))return;
   double oldSL=PositionGetDouble(POSITION_SL),oldTP=PositionGetDouble(POSITION_TP),point=SymbolInfoDouble(activeFollowerSymbol,SYMBOL_POINT);
   if(MathAbs(oldSL-source.sl)<point && MathAbs(oldTP-source.tp)<point)return;
   bool sent=trade.PositionModify(followerTicket,source.sl,source.tp);uint code=trade.ResultRetcode();
   if(!sent || (code!=TRADE_RETCODE_DONE && code!=TRADE_RETCODE_NO_CHANGES))CopyFailure("stops_rejected",trade.ResultRetcodeDescription(),code,source.ticket);
  }

void CopyCycle()
  {
   cycleFailed=false;
   if(!BzExecutionAllowed(activeFollowerSymbol,DemoOnly,true))return;
   SourcePosition sources[];
   if(!ReadState(sources)){BzExecution("source_unavailable",monitorReadError);return;}
   string sourceKey=monitorSourceLogin+"|"+monitorSourceServer;
   if(monitorSourceLogin==(string)AccountInfoInteger(ACCOUNT_LOGIN) && monitorSourceServer==AccountInfoString(ACCOUNT_SERVER))
     {BzExecution("same_account","Master i Follower ne smiju biti isti racun.");return;}
   if(boundSource!="" && boundSource!=sourceKey)
     {BzExecution("master_changed","Promijenjen je master ove grupe. Provjeri stare kopije i StateFile prije ponovnog ucitavanja EA-a.");return;}
   boundSource=sourceKey;
   if(!baselineDone)
     {
      baselineDone=true;ArrayResize(skippedTickets,0);
      if(!CopyExistingPositionsOnStart)
        {
         for(int i=0;i<ArraySize(sources);i++)
           {ulong existing=0;if(!FindFollower(sources[i].ticket,existing)){int n=ArraySize(skippedTickets);ArrayResize(skippedTickets,n+1);skippedTickets[n]=sources[i].ticket;}}
         Print("BADZO BASELINE SET: ",ArraySize(skippedTickets)," existing positions remain skipped.");
        }
     }
   if(uncertainOpenTicket>0)
     {ulong confirmed=0;if(FindFollower(uncertainOpenTicket,confirmed))uncertainOpenTicket=0;}
   for(int i=0;i<ArraySize(sources);i++)
     {
      ulong followerTicket=0;
      if(FindFollower(sources[i].ticket,followerTicket))UpdateStops(followerTicket,sources[i]);
      else if(!WasSkipped(sources[i].ticket))OpenFollower(sources[i]);
     }
   if(CopyMasterClose)
     for(int i=PositionsTotal()-1;i>=0;i--)
       {
        ulong followerTicket=PositionGetTicket(i);if(followerTicket==0 || !PositionSelectByTicket(followerTicket))continue;
        if(PositionGetString(POSITION_SYMBOL)!=activeFollowerSymbol || PositionGetInteger(POSITION_MAGIC)!=MagicNumber)continue;
        ulong sourceTicket=BzMonitorSourceTicket(PositionGetString(POSITION_COMMENT));if(sourceTicket==0)continue;
        if(!HasSource(sources,sourceTicket))CloseFollower(followerTicket);
       }
   if(!cycleFailed)
     {
      if(uncertainOpenTicket>0)BzExecution("unconfirmed","Provjeri nepotvrdjen nalog u Trade/Experts.",0,uncertainOpenTicket);
      else BzExecution("ready","Copier prati nove master pozicije.");
     }
  }

void OnTimer()
  {
   if(BzObserveAccount())
     {baselineDone=false;ArrayResize(skippedTickets,0);boundSource="";uncertainOpenTicket=0;activeFollowerSymbol=BzGoldSymbol(FollowerSymbol);activeMasterSymbol=MasterSymbol;}
   BzEmergencyTick();BzResumeTick();
   CopyCycle();
   if(BzTelemetryDue(TelemetryIntervalSeconds))SendTelemetry();
   BzEmergencyTick();BzResumeTick();
  }
