#!/usr/bin/env python3 # con2mtapi.py - An example for connecting to the mtapi EA on MT4 and get some data # -*- coding: utf-8 -*- # --------------------------------------------------------------------- # Author: EAML # Date: 2020-11-10 # Version: 1.0.2 # License: MIT # URL: https://github.com/eabase/mt4pycon/ # --------------------------------------------------------------------- # # Description: # # This is an example client using the MT4 API (mtapi) to connect # to the MT4 EA, using either an TCP/IP or a pipe port, to get some # historical candle OHLCV data for a specified symbol and timeframe. # # ToDo: # # [ ] Make a native pip installer package (PyPi) # [ ] Automatic detection of MT5 # [ ] Fix bug when requested chart does not (yet) have any candle data... double run! # [x] Allow TF specification as MT4/5 text ('4H') and not in minutes ('240') # [ ] Add CLI options: # ---------------------------------------------------------- # - [ ] '-5' : Use script for MT5 installs (this will use MT5 DLL's) # - [ ] '-c' : Enable ANSI color markup of output # - [x] '-d' : Enable extra debug info # - [ ] '-e' : Show candle data as minimal CSV list (ignores -l) ??? # - [x] '-h' : Show THIS help list # - [x] '-n ' : Show number of candles back ['0' is last complete candle] # - [x] '-p' : Use a named pipe connection (instead of a TCP/IP host:port) # - [x] '-l' : Show candle data as a list (one line per OHLC item) # - [x] '-s ' : Set symbol name to ['EURUSD'] # - [x] '-t ' : Set timeframe to # - [x] '-v' : Show THIS program version # ---------------------------------------------------------- # # Installation: # # # # Usage: # # # # Requirements: # # pip install pythonnet # pip install # # NOTES # # 1. Spread for candles are meaningless, unless we talk about an "average", # which is not available without also getting all the ticks inside. # # 2. Some Default Settings: # For MT5: # MTPATH = C:\Program Files\MtApi5 # Default Path to installed MtApi library files # MTSERV = 127.0.0.1 # Default IP address of your localhost where MtApi terminal is running # MTPORT = 8228 # Default Port setting of your MtApi EA # # 3. Using TCP vs. a "Named Pipe" connection: # TCP: client.BeginConnect(ip, port) # Pipe: client.BeginConnect(port) # (Note that a Windows "named pipe" is using the "localhost" interface.) # # 4. What is the deal with: 0.0.0.0 & 127.0.0.1 ? # See [4] # # 5. To measure performance, use: # (Measure-Command { python.exe .\con2mtapi.py -n10000 -s "CADCHF" -t 15 -p }).TotalSeconds # # 6. How to get Tick data streamed? # # struct MqlTick { # datetime time; // Time of the last prices update # double bid; // Current Bid price # double ask; // Current Ask price # double last; // Price of the last deal (Last) # ulong volume; // Volume for the current Last price # # REFERENCES # # [1] https://www.mql5.com/en/docs/constants/structures/mqlrates # [2] https://docs.mql4.com/constants/structures/mqltick # [3] https://stackoverflow.com/questions/48542644/python-and-windows-named-pipes # [4] https://www.howtogeek.com/225487/what-is-the-difference-between-127.0.0.1-and-0.0.0.0/ # [5] https://docs.mql4.com/constants/chartconstants/enum_timeframes # # --------------------------------------------------------------------- import os, sys import clr import time import getopt #-------------------------------------- # For MT5 installs #-------------------------------------- # C:\Program Files\MtApi5 #sys.path.append(r"C:\Program Files\MtApi5") #asm = clr.AddReference("MtApi5") #import MtApi5 as mt #-------------------------------------- # For MT4 installs #-------------------------------------- # C:\Program Files (x86)\MtApi sys.path.append(r"C:\Program Files\MtApi") asm = clr.AddReference('MtApi') import MtApi as mt #-------------------------------------- # Import/Use colorama #-------------------------------------- ##from colorama import Fore, Style #from colorama import init, AnsiToWin32 #init(wrap=False) #stream = AnsiToWin32(sys.stderr).stream #-------------------------------------- #---------------------------------------------------------- # Global Variables #---------------------------------------------------------- __author__ = 'EAML (EABASE)' __copyright__ = 'MIT (2020)' __version__ = '1.0.2' debug = 0 # Enable debug printing #---------------------------------------------------------- # Default Program Constants #---------------------------------------------------------- #MTSERV = '192.168.1.101' # IP address of "your local machine" where MtApi terminal is running MTSERV = '127.0.0.1' # IP address of "your localhost" where MtApi terminal is running #MTSERV = '0.0.0.0' # IP address of "ALL on local net" where MtApi terminal is running MTPORT = 8222 # Port setting of your MT4 EA [Default:8222] #MTPORT = 8228 # Port setting of your MT5 EA [Default:8228] MTSYMB = 'EURUSD.e' # #------------------------------------------------ # mt.ENUM_TIMEFRAMES.PERIOD_M15 #------------------------------------------------ t1 = '[M1,M5,M15,M30,H1,H4,D1,W1,MN1]' # Standard Periods t2 = '[M2,M3,M4,M6,M10,M12,M20,H2,H3,H6,H8,H12]' # Extended Periods (MQL/API only) p1 = [1,5,15,30,60,240,1440,10080,43200] # [min] p2 = [2,3,4,6,10,12,20,120,180,360,480,720] # [min] z1 = t1.translate(str.maketrans('','','[]')).split(',') # Remove "[" and "]" z2 = t2.translate(str.maketrans('','','[]')).split(',') # d1 = dict(zip(z1,p1)) # convert to dict d2 = dict(zip(z2,p2)) # convert to dict d3 = {**d1,**d2} # Add the 2 dicts (in Py3.9+ use: d3=d1|d2) d4 = {k: v for k, v in sorted(d3.items(), key=lambda item: item[1])} # sort dict # "reverse" the dict key:value pairs, so we can use # get('value') to obtain the key of the original #d4r = dict(zip(d4.values(),d4.keys())) #TF = d4.get('M15') # Get the key, given a value #TFk = [k for k in d4 if (d4[k] == TF)][0] #TFk = next((k for k in d4 if d4[k] == TF), None) #------------------------------------------------ # Text Coloring #------------------------------------------------ # Usage: print(yellow("This is yellow")) def color(text, color_code): #if self.nposix: #if not is_posix(): # return text # for brighter colors, use "1;" in front of "color_code" bright = '' # '1;' return '\x1b[%s%sm%s\x1b[0m' % (bright, color_code, text) def red(text): return color(text, 31) # # noqa def green(text): return color(text, 32) # '1;49;32' # noqa def bgreen(text): return color(text, '1;49;32') # bright green # noqa def orange(text): return color(text, '0;49;91') # 31 - looks bad! # noqa def yellow(text): return color(text, 33) # # noqa def blue(text): return color(text, '1;49;34') # bright blue # noqa def purple(text): return color(text, 35) # aka. magenta # noqa def cyan(text): return color(text, '0;49;96') # 36 # noqa def white(text): return color(text, '0;49;97') # bright white # noqa #------------------------------------------------ # Print Usage #------------------------------------------------ def usage(): myName = os.path.basename(__file__) print('\n Usage: ./{}\n'.format(myName)) print(' This connects to an MT4 EA via a TCP port (or pipe) to receive OHLCV data.\n') # [:cdhprvn:s:t:] print(' ','-'*80) print(' Command Line Options:') # print(' -5 : Use script for MT5 installs (this will use MT5 DLL\'s)') # print(' -c : Enable ANSI color markup of output') print(' -d : Enable extra debug info') #print(' -e : Show candle data as minimal CSV list (ignores -l)') # ??? print(' -n : Show number of candles back') print(' -p : Use a "Named Pipe" connection (instead of a TCP IP host:port) ') print(' -l : Show candle data as a list (one line per OHLC item)') print(' -s : Set symbol name to ["EURUSD"]') print(' -t : Set timeframe to [M1,M5,M15*,M30,H1,H4,D1,W1,MN1]**') print(' -h, --help : Show THIS help list') print(' -v, --version : Show THIS program version') print(' ','-'*80) print(' * = a default setting') print(' ** = The standard MT4 TF\'s are:') print(' [M1,M5,M15,M30,H1,H4,D1,W1,MN1]') print(' [1,5,15,30,60,240,1440,10080,43200]\n') print(' The non-standard TF\'s are: (not yet available)') print(' [M2,M3,M4,M6,M10,M12,M20,H2,H3,H6,H8,H12]') print(' [2,3,4,6,10,12,20,120,180,360,480,720] ') print(' ','-'*80) print('\n Example for Windows:') #print(' python.exe .\\{} -n4 -s "CADCHF.x" -t 240 -p -d\n'.format(myName)) print(' python.exe .\\{} -n4 -s "CADCHF.x" -t H4 -p -d\n'.format(myName)) print(' Please file any bug reports at:') print(' https://github.com/eabase/mt4pycon/\n') print(' For support of the MT4/5 API, see:') print(' https://github.com/vdemydiuk/mtapi/\n') print(' Version: {}'.format(__version__)) print(' License: {}\n'.format(__copyright__)) sys.exit(2) #---------------------------------------------------------- # Helper Functions #---------------------------------------------------------- # Test if a given TF argument is a integer def isArgInt(a) : try: int(a) return True except ValueError: return False def printTFerr(a,b) : print(" ERROR: Bad TF argument: {} reverting to default: {} min.".format(a,b)) #---------------------------------------------------------- # Description: # Get history data of MqlRates structure of a specified symbol-period # in specified quantity into the ratesArray array. The elements ordering # of the copied data is from present to the past, i.e., starting # position of 0 means the current bar. # # NOTE: # # The CopyRates() implementations are different between MT4 and MT5. # # [1] https://docs.mql4.com/series/copyrates # [2] https://www.mql5.com/en/docs/series/copyrates # # MT5: int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, int startPos, int count, out MqlRates[] ratesArray) # MT4: List CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, int startPos, int count) #---------------------------------------------------------- def getCopyRates(count,SYM,TF,useList,useCSV): startPos = 1 # Starting position of candle (backward counting up!) #count = 3 # Number of candles requested (to be received into Rate Array) rA = [] # The "Rate Array" for Receiving results ccnt = 0 # Number of candles actually recived (into array) errn = 0 # The GetLastError "code" (supposedly?) try: # Check how many candles are available to download: #cc = c.iBars(SYM,TF) # if cc < count : print(' INFO: CopyRates: Not all candles are available!'); #ccnt = client.CopyRates(SYM, TF, startPos, count, rA) # MT5: int rA = client.CopyRates(SYM, TF, startPos, count) # MT4: List ccnt = len(rA) # Rate Array length except Exception as e: print('\n ERROR: in CopyRates(): \n {}\n'.format(e)) if debug : print(' DBG: CopyRates:ccnt = ', ccnt) errn = client.GetLastError() errd = client.ErrorDescription(errn) if errn else ''; print(' DBG: CopyRates: ErrorCode : {:d}'.format(errn)) print(' DBG: CopyRates: ErrorDescription :\n {}'.format(errd)) exit() errn = client.GetLastError(); #TFk = next((k for k in d4 if d4[k] == TF), None) TFk = [k for k in d1 if (d1[k] == TF)][0] #if debug : print() print(' ','-'*60) print(' INFO: CopyRates: Symbol : {}'.format(SYM)) print(' INFO: CopyRates: TimeFrame : {}'.format(TFk)) print(' INFO: CopyRates: Requested : {:d} candles'.format(count)) print(' INFO: CopyRates: Received : {:d} candles'.format(ccnt)) print(' INFO: CopyRates: ErrorCode : {:d}'.format(errn)) # rA[0].ErrorCode)) print(' ','-'*60) print(' INFO: CopyRates: Rate Array (rA):\n') #-------------------------------------------- # Response = {"ErrorCode" : 0, "Rates" : # [{ "Close" : 1.17094, # "Open" : 1.17206, # "MtTime" : 1604432700, # "Low" : 1.17079, # "High" : 1.17207, # "TickVolume" : 1251, # "RealVolume" : 0, # "Spread" : 0}, ... #-------------------------------------------- # We skip: rA[i].Spread (see notes) #-------------------------------------------- if useCSV : csv_th = 'Time,Open,High,Low,Close,Volume' csv_tf = '{},{:.5f},{:.5f},{:.5f},{:.5f},{:d}' print(csv_th) for i in range(ccnt): print(csv_tf.format(rA[i].Time, rA[i].Open, rA[i].High, rA[i].Low, rA[i].Close, rA[i].TickVolume)) sys.exit(2) if useList : myLformat = '[{}]: {}\nO: {}\nH: {}\nL: {}\nC: {}\nV: {:d}\n' # S: {:.2f} for i in range(ccnt): print(myLformat.format(i, rA[i].Time, rA[i].Open, rA[i].High, rA[i].Low, rA[i].Close, rA[i].TickVolume)) else : tableHeader = ' {:<4} {:<19} {:<7} {:<7} {:<7} {:<7} {:<6}' header_str = tableHeader.format('#','Time','Open', 'High', 'Low', 'Close', 'Volume') # 0: 2020-11-04 01:45:00 1.17531 1.17616 1.17500 1.17613 590 hlen = len(header_str) print(header_str) print(' ','-'*hlen) myRformat = ' {:<3}: {} {:.5f} {:.5f} {:.5f} {:.5f} {:d}' for i in range(ccnt): print(myRformat.format(i, rA[i].Time, rA[i].Open, rA[i].High, rA[i].Low, rA[i].Close, rA[i].TickVolume)) #---------------------------------------------------------- # ToDo #---------------------------------------------------------- #def getLiveTicks(): # Here be dragons # SymbolInfoTick # MqlTick tick; # if (!SymbolInfoTick(symbol, tick)) { # response = CreateErrorResponse(GetLastError(), "SymbolInfoDouble failed"); # return; # } #SymbolInfoTick #---------------------------------------------------------- # MAIN #---------------------------------------------------------- if __name__ == '__main__': #def main(self): #---------------------------------------------------------- # CLI arguments #---------------------------------------------------------- dTF = d1.get('M15') nmax = 3 # Default number of candles mtVer = 4 # Default to MT4 API version [4,5] aSym = str(MTSYMB) # Default Symbol() aTF = dTF # Default TimeFrame [Default: M15] apipe = 0 # Use a Named Pipe port for connection (instead of TCP host:port) alist = 0 # useCSV = 0 # ... CSV narg = len(sys.argv) - 1 try: opts, args = getopt.getopt(sys.argv[1:], ":cdehpvln:s:t:", ["help", "version"]) except getopt.GetoptError : usage(); sys.exit(); if not opts : if not args : usage(); sys.exit(); else : for opt, arg in opts: if (debug) : print ("opt: ", opt) print ("arg: ", arg) if opt in ("-h", "--help"): usage(); sys.exit(); elif opt in ("-v", "--version"): print ("Version: ", __version__); sys.exit(); #elif opt == "-c": useColors=1; # Use colored markup on: (Time, Volume, OHCL) elif opt == "-d": debug = 1; # elif opt == "-e": useCSV = 1; # print OHLCV data as CSV elif opt == "-l": alist = 1; # print each OHLCV on new line elif opt == "-p": apipe = 1; # use named pipe connection elif opt == "-n": nmax = int(arg); # Number of received candles elif opt == "-s": aSym = str(arg); # Instrument Symbol elif opt == "-t": # if (isArgInt(arg)) : # if int(arg) in d1.values() : # check if '30' in d1 aTF = int(arg) # convert '30' to 30 else : printTFerr(arg,dTF) continue else: if arg in d1.keys() : # check if 'M30' in d1 aTF = d1.get(arg) # get 30 from 'M30' else : printTFerr(arg,dTF) continue #---------------------------------------------------------- #client = mt.MtApi5Client() client = mt.MtApiClient() ip = str(MTSERV) # Host IP port = MTPORT # EA port sleeps = 10 # Sleep this many times cnt = 1 # print('\n INFO: Attmpting to Connect to: {}:{}'.format(ip,port)) if (debug): print(' DBG : using {}'.format('a name pipe' if apipe else 'TCP IP')) try: while sleeps > 0: print(' [{}] Connecting '.format(cnt), end='') if (apipe == 0) : client.BeginConnect(ip, port) else : client.BeginConnect(port) time.sleep(1) sleeps = sleeps - 1 cnt += 1 #------------------------------------ # Check Connection state: #------------------------------------ # 0 = Disconnected # 1 = Connecting # 2 = Connected # 3 = Failed #------------------------------------ if client.ConnectionState == 0: print('Disconnected'); continue; if client.ConnectionState == 1: print('.', end=''); continue; if client.ConnectionState == 2: print('OK'); getCopyRates(nmax,aSym,aTF,alist,useCSV); break if client.ConnectionState == 3: print('FAILED'); break if (debug): print(' DBG: Unknown ConnectionState: ', client.ConnectionState) except Exception as e: print(e) client.BeginDisconnect() if (debug): print('\n DBG: Disconnected!') print('\n Done.') #---------------------------------------------------------- # END #---------------------------------------------------------- #if __name__ == "__main__": # main() # sys.exit(0)