| //+------------------------------------------------------------------+
|
| //| VtLive.mqh |
|
| //| Copyright 2026, Allan Niñal |
|
| //| https://www.allanninal.dev/mql5 |
|
| //+------------------------------------------------------------------+
|
| //| MT4 edition of the terminal side of Vitals: account figures, |
|
| //| open positions (risk at the stops, exposure value) and the order |
|
| //| history turned into the engine's deal list. Read-only: no order |
|
| //| function is ever called. |
|
| //| MT4 keeps ORDERS, not deals, so every order becomes an IN deal at its open time and, once |
|
| //| closed, an OUT deal at its close time carrying profit, swap |
|
| //| and commission. A partial close leaves an order for the closed |
|
| //| part and opens a new one commented "from #<ticket>": the chain |
|
| //| is ONE position (the engine's trade), keyed by the first ticket.|
|
| //| Balance records (type 6) are cash flow, credit (7) is credit. |
|
| //| There is no fee in MT4. Prices come from MarketInfo (the |
|
| //| SymbolInfo bid/ask is 0 for some symbols in the tester); |
|
| //| there is no OrderCalcProfit: risk at a stop = distance / tick |
|
| //| size x tick value x lots, with the documented fallback when the|
|
| //| tester gives MODE_TICKVALUE 0 (gold, crypto). |
|
| //+------------------------------------------------------------------+
|
| // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| #ifndef VTLIVE_MQH
|
| #define VTLIVE_MQH
|
| #include "VtCore.mqh"
|
| #include "VtExposure.mqh"
|
|
|
| #define VT_SO_PERCENT 0 // AccountStopoutMode(): 0 = percent, 1 = money
|
|
|
| //--- the server clock: the last tick's time (MQL4 has no TimeTradeServer); in the tester the simulated time
|
| long VtServerNow() { return((long)TimeCurrent()); }
|
| //--- microsecond counter stand-in (MQL4 has no GetMicrosecondCount): milliseconds x 1000, good enough for the perf log
|
| ulong VtMicros() { return((ulong)GetTickCount()*1000); }
|
|
|
| struct VtAcct
|
| {
|
| long login,leverage;
|
| string name,server,ccy,mode;
|
| double balance,equity,margin,free,level,so,profit,credit;
|
| long so_mode; // VT_SO_PERCENT / 1 = money
|
| };
|
|
|
| void VtReadAccount(VtAcct &a)
|
| {
|
| a.login=AccountNumber();
|
| a.leverage=AccountLeverage();
|
| a.name=AccountName();
|
| a.server=AccountServer();
|
| a.ccy=AccountCurrency();
|
| a.mode="hedging";
|
| a.balance=AccountBalance();
|
| a.equity=AccountEquity();
|
| a.margin=AccountMargin();
|
| a.free=AccountFreeMargin();
|
| a.level=(a.margin>0 ? 100.0*a.equity/a.margin : 0.0);
|
| a.so=AccountStopoutLevel();
|
| a.so_mode=AccountStopoutMode();
|
| a.profit=AccountProfit();
|
| a.credit=AccountCredit();
|
| }
|
|
|
| //--- rate profit currency -> account currency from a cross symbol in the terminal (suffix of the position's symbol
|
| //--- tried first); returns 0 when there is none
|
| double VtCrossRate(const string ccy,const string acct,const string suffix)
|
| {
|
| if(ccy==acct)
|
| return(1.0);
|
| string a=ccy+acct+suffix,b=acct+ccy+suffix,c=ccy+acct,d=acct+ccy;
|
| string cand[4];
|
| cand[0]=a; cand[1]=b; cand[2]=c; cand[3]=d;
|
| for(int i=0; i<4; i++)
|
| {
|
| ResetLastError();
|
| double bid=MarketInfo(cand[i],MODE_BID);
|
| if(bid<=0 || GetLastError()!=0)
|
| continue;
|
| return(i%2==0 ? bid : 1.0/bid);
|
| }
|
| return(0.0);
|
| }
|
|
|
| //--- account money of one tick of one lot: MODE_TICKVALUE; when the terminal gives 0 (tester: gold, crypto),
|
| //--- tick size x contract size x the profit currency's rate to the account currency
|
| double VtTickValue(const string sym,const string acct_ccy,double &tick_size)
|
| {
|
| tick_size=MarketInfo(sym,MODE_TICKSIZE);
|
| double tv=MarketInfo(sym,MODE_TICKVALUE);
|
| if(tv>0)
|
| return(tv);
|
| double contract=MarketInfo(sym,MODE_LOTSIZE);
|
| string ccy=SymbolInfoString(sym,SYMBOL_CURRENCY_PROFIT);
|
| if(tick_size<=0 || contract<=0 || ccy=="")
|
| return(0.0);
|
| string suffix=(StringLen(sym)>6 ? StringSubstr(sym,6) : "");
|
| double rate=VtCrossRate(ccy,acct_ccy,suffix);
|
| return(rate>0 ? tick_size*contract*rate : 0.0);
|
| }
|
|
|
| //--- loss (<= 0) of `lots` from `open` to `stop`, in account money; 0 when the symbol gives no tick value
|
| double VtRiskMoney(const string sym,const int dir,const double lots,const double open,const double stop,const string acct_ccy)
|
| {
|
| double ts;
|
| double tv=VtTickValue(sym,acct_ccy,ts);
|
| if(tv<=0 || ts<=0)
|
| return(0.0);
|
| double dist=(dir==0 ? stop-open : open-stop);
|
| return(dist/ts*tv*lots);
|
| }
|
|
|
| //--- the open positions as engine input
|
| int VtReadPositions(VtPos &out[],const string acct_ccy)
|
| {
|
| int total=OrdersTotal(),k=0;
|
| ArrayResize(out,total);
|
| for(int i=0; i<total; i++)
|
| {
|
| if(!OrderSelect(i,SELECT_BY_POS,MODE_TRADES) || OrderType()>OP_SELL)
|
| continue;
|
| string sym=OrderSymbol();
|
| int type=OrderType();
|
| double vol=OrderLots(),po=OrderOpenPrice(),sl=OrderStopLoss();
|
| double pc=OrderClosePrice(); // the current price of an open order
|
| out[k].symbol=sym;
|
| out[k].magic=OrderMagicNumber();
|
| out[k].comment=OrderComment();
|
| out[k].dir=(type==OP_BUY ? 0 : 1);
|
| out[k].volume=vol;
|
| out[k].profit=OrderProfit()+OrderSwap();
|
| out[k].sl=sl;
|
| int digits=(int)MarketInfo(sym,MODE_DIGITS);
|
| double point=MarketInfo(sym,MODE_POINT);
|
| int mult=VtPipMult(sym,digits);
|
| double pips=0;
|
| if(point>0)
|
| pips=(pc-po)/point/mult;
|
| out[k].pips=(type==OP_BUY ? pips : -pips);
|
| out[k].risk=0;
|
| if(sl>0)
|
| out[k].risk=VtRiskMoney(sym,out[k].dir,vol,po,sl,acct_ccy);
|
| out[k].base=SymbolInfoString(sym,SYMBOL_CURRENCY_BASE);
|
| out[k].ccy=SymbolInfoString(sym,SYMBOL_CURRENCY_PROFIT);
|
| string suffix=(StringLen(sym)>6 ? StringSubstr(sym,6) : "");
|
| double rate=VtCrossRate(out[k].ccy,acct_ccy,suffix);
|
| double contract=MarketInfo(sym,MODE_LOTSIZE);
|
| if(rate<=0) // no cross symbol: the terminal's own conversion
|
| {
|
| double ts;
|
| double tv=VtTickValue(sym,acct_ccy,ts);
|
| rate=(contract>0 && ts>0 ? tv/ts/contract : 0.0);
|
| }
|
| out[k].value=vol*contract*pc*rate;
|
| if(out[k].base=="")
|
| out[k].base=sym;
|
| if(out[k].ccy=="")
|
| out[k].ccy=sym;
|
| k++;
|
| }
|
| ArrayResize(out,k);
|
| return(k);
|
| }
|
|
|
| //--- "from #123" = the rest of a partially closed order; the chain's first ticket is the position
|
| long VtFromTicket(const string c)
|
| {
|
| if(StringFind(c,"from #")!=0)
|
| return(0);
|
| return(StringToInteger(StringSubstr(c,6)));
|
| }
|
|
|
| //--- the comment an EA gave the order: the server's own notes are not part of it
|
| string VtCleanComment(string c)
|
| {
|
| if(StringFind(c,"from #")==0 || StringFind(c,"to #")==0 || StringFind(c,"partial close")==0 || StringFind(c,"close hedge")==0)
|
| return("");
|
| int k=StringFind(c,"["); // "[sl]", "[tp]", "[so]" added when the server closed it
|
| if(k==0)
|
| return(""); // (StringSubstr with length 0 would return the WHOLE string)
|
| if(k>0)
|
| c=StringSubstr(c,0,k);
|
| StringTrimRight(c);
|
| return(c);
|
| }
|
|
|
| //--- the order history, rebuilt into the engine whenever it changes
|
| class CVtHistory
|
| {
|
| public:
|
| long last_time;
|
| ulong last_ticket;
|
| int ndeals;
|
| bool want_risk;
|
| long login;
|
| bool record; // tests only: keep every deal that went into the engine
|
| VtDeal rec[];
|
| int nrec;
|
| string m_sig;
|
| string acct_ccy;
|
| CVtHistory(): last_time(0),last_ticket(0),ndeals(0),want_risk(false),login(0),record(false),nrec(0),m_sig("") {}
|
| void Forget() { last_time=0; last_ticket=0; ndeals=0; m_sig=""; }
|
| //--- what changes when the history or the open orders change
|
| string Signature()
|
| {
|
| int nh=OrdersHistoryTotal(),no=OrdersTotal();
|
| double st=0,sl=0;
|
| for(int i=0; i<no; i++)
|
| if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES))
|
| { st+=OrderTicket(); sl+=OrderLots(); }
|
| long lt=0;
|
| if(nh>0 && OrderSelect(nh-1,SELECT_BY_POS,MODE_HISTORY))
|
| lt=OrderTicket();
|
| return(StringFormat("%d|%d|%.0f|%.2f|%I64d|%I64d",nh,no,st,sl,lt,AccountNumber()));
|
| }
|
| //--- the orders as deals, in time order (IN before OUT at the same second), into the engine
|
| bool Rebuild(CVtEngine &e)
|
| {
|
| e.Reset();
|
| acct_ccy=AccountCurrency();
|
| ndeals=0; nrec=0; last_time=0; last_ticket=0;
|
| int nh=OrdersHistoryTotal(),no=OrdersTotal(),n=0;
|
| long tk[],par[],ot[],ct[],mg[];
|
| int ty[];
|
| double lt[],pf[],sw[],cm[],sl[],op[],cp[];
|
| string cmt[],sy[];
|
| int cap=nh+no;
|
| ArrayResize(tk,cap); ArrayResize(par,cap); ArrayResize(ot,cap); ArrayResize(ct,cap); ArrayResize(mg,cap); ArrayResize(ty,cap);
|
| ArrayResize(lt,cap); ArrayResize(pf,cap); ArrayResize(sw,cap); ArrayResize(cm,cap); ArrayResize(sl,cap);
|
| ArrayResize(op,cap); ArrayResize(cp,cap); ArrayResize(cmt,cap); ArrayResize(sy,cap);
|
| for(int pass=0; pass<2; pass++)
|
| for(int i=0; i<(pass==0 ? nh : no); i++)
|
| {
|
| if(!OrderSelect(i,SELECT_BY_POS,pass==0 ? MODE_HISTORY : MODE_TRADES))
|
| continue;
|
| int t=OrderType();
|
| if(t>OP_SELL && t!=6 && t!=7)
|
| continue; // pending orders: not trades
|
| tk[n]=OrderTicket(); ty[n]=t; cmt[n]=OrderComment(); par[n]=VtFromTicket(cmt[n]);
|
| ot[n]=(long)OrderOpenTime(); ct[n]=(pass==0 ? (long)OrderCloseTime() : 0); mg[n]=OrderMagicNumber();
|
| lt[n]=OrderLots(); pf[n]=OrderProfit(); sw[n]=OrderSwap(); cm[n]=OrderCommission();
|
| sl[n]=OrderStopLoss(); op[n]=OrderOpenPrice(); cp[n]=OrderClosePrice(); sy[n]=OrderSymbol();
|
| n++;
|
| }
|
| //--- position of each order: follow "from #" to the first ticket (tickets sorted for the lookup)
|
| double idx[][2];
|
| ArrayResize(idx,n);
|
| for(int i=0; i<n; i++)
|
| { idx[i][0]=(double)tk[i]; idx[i][1]=i; }
|
| if(n>1)
|
| ArraySort(idx);
|
| long root[];
|
| ArrayResize(root,n);
|
| for(int i=0; i<n; i++)
|
| {
|
| long r=tk[i],p=par[i];
|
| for(int guard=0; p!=0 && guard<10000; guard++)
|
| {
|
| r=p;
|
| p=0;
|
| int lo=0,hi=n-1; // binary search for ticket r
|
| while(lo<=hi)
|
| {
|
| int mid=(lo+hi)/2;
|
| if(idx[mid][0]<(double)r)
|
| lo=mid+1;
|
| else
|
| if(idx[mid][0]>(double)r)
|
| hi=mid-1;
|
| else
|
| { p=par[(int)idx[mid][1]]; break; }
|
| }
|
| }
|
| root[i]=r;
|
| }
|
| //--- tick size and value per symbol (cached for this rebuild)
|
| string csym[];
|
| double cts[],ctv[];
|
| int ncs=0;
|
| //--- the deals; phase 0 cash, 1 opening, 2 closing; sorted by time, phase, reading order
|
| int nd=0;
|
| VtDeal dl[];
|
| double key[][2];
|
| ArrayResize(dl,2*n); ArrayResize(key,2*n);
|
| for(int i=0; i<n; i++)
|
| {
|
| if(ty[i]==6 || ty[i]==7)
|
| {
|
| VtDeal d;
|
| d.time=ct[i]>0 ? ct[i] : ot[i]; d.pos=tk[i]; d.magic=0; d.type=(ty[i]==6 ? VT_T_CASH : VT_T_CREDIT); d.entry=VT_IN;
|
| d.volume=0; d.price=0; d.profit=pf[i]; d.swap=0; d.commission=0; d.fee=0; d.sl=0; d.risk=0; d.symbol=""; d.comment="";
|
| dl[nd]=d;
|
| key[nd][0]=((double)d.time*3.0+0)*1048576.0+nd; key[nd][1]=nd; nd++;
|
| continue;
|
| }
|
| for(int e2=0; e2<2; e2++)
|
| {
|
| if(e2==1 && ct[i]==0)
|
| continue;
|
| VtDeal d;
|
| d.time=(e2==0 ? ot[i] : ct[i]); d.pos=root[i]; d.magic=mg[i];
|
| d.type=(e2==0 ? (ty[i]==OP_BUY ? VT_T_BUY : VT_T_SELL) : (ty[i]==OP_BUY ? VT_T_SELL : VT_T_BUY));
|
| d.entry=(e2==0 ? VT_IN : VT_OUT);
|
| d.volume=lt[i]; d.price=(e2==0 ? op[i] : cp[i]);
|
| d.profit=(e2==0 ? 0 : pf[i]); d.swap=(e2==0 ? 0 : sw[i]); d.commission=(e2==0 ? 0 : cm[i]); d.fee=0;
|
| d.sl=(e2==0 ? sl[i] : 0); d.risk=0;
|
| d.symbol=sy[i]; d.comment=(e2==0 ? VtCleanComment(cmt[i]) : "");
|
| if(want_risk && e2==0 && sl[i]>0)
|
| {
|
| int q=-1;
|
| for(int j=0; j<ncs; j++)
|
| if(csym[j]==sy[i])
|
| { q=j; break; }
|
| if(q<0)
|
| {
|
| q=ncs++;
|
| ArrayResize(csym,ncs); ArrayResize(cts,ncs); ArrayResize(ctv,ncs);
|
| csym[q]=sy[i]; ctv[q]=VtTickValue(sy[i],acct_ccy,cts[q]);
|
| }
|
| if(ctv[q]>0 && cts[q]>0)
|
| {
|
| double dist=(ty[i]==OP_BUY ? sl[i]-op[i] : op[i]-sl[i]);
|
| d.risk=MathMax(0.0,-(dist/cts[q]*ctv[q]*lt[i]));
|
| }
|
| }
|
| dl[nd]=d;
|
| key[nd][0]=((double)d.time*3.0+(e2==0 ? 1 : 2))*1048576.0+nd; key[nd][1]=nd; nd++;
|
| }
|
| }
|
| ArrayResize(key,nd);
|
| if(nd>1)
|
| ArraySort(key);
|
| for(int i=0; i<nd; i++)
|
| {
|
| VtDeal d=dl[(int)key[i][1]];
|
| if(record)
|
| { ArrayResize(rec,nrec+1,4096); rec[nrec++]=d; }
|
| e.Add(d);
|
| last_time=d.time;
|
| ndeals++;
|
| }
|
| last_ticket=(ndeals>0 ? 1 : 0);
|
| m_sig=Signature();
|
| return(true);
|
| }
|
| //--- true when the engine changed (a new order, a close, a partial close, another account)
|
| bool Refresh(CVtEngine &e,bool &rebuilt)
|
| {
|
| rebuilt=false;
|
| long lg=AccountNumber();
|
| string sg=Signature();
|
| if(sg==m_sig && lg==login)
|
| return(false);
|
| login=lg;
|
| rebuilt=true;
|
| Rebuild(e);
|
| return(true);
|
| }
|
| };
|
|
|
| #endif
|
| //+------------------------------------------------------------------+
|