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.
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.