2026-10-21 19:25:02 +08:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
2026-10-31 17:54:51 +08:00 | | | string name,server,ccy,mode;
|
2026-10-21 19:25:02 +08:00 | | | 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();
|
2026-10-31 17:54:51 +08:00 | | | a.name=AccountName();
|
2026-10-21 19:25:02 +08:00 | | | 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;
|
2026-10-31 17:54:51 +08:00 | | | bool want_risk;
|
2026-10-21 19:25:02 +08:00 | | | long login;
|
2026-10-31 17:54:51 +08:00 | | | bool record; // tests only: keep every deal that went into the engine
|
| | | VtDeal rec[];
|
| | | int nrec;
|
2026-10-21 19:25:02 +08:00 | | | string m_sig;
|
2026-10-31 17:54:51 +08:00 | | | 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=""; }
|
2026-10-21 19:25:02 +08:00 | | | //--- 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();
|
2026-10-31 17:54:51 +08:00 | | | acct_ccy=AccountCurrency();
|
| | | ndeals=0; nrec=0; last_time=0; last_ticket=0;
|
2026-10-21 19:25:02 +08:00 | | | int nh=OrdersHistoryTotal(),no=OrdersTotal(),n=0;
|
| | | long tk[],par[],ot[],ct[],mg[];
|
| | | int ty[];
|
2026-10-31 17:54:51 +08:00 | | | double lt[],pf[],sw[],cm[],sl[],op[],cp[];
|
2026-10-21 19:25:02 +08:00 | | | 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);
|
2026-10-31 17:54:51 +08:00 | | | 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);
|
2026-10-21 19:25:02 +08:00 | | | 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();
|
2026-10-31 17:54:51 +08:00 | | | sl[n]=OrderStopLoss(); op[n]=OrderOpenPrice(); cp[n]=OrderClosePrice(); sy[n]=OrderSymbol();
|
2026-10-21 19:25:02 +08:00 | | | 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;
|
| | | }
|
2026-10-31 17:54:51 +08:00 | | | //--- tick size and value per symbol (cached for this rebuild)
|
| | | string csym[];
|
| | | double cts[],ctv[];
|
| | | int ncs=0;
|
2026-10-21 19:25:02 +08:00 | | | //--- 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;
|
2026-10-31 17:54:51 +08:00 | | | 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="";
|
2026-10-21 19:25:02 +08:00 | | | 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);
|
2026-10-31 17:54:51 +08:00 | | | d.volume=lt[i]; d.price=(e2==0 ? op[i] : cp[i]);
|
2026-10-21 19:25:02 +08:00 | | | d.profit=(e2==0 ? 0 : pf[i]); d.swap=(e2==0 ? 0 : sw[i]); d.commission=(e2==0 ? 0 : cm[i]); d.fee=0;
|
2026-10-31 17:54:51 +08:00 | | | d.sl=(e2==0 ? sl[i] : 0); d.risk=0;
|
2026-10-21 19:25:02 +08:00 | | | d.symbol=sy[i]; d.comment=(e2==0 ? VtCleanComment(cmt[i]) : "");
|
2026-10-31 17:54:51 +08:00 | | | 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]));
|
| | | }
|
| | | }
|
2026-10-21 19:25:02 +08:00 | | | 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]];
|
2026-10-31 17:54:51 +08:00 | | | if(record)
|
| | | { ArrayResize(rec,nrec+1,4096); rec[nrec++]=d; }
|
2026-10-21 19:25:02 +08:00 | | | 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
|
| | | //+------------------------------------------------------------------+
|