Working with GSM Modem from an MQL5 Expert Advisor
Introduction
There is currently a fair number of means for a comfortable remote monitoring of a trading account: mobile terminals, push notifications, working with ICQ. But it all requires Internet connection. This article describes the process of creating an Expert Advisor that will allow you to stay in touch with your trading terminal even when mobile Internet is not available, through calls and text messaging. In addition, this Expert Advisor will be able to notify you of the lost or reestablished connection with the trade server.
For this purpose, virtually any GSM modem, as well as most phones with the modem function would do. For illustration, I have chosen Huawei E1550, as this modem is one of the most widely used devices of its kind. Further, at the end of the article, we will try to replace the modem with an old cellphone Siemens M55 (released in 2003) and see what happens.
But first, a few words on how to send a byte of data from an Expert Advisor to a modem.
1. Working with COM Port
Upon connecting the modem to your computer and installing all the necessary drivers, you will be able to see a virtual COM port in the system. All future operations with the modem are performed via this port. Consequently, in order to exchange data with the modem, you should first get access to the COM port.
Fig. 1. Huawei modem is connected to the COM3 port
Here, we will need a DLL library TrComPort.dll which is freely distributed in the Internet together with the source files. It will be used to configure the COM port, query its state, as well as to receive and send data. In order to get that done, we will use the following functions:
#import "TrComPort.dll" int TrComPortOpen(int portnum); int TrComPortClose(int portid); int TrComPortSetConfig(int portid, TrComPortParameters& parameters); int TrComPortGetConfig(int portid, TrComPortParameters& parameters); int TrComPortWriteArray(int portid, uchar& buffer[], uint length, int timeout); int TrComPortReadArray(int portid, uchar& buffer[], uint length, int timeout); int TrComPortGetQueue(int portid, uint& input_queue, uint& output_queue); #import
Transmitted data types had to be slightly modified for compatibility with MQL5.
The TrComPortParameters structure is as follows:
struct TrComPortParameters { uint DesiredParams; int BaudRate; // Data rate int DefaultTimeout; // Default timeout (in milliseconds) uchar ByteSize; // Data size(4-8) uchar StopBits; // Number of stop bits uchar CheckParity; // Parity check(0-no,1-yes) uchar Parity; // Parity type uchar RtsControl; // Initial RTS state uchar DtrControl; // Initial DTR state };
Most devices work with the following settings: 8 data bits, no parity check, 1 stop bit. Therefore, out of all COM port parameters it makes sense to only add to the parameters of the Expert Advisor the COM port number and the data rate:
input ComPortList inp_com_port_index=COM3; // Choosing the COM port input BaudRateList inp_com_baudrate=_9600bps; // Data rate
The COM port initialization function will then be as follows:
//+------------------------------------------------------------------+ //| COM port initialization | //+------------------------------------------------------------------+ bool InitComPort() { rx_cnt=0; tx_cnt=0; tx_err=0; //--- attempt to open the port PortID=TrComPortOpen(inp_com_port_index); if(PortID!=inp_com_port_index) { Print("Error when opening the COM port"+DoubleToString(inp_com_port_index+1,0)); return(false); } else { Print("The COM port"+DoubleToString(inp_com_port_index+1,0)+" opened successfully"); //--- request all parameters, so set all flags com_par.DesiredParams=tcpmpBaudRate|tcpmpDefaultTimeout|tcpmpByteSize|tcpmpStopBits|tcpmpCheckParity|tcpmpParity|tcpmpEnableRtsControl|tcpmpEnableDtrControl; //--- read current parameters if(TrComPortGetConfig(PortID,com_par)==-1) ,bnreturn(false);//read error // com_par.ByteSize=8; //8 bits com_par.Parity=0; //no parity check com_par.StopBits=0; //1 stop bit com_par.DefaultTimeout=100; //100 ms timeout com_par.BaudRate=inp_com_baudrate; //rate - from the parameters of the Expert Advisor //--- if(TrComPortSetConfig(PortID,com_par)==-1) return(false);//write error } return(true); }
In case of successful initialization the PortID variable will store the identifier of the open COM port.
It should be noted here that identifiers are numbered from zero, so the COM3 port identifier will be equal to 2. Now that the port is open we can exchange data with the modem. And, by the way, not only with a modem. The access to the COM port from the Expert Advisor opens up great opportunities for creativity to those who is good at soldering: you can connect the Expert Advisor to an LED or a moving text display to show equity or market prices of certain currency pairs.
The TrComPortGetQueue function shall be used to get details of the data in the queue of the COM port receiver and transmitter:
int TrComPortGetQueue( int portid, // COM port identifier uint& input_queue, // Number of bytes in the input buffer uint& output_queue // Number of bytes in the output buffer );
In case of an error, it returns a negative value of the error code. A detailed description of error codes is available in the archive with the TrComPort.dll library source codes.
If the function returns a non-zero number of data in the receiving buffer, they need to be read. For this purpose, we use the TrComPortReadArray function:
int TrComPortReadArray( int portid, // Port identifier uchar& buffer[], // Pointer to the buffer to read uint length, // Number of data bytes int timeout // Execution timeout (in ms) );
In case of an error, it returns a negative value of the error code. The number of data bytes should correspond to the value returned by the TrComPortGetQueue function.
To use the default timeout (set at the COM port initialization), you need to pass the value of -1.
To transmit data to the COM port, we use the TrComPortWriteArray function:
int TrComPortWriteArray( int portid, // Port identifier uchar& buffer[], // Pointer to the initial buffer uint length, // Number of data bytes int timeout // Execution timeout (in ms) );
Application example. In reply to the message saying "Hello world!", we should send "Have a nice day!".
uchar rx_buf[1024]; uchar tx_buf[1024]; string rx_str; int rxn, txn; TrComPortGetQueue(PortID, rxn, txn); if(rxn>0) { //--- data received in the receiving buffer //--- read TrComPortReadArray(PortID, rx_buf, rxn, -1); //--- convert to a string rx_str = CharArrayToString(rx_buf,0,rxn,CP_ACP); //--- check the received message (expected message "Hello world!" if(StringFind(rx_str,"Hello world!",0)!=-1) {//--- if we have a match, prepare the reply string tx_str = "Have a nice day!"; int len = StringLen(tx_str);//get the length in characters //--- convert to uchar buffer StringToCharArray(tx_str, tx_buf, 0, len, CP_ACP); //--- send to the port if(TrComPortWriteArray(PortID, tx_buf, len, -1)<0) Print("Error when writing to the port"); } }
Special attention should be paid to the port closing function:
int TrComPortClose( int portid // Port identifier );
This function must always be present in the Expert Advisor deinitialization process. In most cases the port that was left open will become available again only after restarting the system. In fact, even turning the modem off and on may not help.
2. AT Commands and Working with Modem
Working with a modem is arranged using АТ commands. Those of you who have ever used mobile Internet from a computer must remember the so-called "modem initialization string", which roughly looks as follows: AT+CGDCONT=1,"IP","internet". This is one of the AT commands. Almost all of them start with the prefix AT and end with 0x0d (carriage return).
We will use the minimum set of AT commands required for the implementation of the desired functionality. This will reduce the effort for ensuring compatibility of the command set with various devices.
Below is the list of the AT commands used by our handler for working with the modem:
Command | Description |
---|---|
ATE1 | Enable echo |
AT+CGMI | Get the name of manufacturer |
AT+CGMM | Get the model of device |
AT^SCKS | Get SIM card status |
AT^SYSINFO | Get system information |
AT+CREG | Get network registration state |
AT+COPS | Get the name of the current mobile operator |
AT+CMGF | Switch between text/PDU modes |
AT+CLIP | Enable calling line identification |
AT+CPAS | Get modem status |
AT+CSQ | Get signal quality |
AT+CUSD | Send a USSD request |
AT+CALM | Enable silent mode (applicable to phones) |
AT+CBC | Get battery status (applicable to phones) |
AT+CSCA | Get SMS service center number |
AT+CMGL | Get list of SMS messages |
AT+CPMS | Select memory for SMS messages |
AT+CMGD | Delete SMS message from the memory |
AT+CMGR | Read SMS message from the memory |
AT+CHUP | Reject incoming call |
AT+CMGS | Send a SMS message |
I am not going to get off topic, describing the subtleties of working with AT commands. There is plenty of relevant information on technical forums. Besides, everything has already been implemented and in order to create an Expert Advisor capable of working with a modem, all we need is to include a header file and start using ready-made functions and structures. This is what I am going to elaborate on.
2.1. Functions
COM port initialization:
bool InitComPort();
Returned value: if initialized successfully - true, otherwise - false. It is called from the OnInit() function before the modem initialization.
COM port deinitialization:
void DeinitComPort();
Returned value: none. It is called from the OnDeinit() function.
Modem initialization:
void InitModem();
Returned value: none. It is called from the OnInit() function following a successful initialization of the COM port.
Modem event handler:
void ModemTimerProc();
Returned value: none. It is called from the OnTimer() function at 1-second intervals.
Reading SMS message by index from the modem memory:
bool ReadSMSbyIndex( int index, // SMS message index in the modem memory INCOMING_SMS_STR& sms // Pointer to the structure where the message will be moved );
Returned value: if read successfully - true, otherwise - false.
Deleting SMS message by index from the modem memory:
bool DelSMSbyIndex( int index // SMS message index in the modem memory );
Returned value: if deleted successfully - true, otherwise - false.
Conversion of the connection quality index to a string:
string rssi_to_str( int rssi // Connection quality index, values 0..31, 99 );
Returned value: a string, e.g. "-55 dBm".
Sending SMS message:
bool SendSMS( string da, // Recipient's phone number in international format string text, // Text of the message, Latin characters and numbers, maximum length - 158 characters bool flash // Flash message flag );
Returned value: if sent successfully - true, otherwise - false. SMS messages can only be sent when written using Latin characters. Cyrillic characters are supported for incoming SMS messages only. If flash=true is set, a flash message will be sent.
2.2. Events (functions called by the modem handler)
Updating data in the modem state structure:
void ModemChState();
Passed parameters: none. When this function is called by the modem handler, it suggests that data has been updated in the modem structure (the description of the structure will be provided below).
Incoming call:
void IncomingCall( string number // Caller number );
Passed parameters: caller number. When this function is called by the modem handler, it suggests that the incoming call from the 'number' number was accepted and rejected.
New incoming SMS message:
void IncomingSMS( INCOMING_SMS_STR& sms // SMS message structure );
Passed parameters: SMS message structure (the description of the structure will be provided below). When this function is called by the modem handler, it suggests that there is one or more new unread SMS messages in the modem memory. If the number of unread messages is greater than one, the most recent message will be passed to this function.
SMS memory full:
void SMSMemoryFull( int n // Number of SMS messages in the modem memory );
Passed parameters: number of messages in the modem memory. When this function is called by the modem handler, it suggests that SMS memory is full and the modem will not accept any new messages until the memory is released.
2.3. State structure of modem parameters
struct MODEM_STR { bool init_ok; // The required minimum initialized // string manufacturer; // Manufacturer string device; // Model int sim_stat; // SIM status int net_reg; // Network registration state int status; // Modem status string op; // Operator int rssi; // Signal quality string sms_sca; // SMS center number int bat_stat; // Battery state int bat_charge; // Battery charge in percent (applicability depends on bat_stat) // double bal; // Mobile account balance string exp_date; // Mobile number expiration date int sms_free; // Package SMS available int sms_free_cnt; // Counter of package SMS used // int sms_mem_size; // SMS memory size int sms_mem_used; // Used SMS memory size // string incoming; // Caller number }; MODEM_STR modem;
This structure is filled exclusively by the modem event handler and should be used by other functions only for reading.
Below is the description of structure elements:
Element | Description |
---|---|
modem.init_ok | An indication that the modem has been initialized successfully. The initial value of false becomes true after the initialization is complete. |
modem.manufacturer | Modem manufacturer, e.g. "huawei". The initial value is "n/a". |
modem.device | Modem model, e.g. "E1550" The initial value is "n/a". |
modem.sim_stat | Sim card status. It can take on the following values: -1 - no data 0 - the card is missing, blocked or out of order 1 - the card is available |
modem.net_reg | Network registration state. It can take on the following values: -1 - no data 0 - not registered 1 - registered 2 - searching 3 - forbidden 4 - state undefined 5 - registered in roaming |
modem.status | Modem status. It can take on the following values: -1 - initialization 0 - ready 1 - error 2 - error 3 - incoming call 4 - active call |
modem.op | Current mobile operator. It can be equal to either the operator name (e.g. "MTS UKR"), or the international operator code (e.g. "25501"). The initial value is "n/a". |
modem.rssi | Signal quality index. It can take on the following values: -1 - no data 0 - signal -113 dBm or lower 1 - signal -111 dBm 2...30 - signal -109...-53 dBm 31 - signal -51 dBm or higher 99 - no data For conversion to a string, use the rssi_to_str() function. |
modem.sms_sca | SMS service center number. It is contained in the SIM card memory. It is necessary for generating an outgoing SMS message. In rare cases, if the number is not saved in the SIM card memory, it will be replaced with the number specified in the input parameters of the Expert Advisor. |
modem.bat_stat | Modem battery status (applicable to phones only). It can take on the following values: -1 - no data 0 - the device works on battery power 1 - the battery is available but the device is not battery powered 2 - no battery 3 - error |
modem.bat_charge | Battery charge in percent. It can take on values from 0 to 100. |
modem.bal | Mobile account balance. The value is obtained from the operator response to the relevant USSD request. The initial value (prior to initialization): -10000. |
modem.exp_date | Mobile number expiration date. The value is obtained from the operator response to the relevant USSD request. The initial value is "n/a". |
modem.sms_free | Number of package SMS available. It is calculated as the difference between the initial number and the counter of package SMS used. |
modem.sms_free_cnt | Counter of package SMS used. The value is obtained from the operator response to the relevant USSD request. The initial value is -1. |
modem.sms_mem_size | Modem SMS memory size. |
modem.sms_mem_used | Modem SMS memory used. |
modem.incoming | Number of the last caller. The initial value is "n/a". |
2.4. SMS message structure
//+------------------------------------------------------------------+ //| SMS message structure | //+------------------------------------------------------------------+ struct INCOMING_SMS_STR { int index; //index in the modem memory string sca; //sender's SMS center number string sender; //sender's number INCOMING_CTST_STR scts; //SMS center time label string text; //text of the message };
The SMS center time label is the time when a given message from the sender was received in the SMS center. The time label structure is as follows:
//+------------------------------------------------------------------+ //| Time label structure | //+------------------------------------------------------------------+ struct INCOMING_CTST_STR { datetime time; // time int gmt; // time zone };
The time zone is expressed in 15-minute intervals. So, the value of 8 corresponds to GMT+02:00.
The text of received SMS message can be written using Latin, as well as Cyrillic characters. 7-bit and UCS2 encodings are supported for received messages. Merging of long messages is not implemented (in view of the fact that this operation is designed for short commands).
SMS messages can only be sent if written using Latin characters. Maximum message length is 158 characters. In case of a longer message, it will be sent without the characters in excess of the specified number.
3. Developing an Expert Advisor
For a start, you need to copy the file TrComPort.dll to the Libraries folder and place ComPort.mqh, modem.mqh and sms.mqh in the Include folder.
Then using the Wizard, we create a new Expert Advisor and add the minimum required for working with the modem. That is:
Include modem.mqh:
#include <modem.mqh>
Add input parameters:
input string str00="COM port settings"; input ComPortList inp_com_port_index=COM3; // Selecting the COM port input BaudRateList inp_com_baudrate=_9600bps; // Data rate // input string str01="Modem"; input int inp_refr_period=3; // Modem query period, sec input int inp_ussd_request_tout=20; // Timeout for response to a USSD request, sec input string inp_sms_service_center=""; // SMS service center number // input string str02="Balance"; input int inp_refr_bal_period=12; // Query period, hr input string inp_ussd_get_balance=""; // Balance USSD request input string inp_ussd_bal_suffix=""; // Balance suffix input string inp_ussd_exp_prefix=""; // Prefix of the number expiration date // input string str03="Number of package SMS"; input int inp_refr_smscnt_period=6; // Query period, hr input string inp_ussd_get_sms_cnt=""; // USSD request for the package service status input string inp_ussd_sms_suffix=""; // SMS counter suffix input int inp_free_sms_daily=0; // Daily SMS limit
Functions called by the modem handler:
//+------------------------------------------------------------------+ //| Called when a new SMS message is received | //+------------------------------------------------------------------+ void IncomingSMS(INCOMING_SMS_STR& sms) { } //+------------------------------------------------------------------+ //| SMS memory is full | //+------------------------------------------------------------------+ void SMSMemoryFull(int n) { } //+------------------------------------------------------------------+ //| Called upon receiving an incoming call | //+------------------------------------------------------------------+ void IncomingCall(string number) { } //+------------------------------------------------------------------+ //| Called after updating data in the modem status structure | //+------------------------------------------------------------------+ void ModemChState() { static bool init_ok = false; if(modem.init_ok==true && init_ok==false) { Print("Modem initialized successfully"); init_ok = true; } }
The COM port and modem initialization along with a timer set at 1 second intervals should be added to the OnInit() function:
int OnInit() { //---COM port initialization if(InitComPort()==false) { Print("Error when initializing the COM"+DoubleToString(inp_com_port_index+1,0)+" port"); return(INIT_FAILED); } //--- modem initialization InitModem(); //--- setting the timer EventSetTimer(1); //1 second interval // return(INIT_SUCCEEDED); }
In the OnTimer() function, we need to call the modem handler:
void OnTimer() { //--- ModemTimerProc(); }
It is necessary to call the COM port deinitialization in the OnDeinit() function:
void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); DeinitComPort(); }
We compile the code and see: 0 error(s).
Now run the Expert Advisor, but remember to allow DLL import and select the COM port associated with the modem. You should be able to see the following messages in the "Expert Advisors" tab:
Fig. 2. Messages of the Expert Advisor following a successful run
If you have got the same messages, it means that your modem (phone) is suitable for working with this Expert Advisor. In this case we move further.
Let's draw a table for visualization of modem parameters. It will be placed in the upper left corner of the terminal window, under the OHLC line. The text font to be used in the table will be monospaced, e.g. "Courier New".
//+------------------------------------------------------------------+ //| TextXY | //+------------------------------------------------------------------+ void TextXY(string ObjName,string Text,int x,int y,color TextColor) { //--- display the text string ObjectDelete(0,ObjName); ObjectCreate(0,ObjName,OBJ_LABEL,0,0,0,0,0); ObjectSetInteger(0,ObjName,OBJPROP_XDISTANCE,x); ObjectSetInteger(0,ObjName,OBJPROP_YDISTANCE,y); ObjectSetInteger(0,ObjName,OBJPROP_COLOR,TextColor); ObjectSetInteger(0,ObjName,OBJPROP_FONTSIZE,9); ObjectSetString(0,ObjName,OBJPROP_FONT,"Courier New"); ObjectSetString(0,ObjName,OBJPROP_TEXT,Text); } //+------------------------------------------------------------------+ //| Drawing the table of modem parameters | //+------------------------------------------------------------------+ void DrawTab() { int x=20, //horizontal indent y = 20, //vertical indent dy = 15; //step along the Y-axis //--- draw the background ObjectDelete(0,"bgnd000"); ObjectCreate(0,"bgnd000",OBJ_RECTANGLE_LABEL,0,0,0,0,0); ObjectSetInteger(0,"bgnd000",OBJPROP_XDISTANCE,x-10); ObjectSetInteger(0,"bgnd000",OBJPROP_YDISTANCE,y-5); ObjectSetInteger(0,"bgnd000",OBJPROP_XSIZE,270); ObjectSetInteger(0,"bgnd000",OBJPROP_YSIZE,420); ObjectSetInteger(0,"bgnd000",OBJPROP_BGCOLOR,clrBlack); //--- port parameters TextXY("str0", "Port: ", x, y, clrWhite); y+=dy; TextXY("str1", "Speed: ", x, y, clrWhite); y+=dy; TextXY("str2", "Rx: ", x, y, clrWhite); y+=dy; TextXY("str3", "Tx: ", x, y, clrWhite); y+=dy; TextXY("str4", "Err: ", x, y, clrWhite); y+=(dy*3)/2; //--- modem parameters TextXY("str5", "Modem: ", x, y, clrWhite); y+=dy; TextXY("str6", "SIM: ", x, y, clrWhite); y+=dy; TextXY("str7", "NET: ", x, y, clrWhite); y+=dy; TextXY("str8", "Operator: ", x, y, clrWhite); y+=dy; TextXY("str9", "SMSC: ", x, y, clrWhite); y+=dy; TextXY("str10", "RSSI: ", x, y, clrWhite); y+=dy; TextXY("str11", "Bat: ", x, y, clrWhite); y+=dy; TextXY("str12", "Modem status: ", x, y, clrWhite); y+=(dy*3)/2; //--- mobile account balance TextXY("str13", "Balance: ", x, y, clrWhite); y+=dy; TextXY("str14", "Expiration date: ", x, y, clrWhite); y+=dy; TextXY("str15", "Free SMS: ", x, y, clrWhite); y+=(dy*3)/2; //--- number of the last incoming call TextXY("str16","Incoming: ",x,y,clrWhite); y+=(dy*3)/2; //--- parameters of the last received SMS message TextXY("str17", "SMS mem full: ", x, y, clrWhite); y+=dy; TextXY("str18", "SMS number: ", x, y, clrWhite); y+=dy; TextXY("str19", "SMS date/time: ", x, y, clrWhite); y+=dy; //--- text of the last received SMS message TextXY("str20", " ", x, y, clrGray); y+=dy; TextXY("str21", " ", x, y, clrGray); y+=dy; TextXY("str22", " ", x, y, clrGray); y+=dy; TextXY("str23", " ", x, y, clrGray); y+=dy; TextXY("str24", " ", x, y, clrGray); y+=dy; //--- ChartRedraw(0); }
To refresh data in the table, we will use the RefreshTab() function:
//+------------------------------------------------------------------+ //| Refreshing values in the table | //+------------------------------------------------------------------+ void RefreshTab() { string str; //--- COM port index: str="COM"+DoubleToString(PortID+1,0); ObjectSetString(0,"str0",OBJPROP_TEXT,"Port: "+str); //--- data rate: str=DoubleToString(inp_com_baudrate,0)+" bps"; ObjectSetString(0,"str1",OBJPROP_TEXT,"Speed: "+str); //--- number of bytes received: str=DoubleToString(rx_cnt,0)+" bytes"; ObjectSetString(0,"str2",OBJPROP_TEXT,"Rx: "+str); //--- number of bytes transmitted: str=DoubleToString(tx_cnt,0)+" bytes"; ObjectSetString(0,"str3",OBJPROP_TEXT,"Tx: "+str); //--- number of port errors: str=DoubleToString(tx_err,0); ObjectSetString(0,"str4",OBJPROP_TEXT,"Err: "+str); //--- modem manufacturer and model: str=modem.manufacturer+" "+modem.device; ObjectSetString(0,"str5",OBJPROP_TEXT,"Modem: "+str); //--- SIM card status: string sim_stat_str[2]={"Error","Ok"}; if(modem.sim_stat==-1) str="n/a"; else str=sim_stat_str[modem.sim_stat]; ObjectSetString(0,"str6",OBJPROP_TEXT,"SIM: "+str); //--- network registration: string net_reg_str[6]={"No","Ok","Search...","Restricted","Unknown","Roaming"}; if(modem.net_reg==-1) str="n/a"; else str=net_reg_str[modem.net_reg]; ObjectSetString(0,"str7",OBJPROP_TEXT,"NET: "+str); //--- name of mobile operator: ObjectSetString(0,"str8",OBJPROP_TEXT,"Operator: "+modem.op); //--- SMS service center number ObjectSetString(0,"str9",OBJPROP_TEXT,"SMSC: "+modem.sms_sca); //--- signal level: if(modem.rssi==-1) str="n/a"; else str=rssi_to_str(modem.rssi); ObjectSetString(0,"str10",OBJPROP_TEXT,"RSSI: "+str); //--- battery status (applicable to phones): string bat_stats_str[4]={"Ok, ","Ok, ","No","Err"}; if(modem.bat_stat==-1) str="n/a"; else str=bat_stats_str[modem.bat_stat]; if(modem.bat_stat==0 || modem.bat_stat==1) str+=DoubleToString(modem.bat_charge,0)+"%"; ObjectSetString(0,"str11",OBJPROP_TEXT,"Bat: "+str); //--- modem status: string modem_stat_str[5]={"Ready","Err","Err","Incoming call","Active call"}; if(modem.status==-1) str="init..."; else { if(modem.status>4 || modem.status<0) Print("Unknown modem status: "+DoubleToString(modem.status,0)); else str=modem_stat_str[modem.status]; } ObjectSetString(0,"str12",OBJPROP_TEXT,"Modem status: "+str); //--- mobile account balance: if(modem.bal==-10000) str="n/a"; else str=DoubleToString(modem.bal,2)+" "+inp_ussd_bal_suffix; ObjectSetString(0,"str13",OBJPROP_TEXT,"Balance: "+str); //--- mobile number expiration date: ObjectSetString(0,"str14",OBJPROP_TEXT,"Expiration date: "+modem.exp_date); //--- package SMS available: if(modem.sms_free<0) str="n/a"; else str=DoubleToString(modem.sms_free,0); ObjectSetString(0,"str15",OBJPROP_TEXT,"Free SMS: "+str); //--- SMS memory full: if(sms_mem_full==true) str="Yes"; else str="No"; ObjectSetString(0,"str17",OBJPROP_TEXT,"SMS mem full: "+str); //--- ChartRedraw(0); }
The DelTab() function deletes the table:
//+------------------------------------------------------------------+ //| Deleting the table | //+------------------------------------------------------------------+ void DelTab() { for(int i=0; i<25; i++) ObjectDelete(0,"str"+DoubleToString(i,0)); ObjectDelete(0,"bgnd000"); }
Let's add functions for working with the table to event handlers OnInit() and OnDeinit(), as well as to the ModemChState() function:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- COM port initialization if(InitComPort()==false) { Print("Error when initializing the COM port"+DoubleToString(inp_com_port_index+1,0)); return(INIT_FAILED); } //--- DrawTab(); //--- modem initialization InitModem(); //--- setting the timer EventSetTimer(1);//1 second interval //--- RefreshTab(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy timer EventKillTimer(); DeinitComPort(); DelTab(); } //+------------------------------------------------------------------+ //| ModemChState | //+------------------------------------------------------------------+ void ModemChState() { static bool init_ok=false; //Print("Modem status changed"); if(modem.init_ok==true && init_ok==false) { Print("Modem initialized successfully"); init_ok=true; } //--- RefreshTab(); }
Further, we add the opportunity to refresh the number of the last incoming call in the table to the IncomingCall() function:
//+------------------------------------------------------------------+ //| Called upon receiving an incoming call | //+------------------------------------------------------------------+ void IncomingCall(string number) { //--- update the number of the last incoming call: ObjectSetString(0, "str16",OBJPROP_TEXT, "Incoming: "+number); }
Now, compile the code and run the Expert Advisor. You should be able to see the following report in the terminal window:
Fig. 3. Modem parameters
Try calling the modem. The call will be rejected and your number will appear in the "Incoming" line.
4. Working with USSD Requests
A mobile account not topped up in due time may disrupt the operation of an Expert Advisor at the least appropriate moment. So, the function that checks the account balance is one of the most important ones. To check the mobile account balance we usually use USSD requests. Further, we will use USSD requests to get information on the number of package SMS available.
Data for generating requests and processing responses received are located in the input parameters:
input string str02="=== Balance ======"; input int inp_refr_bal_period=12; //query period, hr input string inp_ussd_get_balance=""; //balance USSD request input string inp_ussd_bal_suffix=""; //balance suffix input string inp_ussd_exp_prefix=""; //prefix of the number expiration date // input string str03="= Number of package SMS =="; input int inp_refr_smscnt_period=6;//query period, hr input string inp_ussd_get_sms_cnt=""; //USSD request for the package service status input string inp_ussd_sms_suffix=""; //SMS counter suffix input int inp_free_sms_daily=0; //daily SMS limit
If the request number is not specified, the request will not be processed. Alternatively, the request will be sent right after the modem initialization and will be repeatedly sent after the specified period of time. Further, the request will not be processed if your modem (phone) does not support the relevant IT command (it concerns old cellphone models).
Assume that following the balance request you receive the following response from your operator:
7.13 UAH, expires on 22.05.2014. Phone Plan - Super MTS 3D Null 25.
To ensure that the handler identifies the response correctly, the balance suffix must be set to "UAH" and the prefix of the number expiration date should be "expires on".
Since our Expert Advisor is expected to send SMS messages quite often, it would be good to buy an SMS package from your operator, that is, a service whereby you get a certain number of SMS messages for a small fee. In this case, it may be very useful to know how many package SMS are still available. This can also be done using a USSD request. The operator usually responds with the number of used SMS instead of the available ones.
Assume, you have received the following response from your operator:
Balance: 69 minutes of local calls for today. Used today: 0 SMS and 0 MB.
In this case, the SMS counter suffix shall be set to "SMS" and the daily limit shall be set in accordance with the SMS package terms and conditions. For instance, if you are given 30 text messages per day and the request returned the value of 10, it means that you have 30-10=20 SMS available. This number will be placed by the handler to the appropriate element of the modem status structure.
ATTENTION! Be very careful with USSD request numbers! Sending a wrong request can have undesirable consequences, e.g. enabling some unwanted paid service!
In order for our Expert Advisor to start working with USSD requests, we just need to specify the relevant input parameters.
For example, the parameters for the Ukrainian mobile operator, MTS Ukraine, will be as follows:
Fig. 4. Parameters of the USSD request for the available balance
Fig. 5. Parameters of the USSD request for the number of package SMS available
Set the values relevant to your mobile operator. After that, the available balance in your mobile account and the number of available SMS will be displayed in the table of modem status:
Fig. 6. Parameters obtained from USSD responses
At the time of writing this article, my mobile operator was sending Christmas advertisement instead of the number expiration date. Consequently, the handler could not obtain the date value, which is why we can see "n/a" in the "Expiration date" line. Please note that all operator responses are displayed in the "Expert Advisors" tab.
Fig. 7. Operator responses displayed in the "Expert Advisors" tab
5. Sending SMS Messages
We are going to start adding useful functions, for instance, sending SMS messages stating the current profit, equity and the number of open positions. Sending will be initiated by an incoming call.
Such response is certainly expected only in case of the administrator number, so we will have another input parameter:
input string inp_admin_number="+XXXXXXXXXXXX";//administrator's phone number
The number shall be specified in international format, including "+" before the number.
The number check, as well as SMS text generation and sending should be added to the incoming call handler:
//+------------------------------------------------------------------+ //| Called upon receiving an incoming call | //+------------------------------------------------------------------+ void IncomingCall(string number) { bool result; if(number==inp_admin_number) { Print("Administrator's phone number. Sending SMS."); // string mob_bal=""; if(modem.bal!=-10000)//mobile account balance mob_bal = "\n(m.bal="+DoubleToString(modem.bal,2)+")"; result = SendSMS(inp_admin_number, "Account: "+DoubleToString(AccountInfoInteger(ACCOUNT_LOGIN),0) +"\nProfit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2) +"\nEquity: "+DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2) +"\nPositions: "+DoubleToString(PositionsTotal(),0) +mob_bal , false); if(result==true) Print("SMS sent successfully"); else Print("Error when sending SMS"); } else Print("Unauthorized number ("+number+")"); //--- update the number of the last incoming call: ObjectSetString(0, "str16",OBJPROP_TEXT, "Incoming: "+number); }
Now, if there is a call to the modem from the administrator's number inp_admin_number, an SMS message will be sent in response:
Fig. 8. The SMS message sent by the Expert Advisor in response to the call received from the administrator's phone number
Here, we can see the current values of profit and equity, as well as the number of open positions and the mobile account balance.
6. Monitoring Connection with the Trade Server
Let's add notifications in case of lost and reestablished connection with the trade server. For this purpose, we will check trade server connectivity once every 10 seconds using TerminalInfoInteger() with the TERMINAL_CONNECTED property identifier.
To filter short-time connection losses, we will use hysteresis which should be added to the list of input parameters:
input int inp_conn_hyst=6; //Hysteresis, х10 sec
The value of 6 means that connection will be considered lost if there is no connection for more than 6*10=60 seconds. Similarly, connection will be considered reestablished if it is available for more than 60 seconds. The local time of the first registered lack of connectivity will be deemed the time of connection loss, while the first local time when the connection became available will be deemed the recovery time.
To implement this, we add the following code to the OnTimer() function:
static int s10 = 0;//pre-divider by 10 seconds static datetime conn_time; static datetime disconn_time; if(++s10>=10) {//--- once every 10 seconds s10 = 0; // if((bool)TerminalInfoInteger(TERMINAL_CONNECTED)==true) { if(cm.conn_cnt==0) //first successful query in the sequence conn_time = TimeLocal(); //save the time if(cm.conn_cnt<inp_conn_hyst) { if(++cm.conn_cnt>=inp_conn_hyst) {//--- connection has been stabilized if(cm.connected == false) {//--- if there was a long-standing connection loss prior to that cm.connected = true; cm.new_state = true; cm.conn_time = conn_time; } } } cm.disconn_cnt = 0; } else { if(cm.disconn_cnt==0) //first unsuccessful query in the sequence disconn_time = TimeLocal(); //save the time if(cm.disconn_cnt<inp_conn_hyst) { if(++cm.disconn_cnt>=inp_conn_hyst) {//--- long-standing connection loss if(cm.connected == true) {//--- if the connection was stable prior to that cm.connected = false; cm.new_state = true; cm.disconn_time = disconn_time; } } } cm.conn_cnt = 0; } } // if(cm.new_state == true) {//--- connection status changed if(cm.connected == true) {//--- connection is available string str = "Connected "+TimeToString(cm.conn_time,TIME_DATE|TIME_SECONDS); if(cm.disconn_time!=0) str+= ", offline: "+dTimeToString((ulong)(cm.conn_time-cm.disconn_time)); Print(str); SendSMS(inp_admin_number, str, false);//sending message } else {//--- no connection string str = "Disconnected "+TimeToString(cm.disconn_time,TIME_DATE|TIME_SECONDS); if(cm.conn_time!=0) str+= ", online: "+dTimeToString((ulong)(cm.disconn_time-cm.conn_time)); Print(str); SendSMS(inp_admin_number, str, false);//sending message } cm.new_state = false; }
The cm structure is as follows:
//+------------------------------------------------------------------+ //| Structure of monitoring connection with the terminal | //+------------------------------------------------------------------+ struct CONN_MON_STR { bool new_state; //flag of change in the connection status bool connected; //connection status int conn_cnt; //counter of successful connection queries int disconn_cnt; //counter of unsuccessful connection queries datetime conn_time; //time of established connection datetime disconn_time; //time of lost connection }; CONN_MON_STR cm;//structure of connection monitoring
In the text of the SMS message, we will state the time when connection with the trade server was lost (or reestablished), as well as the time during which connection was available (or not available) calculated as the difference between the time of established connection and the time of lost connection. To convert the time difference from seconds to dd hh:mm:ss, we will add the dTimeToString() function:
string dTimeToString(ulong sec) { string str; uint days = (uint)(sec/86400); if(days>0) { str+= DoubleToString(days,0)+" days, "; sec-= days*86400; } uint hour = (uint)(sec/3600); if(hour<10) str+= "0"; str+= DoubleToString(hour,0)+":"; sec-= hour*3600; uint min = (uint)(sec/60); if(min<10) str+= "0"; str+= DoubleToString(min,0)+":"; sec-= min*60; if(sec<10) str+= "0"; str+= DoubleToString(sec,0); // return(str); }
To ensure that the Expert Advisor does not send a text message about the established connection every time the Expert Advisor is run, we add a conn_mon_init() function that sets the values to cm structure elements in such a way, as if the connection were already established. In this case, the connection will be deemed established at the local time of running the Expert Advisor. This function must be called from the OnInit() function.
void conn_mon_init() { cm.connected = true; cm.conn_cnt = inp_conn_hyst; cm.disconn_cnt = 0; cm.conn_time = TimeLocal(); cm.new_state = false; }
Now, compile and run the Expert Advisor. Then try to disconnect your computer from the Internet. In 60 (give or take 10) seconds, you will receive a message saying that the connection with the server has been lost. Connect back to the Internet. In 60 seconds, you will get a message about reestablished connection, stating the total disconnection time:
Fig. 9. Text messages notifying of the lost and reestablished connection with the server
7. Sending Reports on Position Opening and Closing
To monitor the opening and closing of positions, let's add the following code to the OnTradeTransaction() function:
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- if(trans.type==TRADE_TRANSACTION_DEAL_ADD) { if(trans.deal_type==DEAL_TYPE_BUY || trans.deal_type==DEAL_TYPE_SELL) { int i; for(i=0;i<POS_BUF_LEN;i++) { if(ps[i].new_event==false) break; } if(i<POS_BUF_LEN) { ps[i].new_event = true; ps[i].deal_type = trans.deal_type; ps[i].symbol = trans.symbol; ps[i].volume = trans.volume; ps[i].price = trans.price; ps[i].deal = trans.deal; } } } }
where ps is the buffer of POS_STR structures:
struct POS_STR { bool new_event; string symbol; ulong deal; ENUM_DEAL_TYPE deal_type; double volume; double price; }; #define POS_BUF_LEN 3 POS_STR ps[POS_BUF_LEN];
The buffer is required in case more than one position was closed (or opened) within a short period of time. When a position is opened or closed, after the deal is added to the history, we get all the necessary parameters and set the new_event flag.
Below is the code that will be added to the OnTimer() function to watch for new_event flags and generate SMS reports:
//--- processing of the opening/closing of positions string posstr=""; for(int i=0;i<POS_BUF_LEN;i++) { if(ps[i].new_event==true) { string str; if(ps[i].deal_type==DEAL_TYPE_BUY) str+= "Buy "; else if(ps[i].deal_type==DEAL_TYPE_SELL) str+= "Sell "; str+= DoubleToString(ps[i].volume,2)+" "+ps[i].symbol; int digits = (int)SymbolInfoInteger(ps[i].symbol,SYMBOL_DIGITS); str+= ", price="+DoubleToString(ps[i].price,digits); // long deal_entry; HistorySelect(TimeCurrent()-3600,TimeCurrent());//retrieve the history for the last hour if(HistoryDealGetInteger(ps[i].deal,DEAL_ENTRY,deal_entry)==true) { if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_IN) str+= ", entry: in"; else if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_OUT) { str+= ", entry: out"; double profit; if(HistoryDealGetDouble(ps[i].deal,DEAL_PROFIT,profit)==true) { str+= ", profit = "+DoubleToString(profit,2); } } } posstr+= str+"\r\n"; ps[i].new_event=false; } } if(posstr!="") { Print(posstr+"pos: "+DoubleToString(PositionsTotal(),0)); SendSMS(inp_admin_number, posstr+"pos: "+DoubleToString(PositionsTotal(),0), false); }
Now, compile and run the Expert Advisor. Let's try to buy AUDCAD, with the lot size of 0.14. The Expert Advisor will send the following SMS message: "Buy 0.14 AUDCAD, price=0.96538, entry: in". After a little while we close the position and get the following text message regarding the position closing:
Fig. 10. Text messages about the position opening (entry: in) and closing (entry: out)
8. Processing Incoming SMS Messages for Open Position Management
Until now, our Expert Advisor has only sent messages to the administrator's phone number. Let's now teach it to receive and execute SMS commands. This may be useful in, e.g. closing all or some open positions. As we know, there is nothing like having your position closed on time.
But we should first make sure that SMS messages are received correctly. To do this, we add the display of the last received message to the IncomingSMS() function:
//+------------------------------------------------------------------+ //| Called when a new SMS message is received | //+------------------------------------------------------------------+ void IncomingSMS(INCOMING_SMS_STR& sms) { string str, strtmp; //Number from which the last received SMS message was sent: ObjectSetString(0, "str18", OBJPROP_TEXT, "SMS number: "+sms.sender); //Date and time of sending the last received SMS message: str = TimeToString(sms.scts.time,TIME_DATE|TIME_SECONDS); ObjectSetString(0, "str19", OBJPROP_TEXT, "SMS date/time: "+str); //Text of the last received SMS message: strtmp = StringSubstr(sms.text, 0, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str20", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,32, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str21", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,64, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str22", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,96, 32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str23", OBJPROP_TEXT, str); strtmp = StringSubstr(sms.text,128,32); str = " "; if(strtmp!="") str = strtmp; ObjectSetString(0, "str24", OBJPROP_TEXT, str); }
If we now send a SMS message to the modem, it will be displayed in the table:
Fig. 11. The incoming SMS message as displayed in the terminal window
Please note that all incoming SMS messages are displayed in the "Expert Advisors" tab in the following form: <index_in_modem_memory>text_of_the_message:
Fig. 12. The incoming SMS message as displayed in the "Expert Advisors" tab
The word "close" will be used as a command for closing deals. It should be followed by the space and the parameter - the symbol of the position that needs to be closed, or "all" in case you need to close all positions. The case is of no importance as prior to processing the text of the message, we use the StringToUpper() function. When analyzing the message, make sure to check that the sender's phone number matches the set administrator's number.
Further, it should be noted that there might be cases where an SMS message is received with a considerable delay (due to technical glitches at the operator's side, etc.). In such cases, you cannot take into account the command received in the message as the market situation could have changed. In view of this, we introduce another input parameter:
input int inp_sms_max_old=600; //SMS command expiration, sec
The value of 600 suggests that commands that took more than 600 seconds (10 minutes) to be delivered, will be ignored. Please note that the method of checking the delivery time used in the example implies that the SMS service center and the device on which the Expert Advisor is running are situated in the same time zone.
To process SMS commands, let's add the following code to the IncomingSMS() function:
if(sms.sender==inp_admin_number) { Print("SMS from the administrator"); datetime t = TimeLocal(); //--- message expiration check if(t-sms.scts.time<=inp_sms_max_old) {//--- check if the message is a command string cmdstr = sms.text; StringToUpper(cmdstr);//convert everything to upper case int pos = StringFind(cmdstr, "CLOSE", 0); cmdstr = StringSubstr(cmdstr, pos+6, 6); if(pos>=0) {//--- command. send it for processing ClosePositions(cmdstr); } } else Print("The SMS command has expired"); }
If the SMS message was delivered from the administrator, it has not expired and represents a command (contains the keyword "Close"), we send its parameter for processing by the ClosePositions() function:
uint ClosePositions(string sstr) {//--- close the specified positions bool all = false; if(StringFind(sstr, "ALL", 0)>=0) all = true; uint res = 0; for(int i=0;i<PositionsTotal();i++) { string symbol = PositionGetSymbol(i); if(all==true || sstr==symbol) { if(PositionSelect(symbol)==true) { long pos_type; double pos_vol; if(PositionGetInteger(POSITION_TYPE,pos_type)==true) { if(PositionGetDouble(POSITION_VOLUME,pos_vol)==true) { if(OrderClose(symbol, (ENUM_POSITION_TYPE)pos_type, pos_vol)==true) res|=0x01; else res|=0x02; } } } } } return(res); }
This function checks whether any open position is a match in terms of the parameter (symbol) received in the command. Positions that satisfy this condition are closed using the OrderClose() function:
bool OrderClose(string symbol, ENUM_POSITION_TYPE pos_type, double vol) { MqlTick last_tick; MqlTradeRequest request; MqlTradeResult result; double price = 0; // ZeroMemory(request); ZeroMemory(result); // if(SymbolInfoTick(Symbol(),last_tick)) { price = last_tick.bid; } else { Print("Error when getting current prices"); return(false); } // if(pos_type==POSITION_TYPE_BUY) {//--- closing a BUY position - SELL request.type = ORDER_TYPE_SELL; } else if(pos_type==POSITION_TYPE_SELL) {//--- closing a SELL position - BUY request.type = ORDER_TYPE_BUY; } else return(false); // request.price = NormalizeDouble(price, _Digits); request.deviation = 20; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = NormalizeDouble(vol, 2); if(request.volume==0) return(false); request.type_filling = ORDER_FILLING_FOK; // if(OrderSend(request, result)==true) { if(result.retcode==TRADE_RETCODE_DONE || result.retcode==TRADE_RETCODE_DONE_PARTIAL) { Print("Order executed successfully"); return(true); } } else { Print("Order parameter error: ", GetLastError(),", Trade server return code: ", result.retcode); return(false); } // return(false); }
After successful processing of orders, the function for monitoring position changes will generate and send an SMS notification.
9. Deleting Messages from the Modem Memory
Please note that the modem handler does not delete incoming SMS messages on its own. Therefore, when the SMS memory gets full over the course of time, the handler will call the SMSMemoryFull() function and pass to it the current number of messages in the modem memory. You can delete them all or do it on a selective basis. The modem will not accept any new messages until the memory is released.
//+------------------------------------------------------------------+ //| SMS memory is full | //+------------------------------------------------------------------+ void SMSMemoryFull(int n) { sms_mem_full = true; for(int i=0; i<n; i++) {//delete all SMS messages if(DelSMSbyIndex(i)==false) break; else sms_mem_full = false; } }
You can also delete SMS messages right after they have been processed. When the IncomingSMS() function is called by the modem handler, the INCOMING_SMS_STR structure passes the index of the message in the modem memory, which allows deleting the message right after processing, using the DelSMSbyIndex() function:
Conclusion
This article has dealt with the development of the Expert Advisor that uses a GSM modem to remotely monitor the trading terminal. We have considered the methods for getting information about open positions, current profit and other data using SMS notifications. We have also implemented the basic functions for open position management using SMS commands. The example provided features commands in English but you can use the Russian commands equally well (not to waste time on switching between different keyboard layouts in your phone).
Finally, let's check the behavior of our Expert Advisor when dealing with an old mobile phone released to market over 10 years ago. Device - Siemens M55. Let's connect it:
Fig. 13. Connecting Siemens M55
Fig. 14. Successful initialization of Siemens M55, "Expert Advisors" tab
You can see that all the necessary parameters have been obtained. The only problem is the data we get from USSD requests. The thing is that Siemens M55 does not support the AT command for working with USSD requests. Apart from that, its functionality is as good as that of any present-day modem, so it can be used for working with our Expert Advisor.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/797
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
How about CDMA, dude ? Same principle apply ? What need to be change ?
I used to have SMS sender standalone software that only works on GSM but not on CDMA.
Haha! Neato.
I also like the comment: "Welcome to Forex - the world of financial independence." I have yet to believe this...