English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
MQL5 Expert Advisor의 GSM 모뎀 작업

MQL5 Expert Advisor의 GSM 모뎀 작업

MetaTrader 5통합 | 12 10월 2021, 12:47
91 0
Serhii Shevchuk
Serhii Shevchuk

소개

현재 모바일 터미널, 푸시 알림, ICQ 작업과 같이 거래 계정을 원격으로 편안하게 모니터링할 수 있는 방법이 많이 있습니다. 그러나 모두 인터넷 연결이 필요합니다. 이 글에서는 모바일 인터넷을 사용할 수 없는 경우에도 통화 및 문자 메시지를 통해 거래 단말기와 계속 연락할 수 있도록 하는 Expert Advisor를 만드는 과정에 대해 설명합니다. 또한 이 Expert Advisor는 거래 서버와의 연결이 끊겼거나 다시 설정되었음을 알릴 수 있습니다.

이를 위해 모뎀 기능이 있는 대부분의 전화기뿐만 아니라 거의 모든 GSM 모뎀이 이 작업을 수행할 수 있습니다. 예를 들어 필자는 Huawei E1550을 선택했습니다. 이 모뎀은 동종 장치 중 가장 널리 사용되는 장치 중 하나이기 때문입니다. 또한 글 말미에서 해당 모뎀을 구형 휴대폰 Siemens M55(2003년 출시)로 교체하게 되면 어떤 일이 일어나는지 살펴보겠습니다.

그러나 먼저 Expert Advisor에서 모뎀으로 데이터 바이트를 보내는 방법에 대한 몇 마디.


1. COM 포트 작업

모뎀을 컴퓨터에 연결하고 필요한 모든 드라이버를 설치하면 시스템에서 가상 COM 포트를 볼 수 있습니다. 모뎀에 대한 모든 향후 작업은 이 포트를 통해 수행됩니다. 따라서 모뎀과 데이터를 교환하려면 먼저 COM 포트에 액세스해야 합니다.

장치 관리자에 표시된 모뎀

그림 1. Huawei 모뎀이 COM3 포트에 연결되어 있습니다.

여기에서 소스 파일과 함께 인터넷에 자유롭게 배포되는 DLL 라이브러리 TrComPort.dll이 필요합니다. COM 포트를 구성하고 해당 상태를 쿼리하며 데이터를 수신 및 전송하는 데 사용됩니다. 이를 수행하기 위해 다음 기능을 사용할 것입니다.

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

MQL5와의 호환성을 위해 전송된 데이터 유형을 약간 수정해야 했습니다.

TrComPortParameters 구조는 다음과 같습니다.

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
};
-->

대부분의 장치는 8 데이터 비트, 패리티 검사 없음, 1 정지 비트 설정으로 작동합니다. 따라서 모든 COM 포트 매개변수 중에서 Expert Advisor의 COM 포트 번호와 데이터 속도의 매개변수에만 추가하는 것이 합리적입니다.

input ComPortList    inp_com_port_index=COM3;   // Choosing the COM port
input BaudRateList   inp_com_baudrate=_9600bps; // Data rate
-->

COM 포트 초기화 기능은 다음과 같습니다.

//+------------------------------------------------------------------+
//| 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);
  }
-->

초기화에 성공한 경우 PortID 변수는 열려 있는 COM 포트의 식별자를 저장합니다.

여기서 식별자는 0부터 번호가 지정되므로 COM3 포트 식별자는 2와 같습니다. 이제 포트가 열렸으므로 모뎀과 데이터를 교환할 수 있습니다. 그건 그렇고, 모뎀으로 뿐만이 아닙니다. Expert Advisor에서 COM 포트에 액세스하면 납땜에 능숙한 사람들에게 창의성을 위한 훌륭한 기회가 열립니다. Expert Advisor를 LED 또는 움직이는 텍스트 디스플레이에 연결하여 특정 통화 쌍의 주식 또는 시장 가격을 표시할 수 있습니다.

TrComPortGetQueue 함수는 COM 포트 수신기 및 송신기의 대기열에 있는 데이터의 세부 정보를 가져오는 데 사용됩니다.

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
   );
-->

오류의 경우 오류 코드의 음수 값을 반환합니다. 오류 코드에 대한 자세한 설명은 TrComPort.dll 라이브러리 소스 코드와 함께 아카이브에서 볼 수 있습니다.

함수가 수신 버퍼에서 0이 아닌 수의 데이터를 반환하면 읽어야 합니다. 이를 위해 TrComPortReadArray 함수를 사용합니다.

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)
   );
-->

오류의 경우 오류 코드의 음수 값을 반환합니다. 데이터 바이트 수는 TrComPortGetQueue 함수에서 반환된 값과 일치해야 합니다.

기본 시간 초과(COM 포트 초기화 시 설정)를 사용하려면 -1 값을 전달해야 합니다.

COM 포트로 데이터를 전송하기 위해 TrComPortWriteArray 함수를 사용합니다.

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)
   );
-->

적용 사례. "Hello world!"라는 메시지에 대한 응답으로 "좋은 하루 되세요 (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");
   }
}
-->

포트 폐쇄 기능에 특별한 주의를 기울여야 합니다.

int TrComPortClose(
   int portid         // Port identifier
   );  
-->

이 기능은 Expert Advisor 초기화 해제 프로세스에 항상 있어야 합니다. 대부분의 경우 열려 있던 포트는 시스템을 다시 시작한 후에만 다시 사용할 수 있습니다. 실제로 모뎀을 껐다가 켜도 도움이 되지 않을 수 있습니다.


2. AT 명령 및 모뎀 작업

모뎀 작업은 АТ 명령을 사용하여 정렬됩니다. 컴퓨터에서 모바일 인터넷을 사용한 적이 있는 사용자는 AT+CGDCONT=1,"IP","internet"과 같이 대략적으로 보이는 소위 "모뎀 초기화 문자열"을 기억해야 합니다. 이것은 AT 명령 중 하나입니다. 거의 대부분이 접두사 AT로 시작하여 0x0d(캐리지 리턴)로 끝납니다.

원하는 기능을 구현하는 데 필요한 최소한의 AT 명령 집합을 사용합니다. 이렇게 하면 다양한 장치와 명령 집합의 호환성을 보장하기 위한 노력이 줄어듭니다.

다음은 모뎀 작업을 위해 처리기가 사용하는 AT 명령 목록입니다.

 명령 설명
  ATE1                                    
  에코 활성
  AT+CGMI
  제조업체 이름 가져오기
  AT+CGMM
  기기 모델 가져오기
  AT^SCKS
  SIM 카드 상태 가져오기
  AT^SYSINFO
  시스템 정보 가져오기
  AT+CREG
  네트워크 등록 상태 가져오기
  AT+COPS
  현재 이동 통신사의 이름을 가져옵니다.
  AT+CMGF
  텍스트/PDU 모드 간 전환
  AT+CLIP
  발신자 식별 활성화
  AT+CPAS
  모뎀 상태 가져오기
  AT+CSQ
  신호 품질 얻기
  AT+CUSD
  USSD 요청 보내기
  AT+CALM
  무음 모드 활성화(휴대폰에 적용)
  AT+CBC
  배터리 상태 가져오기(휴대전화에 적용 가능)
  AT+CSCA
  SMS 서비스 센터 번호 받기
  AT+CMGL
  SMS 메시지 목록 가져오기
  AT+CPMS
  SMS 메시지용 메모리 선택
  AT+CMGD
  메모리에서 SMS 메시지 삭제
  AT+CMGR
  메모리에서 SMS 메시지 읽기
  AT+CHUP
  수신 전화 거부
  AT+CMGS
  SMS 메시지 보내기


AT 명령 작업의 미묘함을 설명하는 주제에서 벗어나지 않을 것입니다. 기술 포럼에는 관련 정보가 많이 있습니다. 게다가 모든 것이 이미 구현되었으며 모뎀과 함께 작동할 수 있는 Expert Advisor를 만들기 위해 필요한 것은 헤더 파일을 포함하고 기성품 기능과 구조를 사용하기 시작하는 것입니다. 이것이 제가 자세히 설명할 내용입니다.


2.1. 기능

COM 포트 초기화:

bool InitComPort();
-->

반환된 값: 성공적으로 초기화된 경우 - true, 그렇지 않은 경우 - false. 모뎀 초기화 전에 OnInit() 함수에서 호출됩니다.

COM 포트 초기화 해제:

void DeinitComPort();
-->

반환된 값: 없음. OnDeinit() 함수에서 호출됩니다.

모뎀 초기화:

void InitModem();
-->

반환된 값: 없음. COM 포트가 성공적으로 초기화되면 OnInit() 함수에서 호출됩니다.

모뎀 이벤트 핸들러:

void ModemTimerProc();
-->

반환된 값: 없음. OnTimer() 함수에서 1초 간격으로 호출됩니다.

모뎀 메모리에서 인덱스별로 SMS 메시지 읽기:

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
   );
-->

반환된 값: 성공적으로 읽은 경우 - true, 그렇지 않은 경우 - false.

모뎀 메모리에서 인덱스별로 SMS 메시지 삭제:

bool DelSMSbyIndex(
   int index              // SMS message index in the modem memory
   );
-->

반환된 값: 성공적으로 삭제된 경우 - true, 그렇지 않은 경우 - false.

연결 품질 인덱스를 문자열로 변환:

string rssi_to_str(
   int rssi               // Connection quality index, values 0..31, 99
   );
-->

반환된 값: 문자열, 예: "-55dBm".

SMS 메시지 보내기:

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
   );
-->

반환된 값: 성공적으로 전송된 경우 - true, 그렇지 않은 경우 - false. SMS 메시지는 라틴 문자를 사용하여 작성된 경우에만 보낼 수 있습니다. 키릴 문자는 수신 SMS 메시지에만 지원됩니다. flash=true로 설정하면 플래시 메시지가 전송됩니다.


2.2. 이벤트(모뎀 핸들러에 의해 호출되는 함수)

모뎀 상태 구조에서 데이터 업데이트:

void ModemChState();
-->

전달된 매개변수: 없음. 이 함수가 모뎀 핸들러에 의해 호출되면 모뎀 구조에서 데이터가 업데이트되었음을 ​​나타냅니다(구조에 대한 설명은 아래에서 제공됨).

수신 전화:

void IncomingCall(
   string number          // Caller number
   );
-->

전달된 매개변수: 발신자 번호. 모뎀 핸들러에서 이 함수를 호출하면 '번호' 번호에서 들어오는 호출이 수락 및 거부되었음을 나타냅니다.

새로운 수신 SMS 메시지:

void IncomingSMS(
   INCOMING_SMS_STR& sms  // SMS message structure
   );
-->

전달된 매개변수: SMS 메시지 구조(구조에 대한 설명은 아래에 제공됨). 이 함수가 모뎀 처리기에 의해 호출되면 모뎀 메모리에 읽지 않은 새 SMS 메시지가 하나 이상 있음을 나타냅니다. 읽지 않은 메시지의 수가 1보다 크면 가장 최근 메시지가 이 함수로 전달됩니다.

SMS 메모리 가득 참:

void SMSMemoryFull(
   int n                  // Number of SMS messages in the modem memory
   );
-->

전달된 매개변수: 모뎀 메모리의 메시지 수. 이 함수가 모뎀 처리기에 의해 호출되면 SMS 메모리가 가득 차서 모뎀이 메모리가 해제될 때까지 새 메시지를 수락하지 않을 것임을 나타냅니다.


2.3. 모뎀 매개변수의 상태 구조

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

이 구조는 모뎀 이벤트 핸들러에 의해 독점적으로 채워지며 읽기용으로만 다른 함수에서 사용해야 합니다.

다음은 구조 요소에 대한 설명입니다.

 요소설명
  modem.init_ok
  모뎀이 성공적으로 초기화되었음을 나타냅니다.
초기화가 완료된 후 false의 초기 값이 true가 됩니다.
  modem.manufacturer
  모뎀 제조업체, 예: "화웨이".
초기 값은 "해당 사항 없음"입니다.
  modem.device
  모뎀 모델, 예. "E1550"
초기 값은 "해당 사항 없음"입니다.
  modem.sim_stat
  심카드 상태. 다음 값을 사용할 수 있습니다.
-1 - 데이터 없음
0 - 카드가 없거나 차단되었거나 고장났습니다.
1 - 카드 사용 가능
  modem.net_reg
  네트워크 등록 상태입니다. 다음 값을 사용할 수 있습니다.
-1 - 데이터 없음
0 - 등록되지 않음
1 - 등록됨
2 - 검색
3 - 금지
4 - 정의되지 않은 상태
5 - 로밍에 등록됨
  modem.status
  모뎀 상태. 다음 값을 사용할 수 있습니다.
-1 - 초기화
0 - 준비됨
1 - 오류
2 - 오류
3 - 수신 전화
4 - 활성 통화
  modem.op
  현 이동통신사.
운영자 이름(예: "MTS UKR"),
또는 국제 운영자 코드(예: "25501").
초기 값은 "해당 사항 없음"입니다.
  modem.rssi
  신호 품질 지수. 다음 값을 사용할 수 있습니다.
-1 - 데이터 없음
0 - 신호 -113dBm 이하
1 - 신호 -111dBm
2...30 - 신호 -109...-53dBm
31 - 신호 -51dBm 이상
99 - 데이터 없음
문자열로 변환하려면 rssi_to_str() 함수를 사용하십시오.
  modem.sms_sca
  SMS 서비스 센터 번호. SIM 카드 메모리에 들어 있습니다.
발신 SMS 메시지 생성에 필요합니다.
드문 경우지만 번호가 SIM 카드 메모리에 저장되지 않으면
Expert Advisor의 입력 매개변수에 지정된 번호로 대체됩니다.
  modem.bat_stat
  모뎀 배터리 상태(휴대전화에만 해당).
다음 값을 사용할 수 있습니다.
-1 - 데이터 없음
0 - 기기가 배터리 전원으로 작동합니다.
1 - 배터리를 사용할 수 있지만 기기가 배터리로 작동하지 않습니다.
2 - 배터리 없음
3 - 오류
  modem.bat_charge
  배터리 충전량(퍼센트).
0에서 100 사이의 값을 가질 수 있습니다.
  modem.bal
  모바일 계정 잔액. 값을 얻습니다
관련 USSD 요청에 대한 운영자 응답에서.
초기값(초기화 전): -10000.
  modem.exp_date
  Mobile number expiration date. 값을 얻습니다
관련 USSD 요청에 대한 운영자 응답에서.
초기 값은 "해당 사항 없음"입니다.
  modem.sms_free
  사용 가능한 패키지 SMS의 수입니다. 다음의 차이로 계산됩니다.
사용된 패키지 SMS의 초기 번호 및 카운터.
  modem.sms_free_cnt
  사용된 패키지 SMS의 카운터입니다. 값을 얻습니다
관련 USSD 요청에 대한 운영자 응답에서. 초기값은 -1입니다.
  modem.sms_mem_size
  모뎀 SMS 메모리 크기.
  modem.sms_mem_used
  모뎀 SMS 메모리가 사용되었습니다.
  modem.incoming
  마지막 발신자 번호입니다.
초기 값은 "해당 사항 없음"입니다.


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
};
-->

SMS 센터 시간 레이블은 발신자로부터 주어진 메시지가 SMS 센터에서 수신된 시간입니다. 시간 레이블 구조는 다음과 같습니다.

//+------------------------------------------------------------------+
//| Time label structure                                             |
//+------------------------------------------------------------------+
struct INCOMING_CTST_STR
{
   datetime time;            // time
   int gmt;                  // time zone
};
-->

시간대는 15분 간격으로 표시됩니다. 따라서 값 8은 GMT+02:00에 해당합니다.

수신된 SMS 메시지의 텍스트는 키릴 문자뿐만 아니라 라틴어를 사용하여 작성할 수 있습니다. 수신된 메시지에 대해 7비트 및 UCS2 인코딩이 지원됩니다. 긴 메시지 병합은 구현되지 않습니다(이 작업이 짧은 명령을 위해 설계되었다는 사실을 고려하여).

SMS 메시지는 라틴 문자를 사용하여 작성된 경우에만 보낼 수 있습니다. 최대 메시지 길이는 158자입니다. 더 긴 메시지의 경우 지정된 수를 초과하는 문자 없이 전송됩니다.


3. Expert Advisor 개발

시작하려면 TrComPort.dll 파일을 Libraries 폴더에 복사하고 ComPort.mqh, modem.mqh를 배치해야 합니다. 포함 폴더의 sms.mqh.

그런 다음 마법사를 사용하여 새 Expert Advisor를 만들고 모뎀 작업에 필요한 최소값을 추가합니다. 그건:

modem.mqh: 포함

#include <modem.mqh>
-->

입력 매개변수 추가:

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

모뎀 핸들러에 의해 호출되는 함수:

//+------------------------------------------------------------------+
//| 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;
   }
}
-->

1초 간격으로 설정된 타이머와 함께 COM 포트 및 모뎀 초기화를 OnInit() 함수에 추가해야 합니다.

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);
}
-->

OnTimer() 함수에서 모뎀 핸들러를 호출해야 합니다.

void OnTimer()
{
//---
   ModemTimerProc();
}
-->

OnDeinit() 함수에서 COM 포트 초기화 해제를 호출해야 합니다.

void OnDeinit(const int reason)
{
//--- destroy timer
   EventKillTimer();
   DeinitComPort();   
}
-->

우리는 코드를 컴파일하고 0 error(s)를 봅니다.

이제 Expert Advisor를 실행하되 DLL 가져오기를 허용하고 모뎀과 연결된 COM 포트를 선택해야 합니다. "Expert Advisor" 탭에서 다음 메시지를 볼 수 있어야 합니다.

첫 번째 실행

그림 2. 성공적인 실행 후 Expert Advisor의 메시지

동일한 메시지가 표시되면 모뎀(전화)이 이 Expert Advisor와 작업하기에 적합하다는 의미입니다. 이 경우 우리는 더 나아갑니다.

모뎀 매개변수의 시각화를 위한 표를 그려 보겠습니다. 터미널 창의 왼쪽 상단 모서리, OHLC 라인 아래에 배치됩니다. 테이블에 사용할 텍스트 글꼴은 고정 폭입니다. "새로운 택배".

//+------------------------------------------------------------------+
//| 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);
  }
-->

테이블의 데이터를 새로 고치기 위해 RefreshTab() 함수를 사용합니다.

//+------------------------------------------------------------------+
//| 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);
  }
-->

DelTab() 함수는 테이블을 삭제합니다.

//+------------------------------------------------------------------+
//| Deleting the table                                               |
//+------------------------------------------------------------------+
void DelTab()
  {
   for(int i=0; i<25; i++)
      ObjectDelete(0,"str"+DoubleToString(i,0));
   ObjectDelete(0,"bgnd000");
  }
-->

테이블 작업을 위한 함수를 이벤트 핸들러 OnInit()OnDeinit()ModemChState() 함수에 추가해 보겠습니다.

//+------------------------------------------------------------------+
//| 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();
  }
-->

또한 IncomingCall() 함수에 테이블의 마지막 수신 호출 번호를 새로 고칠 수 있는 기회를 추가합니다.

//+------------------------------------------------------------------+
//| 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);   
} 
-->

이제 코드를 컴파일하고 Expert Advisor를 실행하십시오. 터미널 창에서 다음 보고서를 볼 수 있어야 합니다.

모뎀 상태 매개변수

그림 3. 모뎀 매개변수

모뎀을 호출해 보세요. 통화가 거부되고 "수신" 라인에 귀하의 번호가 표시됩니다.


4. USSD 요청 작업

적시에 충전되지 않은 모바일 계정은 최소한의 적절한 순간에 Expert Advisor의 운영을 방해할 수 있습니다. 따라서 계좌 잔고를 확인하는 기능은 가장 중요한 기능 중 하나입니다. 모바일 계정 잔액을 확인하기 위해 일반적으로 USSD 요청을 사용합니다. 또한 USSD 요청을 사용하여 사용 가능한 패키지 SMS 수에 대한 정보를 얻습니다.

요청을 생성하고 수신된 응답을 처리하기 위한 데이터는 입력 매개변수에 있습니다.

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

요청 번호를 지정하지 않으면 요청이 처리되지 않습니다. 또는 모뎀 초기화 직후에 요청이 전송되고 지정된 시간 후에 반복적으로 전송됩니다. 또한 모뎀(전화)이 관련 IT 명령을 지원하지 않는 경우 요청이 처리되지 않습니다(구형 휴대폰 모델에 해당). 

잔액 요청에 따라 교환원으로부터 다음과 같은 응답을 받았다고 가정합니다.

7.13 UAH는 2014년 5월 22일에 만료됩니다. 전화 요금제 - Super MTS 3D Null 25.

핸들러가 응답을 올바르게 식별하도록 하려면 잔액 접미사를 "UAH"로 설정하고 번호 만료 날짜의 접두사를 "만료일"로 설정해야 합니다.

우리의 Expert Advisor는 SMS 메시지를 꽤 자주 보낼 것으로 예상되므로 통신사로부터 SMS 패키지를 구입하는 것이 좋습니다. 즉, 소정의 SMS 메시지를 소정의 요금으로 받는 서비스입니다. 이 경우 아직 사용할 수 있는 패키지 SMS 수를 아는 것이 매우 유용할 수 있습니다. 이것은 USSD 요청을 사용하여 수행할 수도 있습니다. 교환원은 일반적으로 사용 가능한 SMS 대신 사용된 SMS 번호로 응답합니다.

교환원으로부터 다음과 같은 응답을 받았다고 가정합니다.

잔액: 오늘 시내 통화 69분. 오늘 사용: 0 SMS 및 0MB.

이 경우 SMS 카운터 접미사는 "SMS"로 설정하고 일일 한도는 SMS 패키지 이용약관에 따라 설정합니다. 예를 들어, 하루에 30개의 문자 메시지가 제공되고 요청이 값 10을 반환했다면 30-10=20 SMS를 사용할 수 있음을 의미합니다. 이 번호는 처리기에 의해 모뎀 상태 구조의 적절한 요소에 배치됩니다.

주의! USSD 요청 번호에 대해 매우 주의하십시오! 잘못된 요청을 보내면 바람직하지 않은 결과가 발생할 수 있습니다. 일부 원치 않는 유료 서비스를 활성화합니다!

Expert Advisor가 USSD 요청 작업을 시작하려면 관련 입력 매개변수만 지정하면 됩니다.

예를 들어, 우크라이나 이동통신사인 MTS Ukraine의 매개변수는 다음과 같습니다.

사용 가능한 잔액에 대한 요청 매개변수

그림 4. 사용 가능한 잔액에 대한 USSD 요청의 매개변수

사용 가능한 패키지 SMS 수에 대한 요청 매개변수

그림 5. 사용 가능한 패키지 SMS 수에 대한 USSD 요청의 매개변수

이동통신사와 관련된 값을 설정합니다. 그 후 모바일 계정의 사용 가능한 잔액과 사용 가능한 SMS 수가 모뎀 상태 표에 표시됩니다.

사용 가능한 잔액

그림 6. USSD 응답에서 얻은 매개변수

이 글을 쓰는 시점에서 제 이동통신사는 유효기간이 아닌 크리스마스 광고를 보내고 있었습니다. 결과적으로 핸들러는 날짜 값을 얻을 수 없었습니다. 이것이 "만료 날짜" 줄에 "n/a"가 표시되는 이유입니다. 모든 운영자 응답은 "Expert Advisor" 탭에 표시됩니다.

운영자 응답

그림 7. "Expert Advisors" 탭에 표시된 운영자 응답


5. SMS 메시지 보내기

예를 들어 현재 이익, 자본 및 열린 포지션 수를 나타내는 SMS 메시지를 보내는 등 유용한 기능을 추가하기 시작할 것입니다. 수신 전화에 의해 전송이 시작됩니다.

이러한 응답은 확실히 관리자 번호의 경우에만 예상되므로 다른 입력 매개변수가 있습니다.

input string         inp_admin_number="+XXXXXXXXXXXX";//administrator's phone number
-->

숫자는 숫자 앞에 "+"를 포함하여 국제 형식으로 지정해야 합니다.

번호 확인, SMS 텍스트 생성 및 전송이 수신 호출 처리기에 추가되어야 합니다.

//+------------------------------------------------------------------+
//| 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);   
}  
-->

이제 관리자 번호 inp_admin_number에서 모뎀으로 전화가 걸려오면 응답으로 SMS 메시지가 전송됩니다.

수신 전화에 대한 응답 SMS

그림 8. 관리자 전화번호로 걸려온 전화에 대해 Expert Advisor가 보낸 SMS 메시지

여기에서 현재 이익과 자본의 가치, 열린 포지션의 수와 모바일 계좌 잔고를 볼 수 있습니다.


6. 트레이드 서버와의 연결 모니터링

거래 서버와의 연결이 끊겼다가 다시 설정되는 경우 알림을 추가해 보겠습니다. 이를 위해 TERMINAL_CONNECTED 속성 식별자와 함께 TerminalInfoInteger()를 사용하여 10초마다 거래 서버 연결을 확인합니다.

단시간 연결 손실을 필터링하기 위해 입력 매개변수 목록에 추가해야 하는 이력 현상을 사용합니다.

input int            inp_conn_hyst=6; //Hysteresis, х10 sec
-->

값 6은 6*10=60초 이상 연결이 없으면 연결이 끊어진 것으로 간주됨을 의미합니다. 마찬가지로 60초 이상 사용 가능한 경우 연결이 재설정된 것으로 간주됩니다. 최초 등록된 연결 부족의 현지 시간은 연결이 끊어진 시간으로 간주되며, 연결이 가능한 최초의 현지 시간은 복구 시간으로 간주됩니다.

이를 구현하기 위해 OnTimer() 함수에 다음 코드를 추가합니다.

   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;
   }
-->

cm 구조는 다음과 같습니다.

//+------------------------------------------------------------------+
//| 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
-->

SMS 메시지 텍스트에 우리는 거래 서버와의 연결이 끊겼거나 다시 설정된 시간을 명시합니다. 뿐만 아니라 연결이 가능했던 시간(또는 불가능했던 시간)을 설정된 연결 시간과 연결이 끊긴 시간 사이의 차이로 계산합니다. 시간 차이를 초에서 dd hh:mm:ss로 변환하기 위해 dTimeToString() 함수를 추가합니다.

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);
}
-->

Expert Advisor가 실행될 때마다 설정된 연결에 대한 텍스트 메시지를 보내지 않도록 하기 위해 연결이 이미 설정된 것처럼 값을 cm 구조 요소로 설정하는 conn_mon_init() 함수를 추가합니다. 이 경우 Expert Advisor를 실행한 현지 시간에 연결이 설정된 것으로 간주됩니다. 이 함수는 OnInit() 함수에서 호출해야 합니다.

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;
} 
-->

이제 Expert Advisor를 컴파일하고 실행하십시오. 그런 다음 인터넷에서 컴퓨터의 연결을 끊으십시오. 60(오차 10초 전 후)초 후에 서버와의 연결이 끊어졌다는 메시지를 받게 됩니다. 인터넷에 다시 연결합니다. 60초 후에 총 연결 해제 시간을 나타내는 연결 재설정에 대한 메시지가 표시됩니다.

연결 끊김에 대한 메시지 다시 연결되었다는 메시지

그림 9. 서버와의 연결이 끊어졌다가 다시 설정되었음을 알리는 문자 메시지


7. 포지션 개설 및 마감에 대한 보고서 전송

포지션의 개시 및 종료를 모니터링하기 위해 다음 코드를 OnTradeTransaction() 함수에 추가해 보겠습니다.

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;
         }
      }
   }    
}
-->

여기서 psPOS_STR 구조의 버퍼입니다.

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];
-->

버퍼는 짧은 시간 내에 둘 이상의 포지션이 폐쇄(또는 개방)된 경우에 필요합니다. 포지션이 열리거나 닫힐 때 거래가 기록에 추가된 후 필요한 모든 매개변수를 가져오고 new_event 플래그를 설정합니다.

다음은 new_event 플래그를 감시하고 SMS 보고서를 생성하기 위해 OnTimer() 함수에 추가될 코드입니다.

   //--- 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);
   }
-->

이제 Expert Advisor를 컴파일하고 실행하십시오. 랏 크기가 0.14인 AUDCAD를 구매해 보겠습니다. Expert Advisor는 "0.14 AUDCAD 구매, 가격=0.96538, 항목: 입력"이라는 SMS 메시지를 보냅니다. 잠시 후 포지션을 청산하고 포지션 청산에 관한 다음 문자 메시지를 받습니다.

포지션 열림에 대한 메시지 포지션 닫힘에 대한 메시지

그림 10. 포지션 개시(엔트리: in) 및 클로징(엔트리: out)에 대한 문자 메시지


8. 열린 포지션 관리를 위한 수신 SMS 메시지 처리

지금까지 Expert Advisor는 관리자 전화번호로만 메시지를 보냈습니다. 이제 SMS 명령을 수신하고 실행하는 방법을 가르쳐 보겠습니다. 이것은 가령 다음과 같이 모든 또는 일부 열린 포지션을 닫을 경우 유용할 수 있습니다. 알다시피, 귀하의 포지션을 제때 마감하는 것만큼 좋은 것은 없습니다.

그러나 먼저 SMS 메시지가 올바르게 수신되었는지 확인해야 합니다. 이를 위해 IncomingSMS() 함수에 마지막으로 수신된 메시지 표시를 추가합니다.

//+------------------------------------------------------------------+
//| 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);  
}
-->

이제 모뎀에 SMS 메시지를 보내면 다음과 같이 표에 표시됩니다.

새 SMS 메시지

그림. 11. 터미널 창에 표시되는 수신 SMS 메시지

수신되는 모든 SMS 메시지는 <index_in_modem_memory>text_of_the_message> 형식으로 "Expert Advisor" 탭에 표시됩니다.

"Expert Advisors" 탭에 표시되는 SMS 메시지

그림 12. "Expert Advisors" 탭에 표시되는 수신 SMS 메시지

"close"라는 단어는 거래를 마감하는 명령으로 사용됩니다. 그 뒤에 공백과 매개변수(닫아야 하는 위치의 기호)가 와야 하고, 그렇지 않으면 모든 포지션을 닫아야 하는 경우 "all"이 와야 합니다. 메시지 텍스트를 처리하기 전에 StringToUpper() 함수를 사용하므로 대소문자는 중요하지 않습니다. 메시지 분석 시 발신자의 전화번호가 설정된 관리자의 전화번호와 일치하는지 확인하십시오.

또한 SMS 메시지 수신이 상당히 지연되는 경우(운영자 측의 기술적인 결함 등으로 인해)가 있을 수 있다는 점에 유의해야 합니다. 이러한 경우 시장 상황이 변경되었을 수 있으므로 메시지에 수신된 명령을 고려할 수 없습니다. 이를 고려하여 다른 입력 매개변수를 소개합니다.

input int            inp_sms_max_old=600; //SMS command expiration, sec
-->

값 600은 전달하는 데 600초(10분) 이상 소요된 명령이 무시됨을 나타냅니다. 예시에서 사용한 배송 시간 확인 방법은 SMS 서비스 센터와 Expert Advisor가 실행되고 있는 기기가 같은 시간대에 있음을 의미함을 유의하시기 바랍니다.

SMS 명령을 처리하기 위해 다음 코드를 IncomingSMS() 함수에 추가해 보겠습니다.

   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");
   }   
-->

SMS 메시지가 관리자로부터 전달된 경우 만료되지 않았으며 명령(키워드 "Close" 포함)을 나타내는 경우 ClosePositions() 함수로 처리할 매개변수를 보냅니다.

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);
}
-->

이 기능은 열린 포지션이 명령에서 받은 매개변수(기호)와 일치하는지 여부를 확인합니다. 이 조건을 충족하는 포지션은 OrderClose() 함수를 사용하여 닫힙니다.

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);
}
-->

주문이 성공적으로 처리되면 포지션 변경 모니터링 기능이 생성되어 SMS 알림을 보냅니다.


9. 모뎀 메모리에서 메시지 삭제

모뎀 처리기는 들어오는 SMS 메시지를 자체적으로 삭제하지 않습니다. 따라서 시간이 지남에 따라 SMS 메모리가 가득 차면 핸들러는 SMSMemoryFull() 함수를 호출하고 모뎀 메모리에 있는 현재 메시지 수를 전달합니다. 모두 삭제하거나 선택적으로 삭제할 수 있습니다. 모뎀은 메모리가 해제될 때까지 새 메시지를 수락하지 않습니다.

//+------------------------------------------------------------------+
//| 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;   
   }
}
-->

SMS 메시지가 처리된 직후 삭제할 수도 있습니다. 모뎀 핸들러가 IncomingSMS() 함수를 호출하면 INCOMING_SMS_STR 구조가 모뎀 메모리에 있는 메시지 인덱스를 전달하므로 처리 직후 메시지를 삭제할 수 있으며, DelSMSbyIndex() 함수 사용:


결론

이 글은 GSM 모뎀을 사용하여 거래 터미널을 원격으로 모니터링하는 Expert Advisor의 개발을 다루었습니다. 우리는 SMS 알림을 사용하여 열린 포지션, 현재 이익 및 기타 데이터에 대한 정보를 얻는 방법을 고려했습니다. 또한 SMS 명령어를 이용한 열린 포지션 관리를 위한 기본 기능도 구현했습니다. 제공된 예제는 영어로 된 명령을 제공하지만 러시아어 명령을 똑같이 잘 사용할 수 있습니다(휴대폰에서 다른 키보드 레이아웃 사이를 전환하는 데 시간을 낭비하지 않음).

마지막으로 출시된 지 10년이 넘은 구형 휴대전화를 다룰 때 Expert Advisor의 행동을 확인해보자. 장치 - Siemens M55. 연결해보겠습니다.

지멘스 M55의 매개변수지멘스 M55

그림 13. 지멘스 M55 연결

Siemens M55, "Expert Advisor"

그림 14. Siemens M55, "Expert Advisors" 탭의 성공적인 초기화

필요한 모든 매개변수를 얻었음을 알 수 있습니다. 유일한 문제는 USSD 요청에서 얻은 데이터입니다. 문제는 Siemens M55가 USSD 요청 작업을 위한 AT 명령을 지원하지 않는다는 것입니다. 그 외에도 기능은 현재의 모뎀과 비슷하므로 Expert Advisor와 함께 작업하는 데 사용할 수 있습니다.


MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/797

파일 첨부됨 |
trcomport.zip (30.99 KB)
modem.mqh (44.74 KB)
sms.mqh (9.46 KB)
gsminformer.mq5 (25.28 KB)
comport.mqh (5.18 KB)

이 작가의 다른 글

MetaTrader 5의 연속 선물 계약 MetaTrader 5의 연속 선물 계약
선물 계약의 짧은 수명은 기술적 분석을 복잡하게 만듭니다. 짧은 차트를 기술적으로 분석하는 것은 어렵습니다. 예를 들어, UX-9.13 우크라이나 주가 지수 선물의 일 차트에 있는 바의 수는 100개 이상입니다. 따라서 거래자는 합성 선물 매수 계약을 생성합니다. 이 글은 MetaTrader 5 터미널에서 날짜가 다른 선물 계약을 연결하는 방법을 설명합니다.
Renko 차트 표시 Renko 차트 표시
이 글에서는 Renko 차트 작성의 예와 MQL5에서 지표로 구현하는 방법을 설명합니다. 이 지표를 수정하면 기존 차트와 구별됩니다. 지표 창과 메인 차트 모두에서 구성할 수 있습니다. 또한 지그재그 표시기가 있습니다. 차트 구현의 몇 가지 예를 찾을 수 있습니다.
지연되지 않는(Non-Lagging) 디지털 필터 생성하기 지연되지 않는(Non-Lagging) 디지털 필터 생성하기
이 문서에서는 스트림 데이터에서 유용한 신호(트렌드)를 결정하는 방법 중 하나를 설명합니다. 마켓 견적에 적용된 소규모 필터링(스무딩) 테스트는 마지막 막대에 다시 그려지지 않은 지연되지 않는 디지털 필터(지시자)를 만들 수 있는 가능성을 보여줍니다.
MQL5 Cookbook - MQL5의 다중 통화 Expert Advisor 및 대기 중인 주문 작업 MQL5 Cookbook - MQL5의 다중 통화 Expert Advisor 및 대기 중인 주문 작업
이번에는 보류 중인 주문 Buy Stop 및 Sell Stop 작업을 기반으로 하는 거래 알고리즘을 사용하여 다중 통화 Expert Advisor를 만들 것입니다. 이 글은 다음 사항을 고려합니다: 지정된 시간 범위의 거래, 보류 주문 배치/수정/삭제, 이익 실현 또는 손절매에서 마지막 포지션이 마감되었는지 확인하고 각 기호에 대한 거래 내역 제어.