Mt4 proxy python EA --- Introduction

4 May 2023, 10:43
Cheung Ka Ho
0
134

Introduction

Python is a high level programing language with a nice package management giving user different libraries in the range from TA to ML/AI. Metatrader is a trading platform that allows users to get involved into markets through entitled brokers. Combining python with MT4 would give user an unprecedented convienance over the connection of brokers and the freedom of library utilization. The potential of your EA now becomes unlimited as you can create different AI strategies on top of those famous ML python libraries freely.

MT4_Python_relationship


The above picture shows the relationship between the python EA and the MT4 system. You will need to have your customized proxy EA running on the MT4 side. TCP socket will be used for messaging between the python EA and proxy EA. There are 3 main components in the MT4 system:

  • Py3_Proxy_Interface.ex4 --- It holds the core communication implementation. You can get a free version from the MQL5 for backtesting your strategy.
  • WinSocketWrapper.mqh --- It contains the windows socket utilization.
  • Py_Proxy_Ea.mq4 --- The last component in MT4 side. By combining all 3 of them, you will have your own customized proxy EA running as a server on the MT4 side and waiting for the connection from the python EA. In addition, you can control the input parameters as what you did as a normal MT4 EA.

The idea of the proxy EA is not only providing python interfaces but also keeping the life cycle of EAs so that the callback sequences are remained in the python side which means that you can backtest your python EA on the MT4 platform.

As one may notice that the communication between the processes is TPC socket, speed is a bit slower to the share memory method. To keep the speed of the EA operation while does not sacrifice much of the data converage. The OnTick function on the python side will be triggered for each bar instead of each tick. In other words, whenever there is a bar update, the function OnTick will be triggered.

Limitation & Notice

There are limitations of this solution.

  • This is not a bug free library. Users are encouraged to test out their EAs through the free version provided on the marketplace amid the backtesting phases. There is no support guarantee but you can put down the bugs or suggestions you have in the comments.
  • Not all of the functions in MQL4 are supported. You can find the list of supported functions within the file --- function_list.txt
  • The callback frequence is not tick by tick, that is based on bar, for example 1M, 5M, etc...

If you like this library and would you like to use it for live trading, feel free to subscribe it through the marketplace. Please only do this after you tested and confirm it works for your use case.

There is no guarantee of profits by this tool. It is just a bridge tool between MT4 and python.

Example python EA --- Moving average EA

from datetime import datetime

import pysailfish.internal.MT_EA.mt4_const as mc
from pysailfish.internal.MT_EA.MT4_EA import MT4_EA

class MA_EA(MT4_EA):
    def __init__(self):
        super().__init__()

    # override
    def _OnInit(self) -> int:
        self._logger.info(self._user_inputs)
        self._logger.info("Here")
        return 0

    # override
    def _OnDeinit(self, reason: int) -> None:
        self._logger.info(f"Here reason: {reason}")
        return None

    # override
    def _OnTick(self) -> None:
        vv = self.iADX(symbol=self._pv.symbol
                       , timeframe=mc.PERIOD_CURRENT
                       , period=self._user_inputs["MovingPeriod"]
                       , applied_price=mc.PRICE_CLOSE
                       , mode=mc.MODE_MAIN
                       , shift=0)
        # self._logger.info(f"Here {self._pv.symbol} {self._pv.time[0]}")
        # --- check for history and trading
        if self._pv.bars < 100 or self._pv.is_trade_allowed == False:
            return

        # --- calculate open orders by current symbol
        if self.__calculate_current_orders(self._pv.symbol) == 0:
            self.__check_for_open()
        else:
            self.__check_for_close()
        return None

    # override
    def _OnTimer(self) -> None:
        return None

    # override
    def _OnTester(self) -> float:
        self._logger.info("Here")
        return 0.0

    # override
    def _OnChartEvent(self
                      , id: int
                      , lparam: int
                      , dparam: float
                      , sparam: str) -> None:
        self._logger.info(f"Here id: {id} lparam: {lparam} dparam: {dparam} sparam: {sparam}")
        return None

    def __check_for_close(self) -> None:
        ma: float = 0
        #--- go trading only for first tiks of new bar
        if self._pv.volume[0] > 1:
            return None
        #--- get Moving Average
        ma = self.iMA(symbol=self._pv.symbol
                      , timeframe=mc.PERIOD_CURRENT
                      , ma_period=self._user_inputs["MovingPeriod"]
                      , ma_shift=self._user_inputs["MovingShift"]
                      , ma_method=mc.MODE_SMA
                      , applied_price=mc.PRICE_CLOSE
                      , shift=0)
        #---
        for i in range(self.OrdersTotal()):
            if self.OrderSelect(index=i, select=mc.SELECT_BY_POS, pool=mc.MODE_TRADES) == False:
                break
            if self.OrderMagicNumber() != self._pv.magic_num or self.OrderSymbol() != self._pv.symbol:
                continue
            #--- check order type
            if self.OrderType() == mc.OP_BUY:
                if self._pv.open[1] > ma and self._pv.close[1] < ma:
                    if not self.OrderClose(ticket=self.OrderTicket(), lots=self.OrderLots(), price=self._pv.bid, slippage=3, arrow_color=mc.clrWhite):
                        self._logger.error(f"OrderClose error {self.GetLastError()}")
                break
            if self.OrderType() == mc.OP_SELL:
                if self._pv.open[1] < ma and self._pv.close[1] > ma:
                    if not self.OrderClose(ticket=self.OrderTicket(), lots=self.OrderLots(), price=self._pv.ask, slippage=3, arrow_color=mc.clrWhite):
                        self._logger.error(f"OrderClose error {self.GetLastError()}");
                break

    def __lots_optimized(self) -> float:
        lot: float = float(self._user_inputs["Lots"])
        maximum_risk: float = float(self._user_inputs["MaximumRisk"])
        decrease_factor: float = float(self._user_inputs["DecreaseFactor"])
        orders: int = self.OrdersHistoryTotal() # history orders total
        losses: int = 0 # number of losses orders without a break
        #--- select lot size
        lot = self.NormalizeDouble(value=(self.AccountFreeMargin() * maximum_risk / 1000.0), digits=1);
        #--- calcuulate number of losses orders without a break
        if decrease_factor > 0:
            for i in reversed(range(orders)):
                if self.OrderSelect(index=i, select=mc.SELECT_BY_POS, pool=mc.MODE_HISTORY) == False:
                    self._logger.error("Error in history")
                    break
                if self.OrderSymbol() != self._pv.symbol or self.OrderType() > mc.OP_SELL:
                    continue
                #---
                if self.OrderProfit() > 0:
                    break
                if self.OrderProfit() < 0:
                    losses += 1
            if losses > 1:
                lot = self.NormalizeDouble(value=(lot - lot * losses / decrease_factor), digits=1)
        #--- return lot size
        if lot < 0.1:
            lot = 0.1
        return lot

    def __check_for_open(self) -> None:
        ma: float = 0
        res: int = 0
        #--- go trading only for first tiks of new bar
        if self._pv.volume[0] > 1:
            return None
        #--- get Moving Average
        ma = self.iMA(symbol=self._pv.symbol
                      , timeframe=mc.PERIOD_CURRENT
                      , ma_period=self._user_inputs["MovingPeriod"]
                      , ma_shift=self._user_inputs["MovingShift"]
                      , ma_method=mc.MODE_SMA
                      , applied_price=mc.PRICE_CLOSE
                      , shift=0)
        #--- sell conditions
        if self._pv.open[1] > ma and self._pv.close[1] < ma:
            res = self.OrderSend(symbol=self._pv.symbol
                                 , cmd=mc.OP_SELL
                                 , volume=self.__lots_optimized()
                                 , price=self._pv.bid
                                 , slippage=3
                                 , stoploss=0
                                 , takeprofit=0
                                 , comment=""
                                 , magic=self._pv.magic_num
                                 , expiration=datetime(1970, 1, 1, 0, 0, 0)
                                 , arrow_color=mc.clrRed)
            return None
        #--- buy conditions
        if self._pv.open[1] < ma and self._pv.close[1] > ma:
            res = self.OrderSend(symbol=self._pv.symbol
                                 , cmd=mc.OP_BUY
                                 , volume=self.__lots_optimized()
                                 , price=self._pv.ask
                                 , slippage=3
                                 , stoploss=0
                                 , takeprofit=0
                                 , comment=""
                                 , magic=self._pv.magic_num
                                 , expiration=datetime(1970, 1, 1, 0, 0, 0)
                                 , arrow_color=mc.clrBlue)
            return None

    def __calculate_current_orders(self, symbol: str) -> int:
        buys: int = 0
        sells: int = 0
        # ---
        for i in range(self.OrdersTotal()):
            if self._tf.order_select(index=i, select=mc.SELECT_BY_POS, pool=mc.MODE_TRADES) == False:
                break
            if self.OrderSymbol() == self._pv.symbol and self.OrderMagicNumber() == self._pv.magic_num:
                if self.OrderType() == mc.OP_BUY:
                    buys += 1
                if self.OrderType() == mc.OP_SELL:
                    sells += 1
        # --- return orders volume
        if buys > 0:
            return buys
        else:
            return -sells

def main() -> None:
    ea = MA_EA()
    ea.InitComponent(server_ip="127.0.0.1"
                     , server_port=23456
                     , ea_name="MA_EA")
    ea.StartEA()

if __name__ == "__main__":
    main()

Here shows an example of a moving average EA.