preloader
post-thumb

Last Update: May 27, 2024


BY eric


author-thumb


Keywords

Introduction

MetaTrader doesn't have strong support for algorithmic trading if you want to develop a trading bot that doesn't heavily rely on this particular trading platform. Typically, people doing automated trading on MetaTrader platforms have to code it completely in MQL, and a terminal must be run actively in the foreground.

To reduce the dependency solely on the MQL programming language or to increase the flexibility of a trading system (bot) that can be coded in a different language or run on different operating systems using any preferred technical analysis libraries, some MetaTrader libraries using a message queue (MQ) service, such as ZeroMQ, have been developed. In this way, the part of the code that is coupled with MetaTrader platforms acts only as a relay (proxy agent).

Some people want to completely overcome the limitations of what MetaTrader can provide, so they hack the underlying communication protocol between the MetaTrader terminal and the server and develop libraries in a programming language of their choice, such as C# or C++. With such MetaTrader libraries, an active terminal running in the foreground is no longer necessary, and the trading bot can run headless on Windows, MacOS, or Linux. However, there is a catch with this method: since such a hack is not officially supported by the trading platform developer, it may need constant updates due to requirements for latest terminal version or may simply stop working due to a change in the underlying protocol.

In this article, we will try to develop a trading gateway (or proxy agent) that is intended to reduce the dependency on MetaTrader platform, but also maintain the stability and reliability of such a program in the long term. When the gateway is running or live trading is in action, we don't want the unpredicted interrupt the whole workflow: our trading system (bot) could stop receving market data updates all of a sudden, or it could not place order or receive order status information anymore without an update to the trading program.

The Design of the Trading System (Bot)

In order to achieve the following goals:

  • You do not have to use MQL and do extra compilations, and needs as little operations as possible on MetaTrader, other than leaving it running in the foreground
  • The gateway should be stable and reliable. The gateway should continue working even with that the updates of MetaTrader is required constantly.

Fortunately, MetaQuote, the developer of MetaTrader, has provided an official Python package for integration since MetaTrader 5. With this package, you can create a trading bot entirely coded in Python, or you can build a trading gateway that acts as a bridge between the trading platform and an existing trading system (bot) developed in any language (e.g., NodeJS, C++, C#, Java). In the latter case, you can leverage the ecosystem of each language and enjoy the freedom from limitations in both programming language and operating system.

In this article we will focus on developing such a trading gateway using the MetaTrader 5 Python package for the itegration. To ensure seamless communication within the trading system, a complete setup should consist of the following main components:

  • a message queue server for messaging and message relaying (price, orders, etc.)
  • trading gateway(s) for getting price updates and managing orders
  • indicator updater after receiving new price data
  • a trading decision maker (strategy) for generating buy or sell signals

A gateway is a small but essential part of the entire traiding system. It obtains the latest price updates and manages the placement of new orders or the closure of existing orders when buy or sell signals are triggered.

The following figure illustrates the workflow of a complete trading system:

![](/images/blog/mt5-python-gateway/Trading System Flowchart.png)

The Implementation of Core Funtinalities of the MetaTrader Gateway

In this section we will discuss the core functionalities of a trading gateway. We will expect at least the following for the MetaTrader Gateway:

  • pull the price updates
  • submit an order when a signal is triggered
  • close the order when it is time to

A complete implementation of a proper gateway could be complicated, more functionalities might be also needed, for examples, sush as acquisition of account's balance, current P&L or historial data. All depend on what is required for the trading system. Being able to get the account balance could be benificial but is not an absolute requirement.

The actual implementation of this gateway could be straight-forward, as we can just call the coresponding functions provided by the API. However, how to use those functions to achieve the goals of auto trading may be not as that simple as you may think. The following sections will discuss the each aspect of the functionalities in details.

Initialzation of MetaTrader Terminal

As discussed previously, the MetaTrader python library requires MetaTrader terminal running in the foreground, so it can communicate with the terminal in order to retrieve the price updates and place orders.

There are two parts in this process:

  • start the terminal if it has not been running already
  • loging into your account

Start The terminal

    path = "C:\\Program Files\\MetaTrader 5\\terminal64.exe"
    timeout = 1000000
    portable = False 
    if not mt5.initialize(path=path, portable=portable, timeout=timeout):
        print("initialization failed, error code =", mt5.last_error())
        quit()

To start a terminal, the library needs to know where the terminal executable (terminal64.exe) is located, and that is the variable "path" for. In some cases the library might have difficulties in building communciation with the terminal, "timeout" is the time limit in millionseconds when it will stop trying.

Also, different terminal executables provided by the brokers are actually all the same just with different brandings and a different installation path. You can copy and paste the terminal program to a different directory and run it from there. In this case, you can specify the "portable" mode to "True", so the data from the terminal will be installed in that directory.

Account Access

Once the terminal is up and running. You will need to authenciate yourself with your broker's MetaTrader trading server.

    account = 888888
    password = "Your Password Here"
    server = "MetaQuotes-Demo"
    timeout = 60000
    authorized = mt5.login(account, password=password, server=server, timeout=timeout)
    if authorized:
        print("Login successful")
    else
        print("Failed to connect to the trade account with error code =", mt5.last_error())

Getting Price Updates

One major limitation of this package is that you cannot get the price updates through "pumping", which is a machanism that you can get notification whenever new data arrives. In order to get the latest price data, you have to make query to the terminal to see if there is any new data. A chart window for the symbol you are watching should be opened as well, as this active window would make sure the terminal to receive the price updates from the server.

So we can set a timer, or create a infinit loop inside a child thread and then wake up after being asleep for a certain amount of time. In the example below, we will use a timer to get latest bitcoid price:

last_updated = None
timezone = pytz.timezone("Etc/UTC")
def check_updates():
    # print ("checking for updates") 
    # for different brokers, different tickers for BITCOIN could be used
    # such as 'BITCOIN' or 'BTCUSD'
    time_from = None
    time_to = datetime.now(timezone)
    if last_updated is None:
        time_from = datetime.now(timezone)
        # this is the first time we get the price we will try get all ticks in the last two mintues
        time_from = time_from  - timedelta(minutes=2)
    else:
        time_from = last_updated
        time_from = time_from + timedelta(seconds=1)

    ticks = mt5.copy_ticks_range('BTCUSD', time_from, time_to, mt5.COPY_TICKS_ALL)
    if ticks is None or len(ticks) == 0:
        # print("No ticks received from ", utc_from, "to", server_now, "for ", symbol)
        return
    
    # print("Ticks received:", len(ticks))
    time = None
    for tick in ticks:
        # print the time of the tick
        # named time, bid, ask, last and flags 
        # convert ecpoch to datetime
        time = datetime.fromtimestamp(tick[0])
        # conver time to native with timezone
        time = time.astimezone(timezone)

        # quote = quote()
        # quote.bid = tick[1]
        # quote.ask = tick[2]
        # quote.time = time
        # quote.last = tick[3]
        # quote.volume = tick[4]
        #
        # DO WHATEVER YOU WANT WITH THE DATA
        # For example we can send the quote data to the trading system
        # where indicators kept updated and strategies look for signals 
        # to send quotes to the trading system you can use a message queue system
        # such as ZeroMQ, RabbitMQ, Kafka, etc.
        # or you can use a simple REST API to send the data to the trading system
        # or you can my implementation of a message queue server ('TYO-MQ')
    last_updated = time
    set_timer()

def set_timer():
    # Will the bitcoin price again in 5 seconds
    # If you want to do high-frequency trading obviously this is not good for you
    threading.Timer(5.0, check_updates).start()  

# Don't forget to start the timer
set_timer()

Placing a New order

Once we get a signal from the strategy we can place an order.

# ORDER_TYPE_BUY
# Market Buy order
# ORDER_TYPE_SELL
# Market Sell order
# ORDER_TYPE_BUY_LIMIT
# Buy Limit pending order
# ORDER_TYPE_SELL_LIMIT
# Sell Limit pending order
# ORDER_TYPE_BUY_STOP
# Buy Stop pending order
# ORDER_TYPE_SELL_STOP
# Sell Stop pending order
# ORDER_TYPE_BUY_STOP_LIMIT
# Upon reaching the order price, a pending Buy Limit order is placed at the StopLimit price
# ORDER_TYPE_SELL_STOP_LIMIT
# Upon reaching the order price, a pending Sell Limit order is placed at the StopLimit price
# ORDER_TYPE_CLOSE_BY
# 
# depends on what the signal indicates for buying or sell
# the order type can be one of the above
# if we want buy from the market we can use: mt5.ORDER_TYPE_BUY
# if we want to sell to the market we can use: mt5.ORDER_TYPE_SELL
# if we want to place a buy limit order we can use: mt5.ORDER_TYPE_BUY_LIMIT

def place_order(symbol, market, type, price, volume, stop_loss, take_profit, comment, stop_price, expire_time):
    # Order to close a position by an opposite one
    trade_request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": float(volum),
        "type": type,
        "price": price,
        "sl": float(stop_loss),
        "tp": float(take_profit),
        "comment": comment, # a comment for the order, max 31 characters
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_IOC, # mt5.ORDER_FILLING_RETURN
        "deviation": 20,
        "magic": 0,
        # "expiration": datetime.now() + timedelta(minutes=5), # 5 minutes from now
    }
    trade_result = mt5.order_send(trade_request)
    # you will get a None if the trade_request is not constructed properly
    # for example, the volume is not a float
    # or the price is not a float
    # or the stop_loss is not a float
    # or the take_profit is not a float
    # or the stop_price is not a float
    # or the expire_time is not a datetime object
    # or the comment is not a string or longer than 31 characters
    if trade_result.retcode != mt5.TRADE_RETCODE_DONE:
        print("Error placing order: ", symbol, ", type: ", type, ", volume: ", volume, ", price: ", price, ", stop_loss: ", stop_loss, ", take_profit: ", take_profit, ", comment: ", comment, ", stop_price: ", stop_price, ", expire_time: ", expire_time)
        return None

    print("Order placed: ", trade_result.order, ", for ", symbol, ", type: ", type, ", volume: ", volume, ", price: ", price, ", stop_loss: ", stop_loss, ", take_profit: ", take_profit, ", comment: ", comment, ", stop_price: ", stop_price, ", expire_time: ", expire_time)

    return trade_result.order

If the order is successfully placed, the "place_order" function will return a ticket for the trade.

Closing an order

Once we have an order in place, we may expect two outcomes:

  • The signal is valid, and the price moves in the same direction as the signal indicates. In this case, we could exit: 1) if it reaches the price target; 2) when the trend reverses.

  • The signal is not valid, and the price moves in the opposite direction than we expect. In this case, we should exit if the price goes beyond the stop-loss.

The stop-loss can be pre-calculated by the trading strategy or a pre-set threshold if the loss reach x percentage of the total value.

To close an existing order, what we have do is just to create a reverse order and include the ticket number as well. So if the order is a long position, we will create a sell order.

def reverse_order_type(type):
    # if the order is a buy order, we will create a sell order
    # adjust the function based on your need, you may want to create a limit order instead of a market order
    if type == mt5.ORDER_TYPE_BUY:
        return mt5.ORDER_TYPE_SELL
    else
        return mt5.ORDER_TYPE_BUY

def close_order(symbol, type, price, volume, ticket, comment):
    trade_request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": float(volume),
        "type": reverse_order_type(type),
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_IOC, # mt5.ORDER_FILLING_RETURN,
        "deviation": 20,
        "magic": 0,
        "position": int(ticket),
    }
    trade_result = mt5.order_send(trade_request)

    # if the trade_request is not constructed properly, the trade_result will be None
    # for example, the ticket is not valid (e.g. it is a string not an integer)
    # and vloume is not a float
    if trade_result is None or trade_result.retcode != mt5.TRADE_RETCODE_DONE:
        print ("Failed to close order: ", ticket)
        return False
    return True

Please note there are at least three types of filling for opening or closing order: 1) 'ORDER_FILLING_IOC' means that after whatever maximum availabe volume get filled in the market, the remaining volume will be cancelled; 2) 'ORDER_FILLING_FOK' means an order has to be executed as a whole with the specified volume, so it is basically "deal or no deal"; 3) 'ORDER_FILLING_RETURN' can do other types can't do, for example, if the order is partially executed and the rest of the order won't be cancelled. However, not all brokers support this type of filling. It is understandable that the broker may not need the 'ORDER_FILLING_RETURN' filling type because for forex, metals, and other instruments, the volume is usually not a problem and for the share CFD products, basically it is you against the broker so as long as within the maximum volume you can trade, it can be garanteed that your order will be filled when the order price matches.

Conclusion

In this article, we have discussed the core functionalities of a trading gateway using the MetaTrader's python library. The core idea is that it is intended to reduce the dependency on MetaTrader platform, but also maintain the stability and reliability of such a program (the gateway) in the long term. We have discussed the initialzation of the MetaTrader terminal, getting price updates, placing a new order, and closing an existing order. After the implementation of these core functionalities, the gateway can be used as a bridge between the MetaTrader 5 trading platform and an existing trading system (bot) developed in any language (e.g., NodeJS, C++, C#, Java) once a messaging mechanism is in place.

Further Reading

We will continue this topic in part II. In next part we will present a complete implementation of such a gateway for trading bitcoin with a messaging server for the communciation between the trading system and the gateway. Setting up a development environment for futher implementation of building a complete trading system will be also discussed.

Contact

For further information, I can be reach via:

agico

We transform visions into reality. We specializes in crafting digital experiences that captivate, engage, and innovate. With a fusion of creativity and expertise, we bring your ideas to life, one pixel at a time. Let's build the future together.

Copyright ©  2024  TYO Lab