preview
The ZeroMQ Message Transfer Protocol in MQL5: Implementing the REQ/REP pattern

The ZeroMQ Message Transfer Protocol in MQL5: Implementing the REQ/REP pattern

MetaTrader 5Examples |
135 0
Francis Dube
Francis Dube

Contents

  1. Introduction
  2. Protocol Foundations and ZMTP Mechanics
  3. The REQ Pattern Abstraction
  4. Practical Application Architecture: The Markov-switching GARCH Pipeline
  5. Conclusion and Future Extensions


Introduction

Algorithmic traders using MetaTrader 5 often need to execute complex quantitative calculations. MQL5 sometimes lacks native implementations for advanced computations that are readily available in Python and R. This motivates bridging solutions that let MetaTrader 5 work with these external programs. One example of such a solution is message queuing libraries like ZeroMQ. Historically, connecting MetaTrader 5 to external environments via ZeroMQ required using external Dynamic Link Libraries (DLLs). DLLs introduce platform dependencies and deployment complexities. While modern MQL5 includes raw network sockets, they are mostly suited for connecting clients to remote hosts, which should then afford MetaTrader 5 programs the means to communicate directly with ZeroMQ servers without third-party binaries.

This article explores a native MQL5 implementation of the ZeroMQ Message Transfer Protocol (ZMTP), built on top of raw MQL5 sockets. Readers will learn how to send requests and receive responses from a ZeroMQ server using a standard request-reply messaging sequence implemented as the CZmqReqSocket class. As a demonstration, we present a complete end-to-end quantitative pipeline where an MQL5 script streams financial return series directly to a hybrid Python/R server running MS-GARCH estimation and receives real-time market regime probabilities in return.

To discard DLL dependencies and achieve native integration, we must inspect the mechanics of the ZeroMQ protocol implementation before diving into the details of MQL5 ZeroMQ clients, the nature of the server, and how they all work together.


Protocol Foundations and ZMTP Mechanics

For MQL5 sockets to communicate with ZeroMQ servers, they must follow specific rules. These rules cover connection initiation, message structure, and other requirements. Together, they form the ZeroMQ Message Transfer Protocol (ZMTP). The core utilities in Zmtp.mqh map directly to the ZMTP specification. On top of these utilities, the library builds higher-level abstractions that handle patterns of communication between programs. In this text, we focus on the request-reply pattern. This pattern is easy to understand because it mimics a standard client-server relationship: the client sends a request, and the server returns a reply. Here, our abstractions focus on the client side of that interaction, implemented as the CZmqReqSocket class in Zmtp.mqh.

The code in Zmtp.mqh is organized into three main components.

  • ZMTP protocol constants and frame structures.
  • The underlying transport manager (CZmtpTransport).
  • The high-level socket pattern abstraction (CZmqReqSocket).

Library Structure

ZMTP protocol constants and frame structures

The library establishes two main enumerations: ENUM_ZMQ_SOCKET_TYPE, which lists the supported ZeroMQ socket patterns (note that not all have been implemented); and ENUM_ZMTP_SECURITY, which specifies the available security mechanisms, including unauthenticated NULL, cleartext PLAIN, and an unimplemented CURVE option.

//+------------------------------------------------------------------+
//| Socket-Type property this library presents to the ZMTP peer      |
//+------------------------------------------------------------------+
enum ENUM_ZMQ_SOCKET_TYPE
  {
   ZMQ_TYPE_REQ,
   ZMQ_TYPE_REP,
   ZMQ_TYPE_DEALER,
   ZMQ_TYPE_ROUTER,
   ZMQ_TYPE_PUB,
   ZMQ_TYPE_SUB,
   ZMQ_TYPE_PUSH,
   ZMQ_TYPE_PULL,
   ZMQ_TYPE_PAIR
  };

enum ENUM_ZMTP_SECURITY
  {
   ZMTP_SEC_NULL,    // no authentication, no encryption
   ZMTP_SEC_PLAIN,   // username/password, sent in clear text
   ZMTP_SEC_CURVE    // NOT IMPLEMENTED - see note at bottom of file
  };

The helper function ZmqSocketTypeName maps each socket-type enum value to its standard ZeroMQ string. The string is used during the handshake to identify the endpoint type to the peer.

string ZmqSocketTypeName(ENUM_ZMQ_SOCKET_TYPE t)
  {
   switch(t)
     {
      case ZMQ_TYPE_REQ:    return "REQ";
      case ZMQ_TYPE_REP:    return "REP";
      case ZMQ_TYPE_DEALER: return "DEALER";
      case ZMQ_TYPE_ROUTER: return "ROUTER";
      case ZMQ_TYPE_PUB:    return "PUB";
      case ZMQ_TYPE_SUB:    return "SUB";
      case ZMQ_TYPE_PUSH:   return "PUSH";
      case ZMQ_TYPE_PULL:   return "PULL";
      case ZMQ_TYPE_PAIR:   return "PAIR";
     }
   return "";
  }

The ZmtpFrame structure represents an individual message frame transmitted over or received from a ZMTP network connection. The structure uses boolean flags to track protocol states: the more flag indicates whether additional frames follow in a multi-part message, while the command flag differentiates control frames (used for protocol-level operations like greetings or metadata exchange) from standard application payload data. Finally, the frame's raw content is stored as a dynamic byte array, allowing it to accommodate variable-length payloads.

//+------------------------------------------------------------------+
//| One ZMTP frame as read off / written to a node                   |
//+------------------------------------------------------------------+
struct ZmtpFrame
  {
   bool  more;      // MORE flag - another frame follows in this message
   bool  command;   // COMMAND flag - control frame (handshake), not user data
   uchar data[];
  };

The underlying transport manager (CZmtpTransport)

The CZmtpTransport class is the low-level transport manager for native ZMTP communication in MQL5. It sits on top of raw TCP sockets and handles connection lifecycle, receive buffering, session negotiation, and frame serialization. It acts as the foundational transport interface upon which higher-level messaging patterns are constructed.

//+------------------------------------------------------------------+
//| CZmtpTransport                                                   |
//| Low-level ZMTP 3.0 connection: raw socket, receive buffering,    |
//| greeting exchange, security handshake, frame read/write.         |
//| Every socket-pattern class (REQ, and later DEALER/PUB/SUB/etc.)  |
//| is built on top of one of these.                                 |
//+------------------------------------------------------------------+
class CZmtpTransport
  {
private:
   int                m_socket;
   uchar              m_rxbuf[];
   int                m_rxlen;       // valid bytes currently in m_rxbuf
   int                m_rxpos;       // read cursor into m_rxbuf
   int                m_timeout_ms;
   ENUM_ZMTP_SECURITY m_security;
   string             m_username;
   string             m_password;

   bool   EnsureBytes(int n);
   void   PopBytes(int n, uchar &out[]);
   bool   RawSend(const uchar &buf[], int len);

   bool   SendGreeting();
   bool   RecvGreeting(string &peer_mechanism);
   bool   DoPlainHandshake();
   bool   SendReady(ENUM_ZMQ_SOCKET_TYPE stype, string identity);
   bool   RecvReady();

public:
          CZmtpTransport();
         ~CZmtpTransport();

   bool   Connect(string host, int port, ENUM_ZMTP_SECURITY sec, int timeout_ms=5000);
   void   Disconnect();
   bool   IsConnected();

   void   SetPlainCredentials(string user, string pass) { m_username=user; m_password=pass; }

   // Full ZMTP handshake: greeting -> (security mechanism) -> READY/READY.
   // Call once, immediately after Connect().
   bool   Handshake(ENUM_ZMQ_SOCKET_TYPE stype, string identity="");

   bool   SendFrame(const uchar &data[], bool more, bool command=false);
   bool   SendCommand(string name, const uchar &body[]);
   bool   RecvFrame(ZmtpFrame &frame);
  };

A ZMTP session over a raw TCP stream starts with a 64-byte greeting sequence exchanged by both peers right after connecting. This sequence identifies the protocol version, security mechanism, and peer type. Bytes 0 to 9 contain a fixed signature starting with 0xFF, followed by eight zero bytes and ending with 0x7F. The bytes 10 to 11 specify the ZMTP major and minor versions. Thereafter, the bytes 12 to 31 hold a 20-byte security mechanism identifier (like "NULL" or "PLAIN"), padded with null bytes (0x00). Byte 32 sets the peer type (0 for client, 1 for server), and bytes 33 to 63 are reserved zero bytes.


Greeting Sequence


This sequence gives both endpoints the details they need to establish the session before sending normal messages. The SendGreeting() and RecvGreeting() methods construct and validate this packet.

bool CZmtpTransport::SendGreeting()
  {
   uchar g[];
   ArrayResize(g, 64);
   ArrayFill(g, 0, 64, 0);
   g[0] = 0xFF;                 // signature start
   g[9] = 0x7F;                 // signature end
   g[10] = 3;                   // version-major = 3 (ZMTP 3.x)
   g[11] = 0;                   // version-minor

   string mech = "NULL";
   if(m_security == ZMTP_SEC_PLAIN) mech = "PLAIN";
   else if(m_security == ZMTP_SEC_CURVE) mech = "CURVE";
   uchar mech_bytes[];
   StringToCharArray(mech, mech_bytes, 0, StringLen(mech));
   for(int i=0; i<ArraySize(mech_bytes) && i<20; i++)
      g[12+i] = mech_bytes[i];

   g[32] = 0;                   // as-server: this library only ever connects as a client
   // g[33..63] filler, already zero
   return RawSend(g, 64);
  }

bool CZmtpTransport::RecvGreeting(string &peer_mechanism)
  {
   if(!EnsureBytes(64)) return false;
   uchar g[];
   PopBytes(64, g);
   if(g[0] != 0xFF || g[9] != 0x7F)
     {
      Print(__FUNCTION__," : Zmtp: peer sent an invalid greeting signature - is this really a ZMTP 3.x endpoint?");
      return false;
     }
   int major = g[10];
   if(major < 3)
     {
      Print(__FUNCTION__," : Zmtp: peer speaks ZMTP ", major, ".x - this library only supports ZMTP 3.x");
      return false;
     }
   uchar mech_bytes[20];
   for(int i=0; i<20; i++)
      mech_bytes[i] = g[12+i];
   int mlen = 0;
   while(mlen < 20 && mech_bytes[mlen] != 0)
      mlen++;
   peer_mechanism = CharArrayToString(mech_bytes, 0, mlen);
   return true;
  }

After validating greetings, peers finish security handshakes and exchange metadata using ZMTP control commands. A command is encoded as a frame with the command flag bit (0x04) set. The payload contains key-value properties, such as the type of socket and an optional identifier. ZMTP specifies three security modes.

  • No Authentication: Peers trust each other implicitly.
  • PLAIN: Usernames and passwords are sent in plain text across the network.
  • Encrypted: Uses encryption to secure traffic over public networks.

Neither mode 1 nor mode 2 is safe for public networks. The demonstration later in this text uses no authentication, as encryption support is not yet implemented.
Standard TCP is stream-oriented, so data can arrive in arbitrary chunks. The CZmtpTransport class handles buffer management internally using two methods. The EnsureBytes() method reads data from the socket via SocketIsReadable() and appends incoming bytes to an internal dynamic array (m_rxbuf[]) until at least `n` bytes are ready.

bool CZmtpTransport::EnsureBytes(int n)
  {
   uint start = GetTickCount();
   while((m_rxlen - m_rxpos) < n)
     {
      if(!SocketIsConnected(m_socket))
        {
         Print(__FUNCTION__," : Zmtp: connection dropped mid-receive");
         return false;
        }
      uint avail = SocketIsReadable(m_socket);
      if(avail > 0)
        {
         uchar tmp[];
         ArrayResize(tmp, avail);
         int got = SocketRead(m_socket, tmp, avail, 100);
         if(got > 0)
           {
            int need = m_rxlen + got;
            if(need > ArraySize(m_rxbuf))
               ArrayResize(m_rxbuf, need);
            for(int i=0; i<got; i++)
               m_rxbuf[m_rxlen+i] = tmp[i];
            m_rxlen += got;
            start = GetTickCount(); // reset timeout on forward progress
           }
        }
      else
         Sleep(1);
     }
   return true;
  }

The PopBytes() method extracts `n` bytes from m_rxbuf[] for parsing and compacts the buffer periodically to save memory.

void CZmtpTransport::PopBytes(int n, uchar &out[])
  {
   ArrayResize(out, n);
   for(int i=0; i<n; i++)
      out[i] = m_rxbuf[m_rxpos+i];
   m_rxpos += n;
   // compact the buffer once we've drained a meaningful chunk of it
   if(m_rxpos > 4096)
     {
      int remain = m_rxlen - m_rxpos;
      for(int i=0; i<remain; i++)
         m_rxbuf[i] = m_rxbuf[m_rxpos+i];
      m_rxlen = remain;
      m_rxpos = 0;
     }
  }


The REQ Pattern Abstraction

The CZmqReqSocket class provides a high-level API built on CZmtpTransport. The ZeroMQ API works based on messages. Thereby enforcing the use of messaging patterns. In ZeroMQ, a request (REQ) socket requires two things: strict alternation and envelope delimitation.

  • Strict alternation is when the socket must alternate between send and receive. Sending two requests in a row causes a state error.
  • Envelope delimitation describes how ZeroMQ routes messages using envelopes. On every request, a REQ socket automatically prepends an empty delimiter frame (len = 0, MORE = true) before the payload frame.
//+------------------------------------------------------------------+
//| CZmqReqSocket - ZMQ REQ pattern over ZMTP.                       |
//|                                                                  |
//| Enforces the strict send/recv/send/recv alternation that REQ     |
//| requires, and handles the empty delimiter-frame envelope that    |
//| REQ/REP sockets use automatically in real ZeroMQ.                |
//+------------------------------------------------------------------+
   class CZmqReqSocket
  {
private:
   CZmtpTransport    m_t;
   bool              m_awaiting_reply;

public:
                     CZmqReqSocket() { m_awaiting_reply = false; }

   bool              Connect(string host, int port, ENUM_ZMTP_SECURITY sec=ZMTP_SEC_NULL,
                             int timeout_ms=5000, string identity="", string plain_user="", string plain_pass="");

   void              Disconnect()   { m_t.Disconnect(); }
   bool              IsConnected()  { return m_t.IsConnected(); }

   // Single-part string request.
   bool              Send(const string &request);
   // Single-part binary request (e.g. a packed struct of doubles).
   bool              SendRaw(const uchar &request[]);

   // Blocks for the matching reply (up to the connect-time timeout).
   bool              Recv(string &reply);
   bool              RecvRaw(uchar &reply[]);

  };

The CZmqReqSocket class exposes request/reply semantics directly over ZMTP. An internal variable, m_awaiting_reply, tracks state to prevent sending two requests back-to-back or receiving without sending first. The Connect() method sets up the TCP connection and completes the ZMTP handshake. If using PLAIN security, SetPlainCredentials() passes credentials before connecting.

bool Connect(string host, int port, ENUM_ZMTP_SECURITY sec=ZMTP_SEC_NULL,
             int timeout_ms=5000, string identity="", string plain_user="", string plain_pass="")
  {
   if(sec == ZMTP_SEC_PLAIN)
      m_t.SetPlainCredentials(plain_user, plain_pass);
   if(!m_t.Connect(host, port, sec, timeout_ms))
      return false;
   return m_t.Handshake(ZMQ_TYPE_REQ, identity);
  }

The library then calls the handshake routine to exchange socket metadata, setting ZMQ_TYPE_REQ so the peer understands the expected messaging pattern. The Disconnect() and IsConnected() methods simply expose the underlying transport status. The Send() method takes strings, converts them into byte arrays, and calls SendRaw().

bool Send(const string &request)
  {
   uchar body[];
   StringToCharArray(request, body, 0, StringLen(request));
   return SendRaw(body);
  }

In SendRaw(), the method builds the REQ envelope by sending two ZMTP frames.

bool SendRaw(const uchar &request[])
  {
   if(m_awaiting_reply)
     {
      Print(__FUNCTION__," : Zmq REQ: Send() called out of order - a reply is still pending");
      return false;
     }
   uchar empty[];
   ArrayResize(empty, 0);
// REQ envelope: empty delimiter frame, then the request body
   if(!m_t.SendFrame(empty, true))
      return false;
   if(!m_t.SendFrame(request, false))
      return false;
   m_awaiting_reply = true;
   return true;
  }

[empty delimiter frame, MORE=1] → [request body frame, MORE=0]

First, a zero-length frame, marked to show another frame is coming, is sent. Then, the request body with `MORE` turned off is sent to finish the message. This delimiter handles internal routing, so the main application code only deals with the actual request payload. The Recv() and RecvRaw() methods handle the reply phase. The RecvRaw() routine checks m_awaiting_reply to ensure a request was sent, then calls RecvFrame() for the first frame. If communicating through an intermediary, extra routing frames may arrive before the empty delimiter. The code discards these extra frames using a while loop until it hits the empty delimiter.

bool Recv(string &reply)
  {
   uchar raw[];
   if(!RecvRaw(raw))
      return false;
   reply = CharArrayToString(raw, 0, ArraySize(raw));
   return true;
  }

bool RecvRaw(uchar &reply[])
  {
   if(!m_awaiting_reply)
     {
      Print(__FUNCTION__," : Zmq REQ: Recv() called before Send()");
      return false;
     }
   ZmtpFrame f;
   if(!m_t.RecvFrame(f))
      return false;
// If a ROUTER sits between us and the REP, extra identity/envelope
// frames may precede the empty delimiter - skip anything non-empty.
   while(ArraySize(f.data) > 0 && f.more)
     {
      if(!m_t.RecvFrame(f))
         return false;
     }
   ArrayResize(reply, 0);
   while(f.more)
     {
      if(!m_t.RecvFrame(f))
         return false;
      int old = ArraySize(reply);
      int add = ArraySize(f.data);
      ArrayResize(reply, old+add);
      ArrayCopy(reply, f.data, old, 0, add);
     }
   m_awaiting_reply = false;
   return true;
  }

Once past the delimiter, the socket gathers response frames. Because a reply can span multiple frames, the code reads incoming frames as long as `f.more` is set and appends their bytes to `reply`. Once a frame arrives with `MORE=0`, the message is complete. The Recv() method converts the byte array back to an MQL5 string, resets `m_awaiting_reply` to `false`, and prepares the socket for the next request. This workflow underlines the role of the protocol and the messaging pattern defined on top of it. ZMTP handles wire-level details—connections, handshakes, frames, and flags—while the REQ socket enforces the higher-level request/reply procedures. In the next section, we will look at a practical demonstration of the CZmqReqSocket class.


Practical Application Architecture: The Markov-switching GARCH Pipeline

Financial return series frequently display dynamic variance patterns across different market regimes (for example, low-volatility trending markets versus high-volatility stress periods). Standard GARCH models assume static parameters over time, whereas Markov-switching GARCH (MS-GARCH) models allow parameters to transition dynamically across hidden Markov states.

If we wanted to apply such a model, we would have to implement MS-GARCH models in MQL5. But why should we when robust implementations already exist on other platforms? The most prominent being the R package MSGARCH. We can leverage this library in our own workflow with the help of ZeroMQ. We therefore present an end-to-end quantitative pipeline. MetaTrader 5 streams return data to an external ZeroMQ server running in Python, which invokes R's `MSGARCH` package, fits the model, and returns regime probability estimates. The pipeline consists of a Python server and an MQL5 script. The routines that constitute the MQL5 script can also work within an EA.

Execution Flow

The Python Server Architecture

The `msgarch_server.py` application is a Python service that bridges ZeroMQ network requests with R’s econometric `MSGARCH` library to calculate Markov-switching GARCH states on return series. Using Python's Ryp library, it initializes an R environment directly inside Python to run R-native estimation functions while serving predictions over a standard JSON API. The lesser-known `ryp` module was favored because the better-known `rpy2` package does not work well on Microsoft Windows operating systems.

The application runs a synchronous ZeroMQ event loop listening on a REP (Reply) socket bound to TCP port 5555 by default. The internal logic relies on the RMSGarchEstimator class, which constructs and manages dynamic model specifications. To optimize throughput, estimators are lazily instantiated and stored in an in-memory dictionary cache keyed by their exact configuration parameters. This strategy avoids the overhead of repeatedly rebuilding R specification objects for recurring request signatures.

class RMSGarchEstimator:
    """
    Wraps an R MSGARCH::CreateSpec object with support for:
      - arbitrary number of regimes (n_states)
      - a different variance model / distribution per regime
      - mixture (do.mix=TRUE) vs Markov-switching (do.mix=FALSE)
      - ML or MCMC estimation, with MCMC controls exposed

    fit_and_predict() can return either just the latest-bar summary
    or the full in-sample regime path so it can be
    overlaid on the original return series.
    """

    def __init__(self,
                 n_states=2,
                 variance_models="sGARCH",
                 distributions="std",
                 do_mix=False,
                 estimation_method="ML",
                 mcmc_nburn=1000,
                 mcmc_nmesh=2500,
                 mcmc_nthin=1):
        """
        n_states: number of regimes (>= 2)
        variance_models: single model name (broadcast to all states) or a
            list of length n_states, e.g. ["sGARCH", "eGARCH", "gjrGARCH"]
            Any MSGARCH-supported variance model is valid: sGARCH, eGARCH,
            gjrGARCH, tGARCH, fGARCH, ...
        distributions: single distribution name (broadcast) or a list of
            length n_states, e.g. "norm", "std" (Student-t), "ged", "snorm", ...
        do_mix: True for an iid mixture, False (default) for a Markov-switching model
        estimation_method: "ML" or "MCMC"
        mcmc_nburn/mcmc_nmesh/mcmc_nthin: only used when estimation_method == "MCMC"
        """
        if n_states < 2:
            raise ValueError("n_states must be >= 2")

        self.n_states = n_states
        self.variance_models = self._broadcast(variance_models, n_states)
        self.distributions = self._broadcast(distributions, n_states)
        self.do_mix = bool(do_mix)
        self.method = estimation_method
        self.mcmc_ctr = {"nburn": mcmc_nburn, "nmesh": mcmc_nmesh, "nthin": mcmc_nthin}

        self._spec_var = f"spec_{uuid.uuid4().hex[:8]}"
        self._init_model()

    @staticmethod
    def _broadcast(val, n):
        """Allow a single string to be broadcast to all states, or an explicit list."""
        if isinstance(val, str):
            return [val] * n
        val = list(val)
        if len(val) != n:
            raise ValueError(f"Expected {n} entries, got {len(val)}: {val}")
        return val

    def _init_model(self):
        var_vec = "c(" + ", ".join(f'"{v}"' for v in self.variance_models) + ")"
        dist_vec = "c(" + ", ".join(f'"{d}"' for d in self.distributions) + ")"
        do_mix_r = "TRUE" if self.do_mix else "FALSE"

        r(f'''
            {self._spec_var} <- CreateSpec(
                variance.spec = list(model = {var_vec}),
                distribution.spec = list(distribution = {dist_vec}),
                switch.spec = list(do.mix = {do_mix_r})
            )
        ''')
        print(f"[R Engine] Initialized {self.n_states}-state MS-GARCH "
              f"(variance={self.variance_models}, dist={self.distributions}, "
              f"do.mix={self.do_mix}, method={self.method})")

    @staticmethod
    def _r_matrix_or_none(varname):
        """Fetch an R object as a numpy array, or None if it's NULL / missing.
        ryp converts R NULL -> Python None on its own, so this is just a
        thin, explicitly-typed wrapper around to_py()."""
        val = to_py(varname)
        return val

    @staticmethod
    def _regime_label(rank, n_states):
        if n_states == 2:
            return ["CALM", "CRISIS"][rank]
        if n_states == 3:
            return ["CALM", "TRANSITION", "CRISIS"][rank]
        return f"REGIME_{rank}"  # 0 = lowest average volatility ... n_states-1 = highest

    def fit_and_predict(self, returns, timestamps=None, include_series=False):
        """
        Fits the model on `returns` and returns:
          - a latest-bar summary (state probabilities, dominant regime, etc.)
          - if include_series=True, the full in-sample regime path aligned
            to `returns` / `timestamps`, suitable for overlaying regimes on
            the original series.

        Regimes are consistently ordered by ascending average volatility
        (rank 0 = calmest ... rank n_states-1 = most volatile) rather than
        by R's arbitrary internal state index, since MSGARCH state labels
        are not guaranteed to be stable across fits.
        """
        to_r(np.array(returns, dtype=np.float64), 'returns_vec')

        try:
            if self.method == "ML":
                r(f'''
                    fit <- FitML(spec = {self._spec_var}, data = returns_vec)
                    st <- State(fit)
                    predprob <- tryCatch(st$PredProb, error = function(e) NULL)
                    filtprob <- tryCatch(st$FiltProb, error = function(e) NULL)
                    viterbi  <- tryCatch(st$Viterbi,  error = function(e) NULL)
                    vol      <- tryCatch(Volatility(fit), error = function(e) NULL)
                ''')
            elif self.method == "MCMC":
                r(f'''
                    fit <- FitMCMC(spec = {self._spec_var}, data = returns_vec,
                                   ctr = list(nburn = {self.mcmc_ctr["nburn"]},
                                              nmesh = {self.mcmc_ctr["nmesh"]},
                                              nthin = {self.mcmc_ctr["nthin"]}))
                    st <- State(fit)
                    predprob <- tryCatch(st$PredProb, error = function(e) NULL)
                    filtprob <- tryCatch(st$FiltProb, error = function(e) NULL)
                    viterbi  <- tryCatch(st$Viterbi,  error = function(e) NULL)
                    vol      <- tryCatch(Volatility(fit), error = function(e) NULL)
                ''')
            else:
                return {"status": "error", "message": f"Unknown estimation_method: {self.method}"}

            filt_probs = self._r_matrix_or_none('filtprob')
            pred_probs = self._r_matrix_or_none('predprob')
            viterbi = self._r_matrix_or_none('viterbi')
            vol = self._r_matrix_or_none('vol')

            # Prefer filtered/smoothed probabilities for in-sample regime labeling;
            # fall back to predicted probabilities if FiltProb isn't available.
            state_probs = filt_probs if filt_probs is not None else pred_probs
            if state_probs is None:
                return {"status": "error", "message": "MSGARCH did not return state probabilities."}

            n_states = self.n_states
            
            if self.method == "MCMC":
                state_probs = np.mean(state_probs,axis=-2,keepdims=True)
                pred_probs = np.mean(pred_probs,axis=-2,keepdims=True)
                viterbi = np.mean(viterbi,axis=-1)
                viterbi = np.round(viterbi,0)

            # Order regimes by ascending average volatility across the whole fit,
            # not just the last bar, so the calm/crisis labeling is stable.
            if vol is not None and vol.ndim == 2 and vol.shape[1] == n_states:
                avg_vol = vol.mean(axis=0)
                rank_order = np.argsort(avg_vol)  # rank_order[0] = calmest raw state index
            else:
                avg_vol = None
                rank_order = np.arange(n_states)

            raw_to_rank = {int(raw): rank for rank, raw in enumerate(rank_order)}

            latest_probs_ranked = np.reshape(state_probs[-1],state_probs.shape[-1])
            dominant_state_raw = int(np.argmax(latest_probs_ranked))
            dominant_rank = raw_to_rank[dominant_state_raw]

            summary = {
                "status": "success",
                "method": self.method,
                "n_states": n_states,
                "variance_models": self.variance_models,
                "distributions": self.distributions,
                "do_mix": self.do_mix,
                "state_probabilities": latest_probs_ranked.tolist(),  # ordered calm -> turbulent
                "dominant_regime": dominant_rank,
                "regime_label": self._regime_label(dominant_rank, n_states),
                "state_volatility": (avg_vol[rank_order].tolist() if avg_vol is not None else None),
            }

            # Backward-compatible fields for existing 2-state consumers (e.g. CSignalGarchTouch.mqh)
            if n_states == 2:
                summary["p_calm"] = float(latest_probs_ranked[0])
                summary["p_crisis"] = float(latest_probs_ranked[1])
                summary["current_state"] = "CRISIS" if latest_probs_ranked[1] > 0.5 else "CALM"
                summary["vol_calm"] = summary["state_volatility"][0] if avg_vol is not None else None
                summary["vol_crisis"] = summary["state_volatility"][1] if avg_vol is not None else None

            if include_series:
                T = state_probs.shape[0]
                ranked_probs_series = state_probs.reshape((state_probs.shape[0],state_probs.shape[-1]))
                dominant_per_bar_raw = np.argmax(state_probs, axis=2)
                dominant_rank_per_bar = np.array([raw_to_rank[int(r_[0])] for r_ in dominant_per_bar_raw])

                vol_series_ranked = None
                if vol is not None and vol.ndim == 2 and vol.shape[1] == n_states:
                    vol_series_ranked = vol[:, rank_order]

                viterbi_rank_series = None
                if viterbi is not None:
                    # MSGARCH's Viterbi path is 1-indexed R state labels
                    viterbi_raw = np.array(viterbi).astype(int).flatten() - 1
                    viterbi_rank_series = np.array([raw_to_rank[int(r_)] for r_ in viterbi_raw])

                returns_arr = np.array(returns, dtype=np.float64)
                series = []
                for i in range(T):
                    entry = {
                        "index": i,
                        "timestamp": timestamps[i] if timestamps is not None and i < len(timestamps) else None,
                        "return": float(returns_arr[i]) if i < len(returns_arr) else None,
                        "state_probs": ranked_probs_series[i].tolist(),
                        "regime": int(dominant_rank_per_bar[i]),
                        "regime_label": self._regime_label(int(dominant_rank_per_bar[i]), n_states),
                    }
                    if vol_series_ranked is not None:
                        entry["state_vol"] = vol_series_ranked[i].tolist()
                    if viterbi_rank_series is not None and i < len(viterbi_rank_series):
                        entry["viterbi_regime"] = int(viterbi_rank_series[i])
                    series.append(entry)

                summary["series"] = series

            return summary

        except Exception as e:
            return {"status": "error", "message": str(e)}

When a prediction request arrives, the server passes the return series into R via `ryp` and executes maximum likelihood (`FitML`) or MCMC (`FitMCMC`) estimation. Once fitted, it extracts state probabilities, Viterbi regime paths, and regime volatility matrices. The application exposes a single ZeroMQ REP endpoint that expects JSON-encoded request strings and returns structured JSON responses. The API supports two distinct actions.

  • The `configure` specification modifies the server's global default configuration at runtime. It accepts parameters that correspond to the inputs of MSGARCH's CreateSpec(). It validates and updates defaults for subsequent prediction calls that do not specify their own configurations. Parameter names map directly to parameters used in R's `MSGARCH` library.
  • The `predict` specification ingests a container of observations (minimum 50) alongside optional parameters like `timestamps`, an `include_series` boolean flag, and a single-request `config` override object. If `include_series` is set to `true`, the server returns a bar-by-bar regime breakdown overlay; otherwise, it returns a concise summary containing the latest bar's state probabilities, dominant regime rank, and regime labels.

The MQL5 Client Architecture

The MS_GARCH_Regimes.mq5 script acts as an MQL5 client that requests real-time regime-switching models from msgarch_server.py and overlays the resulting volatility regimes directly onto a MetaTrader 5 chart. It communicates via the high-level CMSGarchClient wrapper class, which internally builds upon the low-level native ZMTP implementation.

//+------------------------------------------------------------------+
//| Client wrapping a single ZMQ REQ socket to the MSGARCH server.   |
//+------------------------------------------------------------------+
class CMSGarchClient
  {
private:
   CZmqReqSocket    m_req;
   int              m_port;
   string           m_endpoint;
   int              m_timeout_ms;
   bool             m_connected;

   //--- send a request string and block (up to m_timeout_ms) for the reply
   bool              SendReceive(const string request_json, string &response_json);

   //--- build a JSON array literal like [1.0,2.0,-3.5] from a double[]
   string            DoubleArrayToJson(const double &arr[]);

   //--- build a JSON array literal of quoted strings like ["sGARCH","eGARCH"]
   string            StringArrayToJson(const string &arr[]) ;

   //--- parse the fields common to every successful "predict" response
   void              ParseSummary(const CJsonValue *root, MSGarchPredictResult &result);
   //--- parse the optional "series" array into an MSGarchRegimeBar[]
   void              ParseSeries(const CJsonValue *root, MSGarchRegimeBar &series[]);
    

public:
                     CMSGarchClient(const string endpoint = "tcp://127.0.0.1", const int port = 5555, const int timeout_ms = 5000)
     {
      m_port = port;
      m_endpoint   = endpoint;
      m_timeout_ms = timeout_ms;
      m_connected  = false;
     }

                    ~CMSGarchClient(void)
     {
      if(m_connected)
         m_req.Disconnect();
     }

   bool              Connect(void);

   //--- {"action":"configure", ...}. Pass an empty array to leave variance/dist
   //--- at their broadcast default ("sGARCH" / "std") server-side.
   bool              Configure(const int n_states,
                                const string &variance_models[],
                                const string &distributions[],
                                const bool do_mix,
                                const string estimation_method,
                                string &error_message);

   //--- {"action":"predict", "returns":[...], "include_series": bool}
   //--- `returns` should already be whatever return series your model expects
   //--- (e.g. log returns), computed by the caller.
   bool              Predict(const double &returns[],
                              MSGarchPredictResult &result,
                              const bool include_series,
                              MSGarchRegimeBar &series[]);
  };

The wrapper lets an MQL5 EA or script send return data to a Python process running a Markov-switching GARCH model. It returns market regimes, state probabilities, and state-specific volatility in a structured format. Communication payloads are encoded in JSON, while Zmtp.mqh provides ZeroMQ socket functionality and JsonValue.mqh handles JSON parsing.

The CMSGarchClient class encapsulates client-side communication. Connection management is maintained via a single ZeroMQ  REQ  socket, configured with the server endpoint, port, timeout, and connection state. Connect() establishes the connection to the Python server, while the destructor automatically disconnects the socket when the client object is destroyed.

bool              Connect(void)
  {
   m_connected = m_req.Connect(m_endpoint,m_port);
   if(!m_connected)
      Print(__FUNCTION__," : [MSGarchClient] connect() failed for ", m_endpoint);
   return m_connected;
  }

The SendReceive() method provides the basic request/response mechanism. It sends a JSON request through the REQ socket and repeatedly attempts to receive the server's response, sleeping between attempts until a reply arrives or the retry limit is exhausted.

bool              SendReceive(const string request_json, string &response_json)
  {
   response_json = "";

   if(!m_req.Send(request_json))
     {
      Print(__FUNCTION__," : [MSGarchClient] send() failed for endpoint ", m_endpoint);
      return false;
     }

   uint retries = 100;
   while(!m_req.Recv(response_json) && retries)
     {
      --retries;
      Sleep(m_timeout_ms);
     }

   if(StringLen(response_json)<1)
     {
      Print(__FUNCTION__," : [MSGarchClient] recv() timed out / failed after ", m_timeout_ms, " ms");
      return false;
     }
//response_json = reply.getData();
   return true;
  }

The JSON helper DoubleArrayToJson() converts a container of double type into a JSON numerical array, while StringArrayToJson() converts a string array into a quoted JSON array.

string            DoubleArrayToJson(const double &arr[]) const
  {
   string out = "[";
   int n = ArraySize(arr);
   for(int i = 0; i < n; i++)
     {
      out += DoubleToString(arr[i], 10);
      if(i < n - 1)
         out += ",";
     }
   out += "]";
   return out;
  }

//--- build a JSON array literal of quoted strings like ["sGARCH","eGARCH"]
string            StringArrayToJson(const string &arr[]) const
  {
   string out = "[";
   int n = ArraySize(arr);
   for(int i = 0; i < n; i++)
     {
      out += "\"" + arr[i] + "\"";
      if(i < n - 1)
         out += ",";
     }
   out += "]";
   return out;
  }

The Configure() method sends an `action="configure"` request specifying the number of states, variance models, distributions, model mixing settings, and estimation method. Optional variance-model and distribution arrays are only included when supplied, allowing the Python server to fall back to its own defaults.

bool              Configure(const int n_states,
                            const string &variance_models[],
                            const string &distributions[],
                            const bool do_mix,
                            const string estimation_method,
                            string &error_message)
  {
   string req = "{\"action\":\"configure\"";
   req += ",\"n_states\":" + IntegerToString(n_states);
   if(ArraySize(variance_models) > 0)
      req += ",\"variance_models\":" + StringArrayToJson(variance_models);
   if(ArraySize(distributions) > 0)
      req += ",\"distributions\":" + StringArrayToJson(distributions);
   req += ",\"do_mix\":" + (do_mix ? "true" : "false");
   req += ",\"estimation_method\":\"" + estimation_method + "\"";
   req += "}";

   string response_json;
   if(!SendReceive(req, response_json))
     {
      error_message = "transport error (no reply from server)";
      return false;
     }

   CJsonParser parser;
   CJsonValue *root = parser.Parse(response_json);
   if(root == NULL)
     {
      error_message = "could not parse server response";
      return false;
     }

   bool ok = (root.GetString("status", "error") == "success");
   error_message = ok ? "" : root.GetString("message", "unknown error");
   delete root;
   return ok;
  }

The Predict() method accepts an array of returns calculated by the MQL5 application and sends them using an `action="predict"` JSON request. After receiving the response, it parses the JSON, using ParseSummary() to populate the latest prediction and  ParseSeries() to populate the historical MSGarchRegimeBar[] array.

bool              Predict(const double &returns[],
                          MSGarchPredictResult &result,
                          const bool include_series,
                          MSGarchRegimeBar &series[])
  {
   ArrayResize(result.state_probabilities, 0);
   ArrayResize(result.state_volatility, 0);
   ArrayResize(series, 0);
   result.success = false;

   string req = "{\"action\":\"predict\",\"returns\":" + DoubleArrayToJson(returns) +
                ",\"include_series\":" + (include_series ? "true" : "false") + "}";

   string response_json;
   if(!SendReceive(req, response_json))
     {
      result.error_message = "transport error (no reply from server)";
      return false;
     }

   CJsonParser parser;
   CJsonValue *root = parser.Parse(response_json);
   if(root == NULL)
     {
      result.error_message = "could not parse server response";
      return false;
     }

   bool ok = (root.GetString("status", "error") == "success");
   if(!ok)
     {
      result.error_message = root.GetString("message", "unknown error");
      delete root;
      return false;
     }

   ParseSummary(root, result);
   if(include_series)
      ParseSeries(root, series);

   result.success = true;
   delete root;
   return true;
  }

void              ParseSummary(const CJsonValue *root, MSGarchPredictResult &result) const
  {
   result.method          = root.GetString("method", "");
   result.n_states         = (int)root.GetDouble("n_states", 0);
   result.dominant_regime  = (int)root.GetDouble("dominant_regime", -1);
   result.regime_label     = root.GetString("regime_label", "");

   CJsonValue *probs = root.Get("state_probabilities");
   if(probs != NULL)
      probs.ToDoubleArray(result.state_probabilities);

   CJsonValue *vols = root.Get("state_volatility");
   if(vols != NULL && !vols.IsNull())
      vols.ToDoubleArray(result.state_volatility);

   result.p_calm       = root.GetDouble("p_calm", -1.0);
   result.p_crisis      = root.GetDouble("p_crisis", -1.0);
   result.current_state = root.GetString("current_state", "");
  }

void              ParseSeries(const CJsonValue *root, MSGarchRegimeBar &series[]) const
  {
   CJsonValue *arr = root.Get("series");
   if(arr == NULL)
     {
      ArrayResize(series, 0);
      return;
     }

   int n = arr.Size();
   ArrayResize(series, n);
   for(int i = 0; i < n; i++)
     {
      CJsonValue *bar = arr.At(i);
      series[i].index        = (int)bar.GetDouble("index", i);
      series[i].bar_time      = (datetime)(long)bar.GetDouble("timestamp", 0);
      series[i].ret           = bar.GetDouble("return", 0.0);
      series[i].regime        = (int)bar.GetDouble("regime", 0);
      series[i].regime_label  = bar.GetString("regime_label", "");

      CJsonValue *sp = bar.Get("state_probs");
      if(sp != NULL)
         sp.ToDoubleArray(series[i].state_probs);

      CJsonValue *sv = bar.Get("state_vol");
      if(sv != NULL && !sv.IsNull())
         sv.ToDoubleArray(series[i].state_vol);

      CJsonValue *vit = bar.Get("viterbi_regime");
      if(vit != NULL && !vit.IsNull())
        {
         series[i].has_viterbi    = true;
         series[i].viterbi_regime = vit.AsInt();
        }
      else
        {
         series[i].has_viterbi    = false;
         series[i].viterbi_regime = -1;
        }
     }
  }

Two structures define the data returned by the server. The MSGarchPredictResult structure represents the latest-bar prediction summary. It contains request success status, fitted model method, number of states, posterior probability of each regime, dominant regime, textual label, and volatility associated with each state. For two-state models, convenience fields make it easy for an EA to directly check the probability of a calm or crisis regime.

struct MSGarchPredictResult
  {
   bool             success;
   string           error_message;
   string           method;
   int              n_states;
   double           state_probabilities[];  // ordered calm(0) -> turbulent(n-1)
   int              dominant_regime;        // rank of the highest-probability state
   string           regime_label;
   double           state_volatility[];     // same ordering as state_probabilities
   //--- convenience fields, only populated when n_states == 2
   double           p_calm;
   double           p_crisis;
   string           current_state;
  };

The MSGarchRegimeBar structure represents one observation in the optional historical regime series returned when `include_series=true`. Each bar stores its index, timestamp, return, filtered state probabilities, inferred regime and label, state-specific volatility, and optionally the Viterbi-decoded regime.

struct MSGarchRegimeBar
  {
   int              index;
   datetime         bar_time;     // 0 if the server wasn't given timestamps
   double           ret;
   double           state_probs[];
   int              regime;
   string           regime_label;
   double           state_vol[];
   bool             has_viterbi;
   int              viterbi_regime;
  };

Script Execution and Operational Flow

The script runs as a one-shot MQL5 program through three distinct stages.

  1. Data Collection: The script fetches a sample of closed bars (excluding the currently forming bar) to prevent repainting. It converts rates to chronological order and calculates log returns alongside matching bar timestamps.
  2. Server Integration: A CMSGarchClient instance is initialized. Calling Connect() establishes a native raw MQL5 socket connection that performs the 64-byte ZMTP greeting and `READY` handshake over the wire. The script then transmits a `configure` payload requesting an N-state model with standard GARCH variance and Student-t error distributions estimated via maximum likelihood. Next, Predict() sends log returns with `include_series = true`. The client parses the JSON response into MSGarchPredictResult and an array of MSGarchRegimeBar structs.
  3. Chart Rendering: Invoking DrawRegimeBands() colors each regime band using a dynamic gradient generated by RegimeColor(). The visualization remains on screen for 30 seconds before clearing rectangle objects via ClearRegimeObjects() and terminating.
void OnStart(void)
  {
   int bars = InpLookbackBars + 1; // +1 because returns need one extra prior close
   if(Bars(_Symbol, _Period) < bars)
     {
      Print("Not enough history: need ", bars, " bars.");
      return;
     }

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int copied = CopyRates(_Symbol, _Period, 1, bars, rates); // exclude the still-forming bar
   if(copied < bars)
     {
      Print("CopyRates only returned ", copied, " bars.");
      return;
     }
   ArraySetAsSeries(rates, false); // switch back to chronological order for the loop below

   double returns[];
   datetime bar_times[];
   int n_returns = copied - 1;
   ArrayResize(returns, n_returns);
   ArrayResize(bar_times, n_returns);
   double average = 0.0;
   for(int i = 0; i < n_returns; i++)
     {
      returns[i]   = MathLog(rates[i + 1].close / rates[i].close);
      bar_times[i] = rates[i + 1].time;
      average += returns[i];
     }
     
   //--- Demean to remove ARCH effects 
   average /= double(n_returns);  
   for(int i = 0; i < n_returns; returns[i] -= average, ++i);
   
   CMSGarchClient client(InpEndpoint, InpPort, InpTimeoutMs);
   if(!client.Connect())
     {
      Print("Could not connect to MSGARCH server at ", InpEndpoint);
      return;
     }

//--- 3-state model: sGARCH in every regime, Student-t innovations, Markov-switching
   string variance_models[3] = {"sGARCH", "sGARCH", "sGARCH"};
   string distributions[3]   = {"std", "std", "std"};
   string err;
   if(!client.Configure(InpNStates, variance_models, distributions, false, "ML", err))
     {
      Print("Configure failed: ", err);
      return;
     }

   MSGarchPredictResult result;
   MSGarchRegimeBar series[];
   if(!client.Predict(returns, result, true, series))
     {
      Print("Predict failed: ", result.error_message);
      return;
     }

   PrintFormat("MSGARCH: %d-state fit, dominant regime = %s, latest probs: %s",
               result.n_states, result.regime_label,
               DoubleArrayToString(result.state_probabilities));

   DrawRegimeBands(series, bar_times);
   PrintFormat("Painted %d regime bars onto the chart.", ArraySize(series));
   Sleep(30000);
   ClearRegimeObjects();
   ChartRedraw();

To run this pipeline, start the Python server first. Before invoking the script in MetaTrader 5, add the server's IP address and port to the list of allowed WebRequest/socket hosts in the MetaTrader 5 options menu. The graphic below captures the sequence of events as the pipeline is executed.

MS-GARCH Pipeline Execution


Conclusion and Future Extensions

The Zmtp.mqh library provides a simple way to connect MetaTrader 5 with external analytical tools using native MQL5 sockets. Implementation of ZMTP framing directly removes the need for DLL wrappers and external dependencies. This gives developers a cleaner way to build MQL5 applications that communicate with external systems. The current implementation focuses on the REQ/REP messaging pattern. This is sufficient for client-server workflows, as demonstrated in the text. This pattern works well as a type of remote procedure call as well as general task distribution.

Looking towards the future, the CZmtpTransport class provides the foundation for supporting additional ZeroMQ socket patterns. For example, PUSH/PULL could support unidirectional data streams such as continuous tick or trade telemetry. PUB/SUB could provide topic-based market data distribution to multiple services. DEALER/ROUTER could support more flexible asynchronous and non-blocking message routing.

These extensions would allow native MQL5 applications to move beyond simple request-response communication. They could become part of larger distributed systems in which MetaTrader 5 handles market data and execution while external services perform tasks as part of a broader pipeline. The result is a flexible foundation for building distributed quantitative trading architectures without requiring DLL-based networking. All code referenced in the article is listed below and available on AlgoForge.

Note that the Python server requires a standard R installation with the MSGARCH package.

MQL5/Files/ZmqRequestSocket/requirements.txt: These are the dependencies required for the Python application ms_garch_server.py.
MQL5/Files/ZmqRequestSocket/Python/ms_garch_server.py:
The ZeroMQ-based REP server fitting MS-GARCH regimes.
MQL5/Include/ZmqRequestSocket/JsonValue.mqh: Minimal JSON parser used in MSGarchClient.mqh.
MQL5/Include/ZmqRequestSocket/Zmtp.mqh: Header for ZeroMQ Message Transfer Protocol implementation.
MQL5/Include/ZmqRequestSocket/MSGarchClient.mqh: This header contains utilities used by clients connecting to ms_garch_server.py, used in the MS_GARCH_Regimes script.
MQL5/Scripts/ZmqRequestSocket/MS_GARCH_Regimes.mq5: An example script that connects to ms_garch_server.py to fit MS-GARCH models and paints the regimes on the chart.
Attached files |
requirements.txt (0.14 KB)
ms_garch_server.py (15.58 KB)
JsonValue.mqh (8.07 KB)
MSGarchClient.mqh (9.69 KB)
Zmtp.mqh (18.81 KB)
Neural Networks in Trading: The Temporal Query Model (Conclusion) Neural Networks in Trading: The Temporal Query Model (Conclusion)
We are pleased to present the final stage of the TQNet framework’s development and testing, where theory meets real-world trading practice. We will move from historical training to a stress test using recent market data, evaluating the model's robustness and accuracy. The final results are not just dry statistics, but also a clear demonstration of the practical value of the proposed approach.
Native Isolation Forest for Execution-Quality Anomaly Detection in MQL5 Native Isolation Forest for Execution-Quality Anomaly Detection in MQL5
A step-by-step guide to a native Isolation Forest in MQL5 focused on execution metrics rather than price. It details five features, tree construction and path‑length scoring, rolling‑window training, CSV logging, and FILE_COMMON persistence, all integrated into OnTradeTransaction(). The resulting circuit breaker flags unusual fills in real time and applies controlled responses to stabilize live trading under changing execution conditions.
Defining your Edge (Part 5): Using GARCH Variance and Volatility-Scaled LSTM in an Expert Advisor Defining your Edge (Part 5): Using GARCH Variance and Volatility-Scaled LSTM in an Expert Advisor
We merge GARCH(1,1) variance projections with ATR plus Bollinger-Bands patterns to form an algorithm that could optionally be used with volatility-scaled LSTM within LSTM Wizard-ready signal class. We cover feature scaling, mode scoring, thresholds, and safety checks. Readers can replicate backtest/forward test results to verify if the recurrent layer gives incremental discrimination over our deterministic baseline.
The MQL5 Standard Library Explorer (Part 16): Building a Regime-Adaptive Expert Advisor The MQL5 Standard Library Explorer (Part 16): Building a Regime-Adaptive Expert Advisor
We convert the Part 15 decision‑forest classifier into a regime‑adaptive Expert Advisor that decouples statistical inference from trading authority. The EA trains on completed bars, scores each new completed bar, and confirms stable bullish, neutral, or bearish regimes before acting. It then applies spread, ownership, risk, and execution checks to authorize opening, holding, closing, or blocking a position.