![MQL5 쿡북: 커스텀 차트 이벤트 핸들링](https://c.mql5.com/i/articles/overlay.png)
MQL5 쿡북: 커스텀 차트 이벤트 핸들링
개요
해당 글은 MQL5 쿡북: 전형적인 차트 이벤트 핸들링과 논리적으로 이어집니다. 커스텀 차트 이벤트 핸들링 메소드에 대한 내용입니다. 커스텀 이벤트 개발 및 핸들링 예제가 포함되어 있습니다. 본문에 언급된 아이디어는 객체 지향 도구를 이용해 구현되었습니다.
커스텀 이벤트는 굉장히 광범위하기 때문에 개발자가 창의력을 발휘하기 좋은 분야입니다.
1. 커스텀 차트 이벤트
이름만 봐도 사용자가 정하는 이벤트라는 걸 알 수 있죠. 정확히 어떤 이벤트가 발생하는 지는 프로그래머가 결정합니다. 원하는 경우 MQL5 개발자는 언어 능력을 얼마든지 활용해 복잡한 알고리즘을 구현할 수 있죠.
커스텀 이벤트는 차트 이벤트의 두 번째 형태인데요. 첫 번째는 전형적인 이벤트입니다. 관련 자료에서 '전형적인 차트 이벤트'라는 말은 사용된 적 없지만 처음 열 가지 차트 이벤트는 여기에 해당한다고 봐요.
전체 차트 이벤트에 대해 오직 한 가지의 열거형이 적용됩니다. 바로 ENUM_CHART_EVENT죠.
커스텀 이벤트에는 65535개의 식별자가 있다고 하네요. 커스텀 이벤트의 첫 번째와 마지막 식별자는 CHARTEVENT_CUSTOM과 CHARTEVENT_CUSTOM_LAST의 값으로 설정됩니다. 각각 1000과 66534에 해당하죠(그림 1).
그림 1. 커스텀 이벤트 첫 번째와 마지막 식별자
간단한 계산 하나면 식별자 개수를 알 수 있죠. 66534-1000+1=65535입니다.
커스텀 이벤트는 우선 만들어야 쓸 수가 있죠. 따라서 개발자가 EA 알고리즘으로 구현될 이벤트의 컨셉을 고안합니다. 커스텀 이벤트를 분류할 수 있으면 편할 텐데요. 그렇다고 모호성을 식별할 수 있는 건 아니지만 줄일 수는 있고 인수 구조도 정렬시킬 수 있죠.
이걸 기준으로 삼아볼게요. 사용자명이 sergeev인 개발자가 제안한 트레이딩 로봇 원형을 예로 들겠습니다. 해당 개발자는 전체 이벤트를 세 개의 그룹으로 나눕니다(그림 2).
그림 2. 커스텀 이벤트 소스 그룹
그리고 모든 커스텀 이벤트는 분류 그룹을 기준으로 개발되죠.
간단하게 시작해 볼까요? 인디케이터 이벤트로 구성된 첫 번째 그룹을 한번 보죠. 해당 그룹에는 인디케이터 생성 및 삭제, 포지션 오픈 및 청산 신호 수신이 포함됩니다. 두 번째 그룹은 주문 및 포지션 상태 변경 이벤트를 포함하는데요. 우리 예제에서는 포지션 오픈 및 청산이 여기에 포함됩니다. 굉장히 간단하죠. 마지막은 형식화하기 가장 어려운 그룹입니다. 외부 이벤트 그룹이죠.
수동 거래 활성화 및 비활성화의 두 가지 이벤트를 설정할게요.
그림 3. 커스텀 이벤트 소스
첫 번째 패턴은 연역적 추론을 통해 얻어집니다. 일반적인 것에서 특수한 것을 제외하는 거죠(그림 3). 다음은 향후 해당 클래스에 포함되는 이벤트 생성에 사용할 패턴입니다(표 1).
표 1. 커스텀 이벤트
아직 '컨셉'이라고 부르기는 부족하지만 그래도 잘 시작했네요. 다른 방식으로 접근해 보겠습니다. 추상적 거래 시스템에는 세 개의 서브시스템, 즉 기본 모듈이 포함된다는 걸 다들 잘 알고 계시죠(그림 4).
그림 4. 추상적 거래 시스템 모델
'소스' 기준을 기반으로 하는 커스텀 이벤트는 다음에서 생성된 이벤트로 분류될 수 있습니다.
- 신호 서브시스템
- 오픈 포지션 추적 서브시스템
- 자금 관리 서브시스템
자금 관리 서브시스템의 경우 허용 가능한 드로우다운 레벨 도달, 설정 값 만큼 거래량 증가, 손절 한도(%) 증가 등을 포함하게 되죠.
2. ChartEvent 핸들러 및 생성기
다음은 차트이벤트 핸들러와 생성기에 대한 라인을 작성할 차례입니다. 커스텀 차트 이벤트 핸들링 원칙은 일반 차트 이벤트 핸들링 원칙과 비슷합니다.
핸들러인 OnChartEvent() 함수가 네 개의 정수형 변수를 갖게 되죠. 개발자들이 해당 메커니즘을 이용해서 이벤트를 구별하고 추가 정보를 획득할 수 있도록 만들었습니다. 제 생각에는 아주 간단하면서도 편리한 프로그램 메커니즘 같군요.
EventChartCustom() 함수는 커스텀 차트 이벤트를 생성합니다. 커스텀 차트 이벤트는 '원래' 차트와 '새로운' 차트에 생성될 수 있는데요. '원래'와 '새로운'의 의미를 가장 흥미롭게 설명한 아티클은 MetaTrader5에서 다중 통화 모드 구현하기가 아닐까 싶네요.
제 생각에는 이벤트 식별자는 ushort형인데 비해 핸들러는 int형인 게 조금 별로인 것 같아요. 핸들러도 ushort형인 게 좀 더 논리적이겠죠.
3. 커스텀 이벤트 클래스
다시 한번 말하지만 이벤트 컨셉은 EA 개발자에게 달려 있습니다. 이번에는 표 1의 이벤트를 이용해 볼 겁니다. 우선 커스텀 이벤트 클래스 CEventBase와 그 파생 클래스를 정리해보겠습니다.
그림 5. 이벤트 클래스 계층 구조
기본 클래스는 다음과 같이 나타납니다.
//+------------------------------------------------------------------+ //| Class CEventBase. | //| Purpose: base class for a custom event | //| Derives from class CObject. | //+------------------------------------------------------------------+ class CEventBase : public CObject { protected: ENUM_EVENT_TYPE m_type; ushort m_id; SEventData m_data; public: void CEventBase(void) { this.m_id=0; this.m_type=EVENT_TYPE_NULL; }; void ~CEventBase(void){}; //-- bool Generate(const ushort _event_id,const SEventData &_data, const bool _is_custom=true); ushort GetId(void) {return this.m_id;}; private: virtual bool Validate(void) {return true;}; };-->
이벤트 형식은 ENUM_EVENT_TYPE 열거형으로 설정합니다.
//+------------------------------------------------------------------+ //| A custom event type enumeration | //+------------------------------------------------------------------+ enum ENUM_EVENT_TYPE { EVENT_TYPE_NULL=0, // no event //--- EVENT_TYPE_INDICATOR=1, // indicator event EVENT_TYPE_ORDER=2, // order event EVENT_TYPE_EXTERNAL=3, // external event };-->
여기에는 이벤트 식별자와 데이터 구조가 포함됩니다.
CEventBase 기본 클래스의 Generate() 메소드는 이벤트 발생을 담당합니다. GetId() 메소드는 이벤트 id를 반환하며 가상 메소드 Validate() 는 이벤트 식별자 값을 확인합니다. 처음에는 이벤트 핸들링 메소드도 클래스에 포함시켰었는데요. 생각해 보니 각각의 이벤트는 그 특성이 달라 추상적 메소드는 여기에 맞지 않겠더라고요. 그래서 그 대신 커스텀 이벤트를 담당하는 CEventProcessor 클래스로 넘겼습니다.
4. 커스텀 이벤트 핸들러 클래스
CEventProcessor 클래스는 8개의 이벤트를 생성하고 핸들링합니다. 클래스 데이터 멤버는 다음과 같습니다.
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+----------------------------Data members--------------------------+ protected: ulong m_magic; //--- flags bool m_is_init; bool m_is_trade; //--- CEventBase *m_ptr_event; //--- CTrade m_trade; //--- CiMA m_fast_ema; CiMA m_slow_ema; //--- CButton m_button; bool m_button_state; //+------------------------------------------------------------------+ };-->
그 속성 가운데에는 초기화 및 거래 플래그가 포함됩니다. EA가 올바르게 시작하지 않은 경우 첫 번째 멤버가 EA의 거래를 막습니다. 두 번째 멤버는 거래 허가 여부를 확인하죠.
CEventBase형 객체에 대한 포인터도 포함되는데요. 이는 다형성을 갖기 때문에 여러 형태의 이벤트를 담당하게 됩니다. CTrade 클래스 인스턴스는 거래 오퍼레이션에 대한 액세스를 제공합니다.
CiMA형 객체는 인디케이터로부터 수신한 데이터의 핸들링을 돕습니다. 간단한 설명을 위해 거래 시그널을 수신할 예정인 두 개의 MA 인디케이터를 예로 들겠습니다. 'CButton' 클래스의 인스턴스가 EA의 수동 활성화 및 비활성화에 사용될 겁니다.
클래스 메소드는 '모듈-프로시저-함수-매크로' 원칙을 따라 분리됩니다.
//+------------------------------------------------------------------+ //| Class CEventProcessor. | //| Purpose: base class for an event processor EA | //+------------------------------------------------------------------+ class CEventProcessor { //+-------------------------------Methods----------------------------+ public: //--- constructor/destructor void CEventProcessor(const ulong _magic); void ~CEventProcessor(void); //--- Modules //--- event generating bool Start(void); void Finish(void); void Main(void); //--- event processing void ProcessEvent(const ushort _event_id,const SEventData &_data); private: //--- Procedures void Close(void); void Open(void); //--- Functions ENUM_ORDER_TYPE CheckCloseSignal(const ENUM_ORDER_TYPE _close_sig); ENUM_ORDER_TYPE CheckOpenSignal(const ENUM_ORDER_TYPE _open_sig); bool GetIndicatorData(double &_fast_vals[],double &_slow_vals[]); //--- Macros void ResetEvent(void); bool ButtonStop(void); bool ButtonResume(void); };-->
모듈 가운데 이벤트만을 생성하는 모듈은 세 가지가 있습니다. 이벤트를 시작하는 Start(), 이벤트를 끝내는 Finish() , 그리고 메인 이벤트인 Main()가 여기에 해당하죠. 네 번째 모듈인 ProcessEvent()는 이벤트 핸들러인 동시에 생성기가 됩니다.
4.1 시작 모듈
해당 모듈은 OnInit() 핸들러에서 호출되도록 고안되었습니다.
//+------------------------------------------------------------------+ //| Start module | //+------------------------------------------------------------------+ bool CEventProcessor::Start(void) { //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+1 event if(this.m_ptr_event.Generate(1,data)) //--- create a button if(this.m_button.Create(0,"Start_stop_btn",0,25,25,150,50)) if(this.ButtonStop()) { this.m_button_state=false; return true; } } //--- return false; }-->
이 모듈에서는 인디케이터 이벤트 객체에 대한 포인터가 생성됩니다. 그리고 나면 '인디케이터 생성' 이벤트가 생성되죠. 가장 마지막으로 버튼이 만들어지고요. '정지' 모드로 변경됩니다. 버튼이 눌리는 경우 EA가 멈추게 되는 것이죠.
해당 메소드 정의에는 SEventData 구조 또한 포함됩니다. 커스텀 이벤트 생성기로 전달되는 매개 변수들의 컨테이너가 되죠. 해당 구조 내 한 개의 필드만 채워지는데요. long형 필드입니다. EA의 매직 넘버를 나타낼 겁니다.
4.2 종료 모듈
해당 모듈은 OnDeinit() 핸들러에서 호출되도록 고안되었습니다.
//+------------------------------------------------------------------+ //| Finish module | //+------------------------------------------------------------------+ void CEventProcessor::Finish(void) { //--- reset the event object this.ResetEvent(); //--- create an indicator event object this.m_ptr_event=new CIndicatorEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; //--- generate CHARTEVENT_CUSTOM+2 event bool is_generated=this.m_ptr_event.Generate(2,data,false); //--- process CHARTEVENT_CUSTOM+2 event if(is_generated) this.ProcessEvent(CHARTEVENT_CUSTOM+2,data); } }-->
앞서 생성된 이벤트 포인터가 삭제되며 '인디케이터 삭제' 이벤트가 생성됩니다. OnDeinit() 핸들러에서 커스텀 이벤트가 생성되는 경우 런타임 에러 4001(예기치 않은 외부 오류)이 발생하므로 주의하세요. 해당 메소드에서 이벤트 생성 및 핸들링은 OnChartEvent()를 호출하지 않고 실행됩니다.
앞서 말했듯이 EA의 매직 넘버가 SEventData 구조에 저장됩니다.
4.3 메인 모듈
해당 모듈은 OnTick() 핸들러에서 호출되도록 고안되었습니다.
//+------------------------------------------------------------------+ //| Main module | //+------------------------------------------------------------------+ void CEventProcessor::Main(void) { //--- a new bar object static CisNewBar newBar; //--- if initialized if(this.m_is_init) //--- if not paused if(this.m_is_trade) //--- if a new bar if(newBar.isNewBar()) { //--- close module this.Close(); //--- open module this.Open(); } }-->
해당 모듈에서는 Open()과 Close() 프로시저가 호출됩니다. 첫 번째 프로시저는 '오프닝 시그널 수신' 이벤트를 생성하고, 두 번째 프로시저는 '청산 시그널 수신' 이벤트를 생성합니다. 현재 버전의 모듈은 새로운 바가 추가되어도 제대로 작동하는 것으로 나타납니다. Konstantin Gruzdev가 앞서 새로운 바를 탐지하는 클래스를 구현한 바 있습니다.
4.4 이벤트 핸들링 모듈
해당 모듈은 OnChartEvent() 핸들러에서 호출되도록 고안되었습니다. 크기와 기능면에서 가장 큰 모듈입니다.
//+------------------------------------------------------------------+ //| Process event module | //+------------------------------------------------------------------+ void CEventProcessor::ProcessEvent(const ushort _event_id,const SEventData &_data) { //--- check event id if(_event_id==CHARTEVENT_OBJECT_CLICK) { //--- button click if(StringCompare(_data.sparam,this.m_button.Name())==0) { //--- button state bool button_curr_state=this.m_button.Pressed(); //--- to stop if(button_curr_state && !this.m_button_state) { if(this.ButtonResume()) { this.m_button_state=true; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+7 event ushort curr_id=7; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- to resume else if(!button_curr_state && this.m_button_state) { if(this.ButtonStop()) { this.m_button_state=false; //--- reset the event object this.ResetEvent(); //--- create an external event object this.m_ptr_event=new CExternalEvent(); //--- if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)TimeCurrent(); //--- generate CHARTEVENT_CUSTOM+8 event ushort curr_id=8; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } } } //--- user event else if(_event_id>CHARTEVENT_CUSTOM) { long magic=_data.lparam; ushort curr_event_id=this.m_ptr_event.GetId(); //--- check magic if(magic==this.m_magic) //--- check id if(curr_event_id==_event_id) { //--- process the definite user event switch(_event_id) { //--- 1) indicator creation case CHARTEVENT_CUSTOM+1: { //--- create a fast ema if(this.m_fast_ema.Create(_Symbol,_Period,21,0,MODE_EMA,PRICE_CLOSE)) if(this.m_slow_ema.Create(_Symbol,_Period,55,0,MODE_EMA,PRICE_CLOSE)) if(this.m_fast_ema.Handle()!=INVALID_HANDLE) if(this.m_slow_ema.Handle()!=INVALID_HANDLE) { this.m_trade.SetExpertMagicNumber(this.m_magic); this.m_trade.SetDeviationInPoints(InpSlippage); //--- this.m_is_init=true; } //--- break; } //--- 2) indicator deletion case CHARTEVENT_CUSTOM+2: { //---release indicators bool is_slow_released=IndicatorRelease(this.m_fast_ema.Handle()); bool is_fast_released=IndicatorRelease(this.m_slow_ema.Handle()); if(!(is_slow_released && is_fast_released)) { //--- to log? if(InpIsLogging) Print("Failed to release the indicators!"); } //--- reset the event object this.ResetEvent(); //--- break; } //--- 3) check open signal case CHARTEVENT_CUSTOM+3: { MqlTick last_tick; if(SymbolInfoTick(_Symbol,last_tick)) { //--- signal type ENUM_ORDER_TYPE open_ord_type=(ENUM_ORDER_TYPE)_data.dparam; //--- double open_pr,sl_pr,tp_pr,coeff; open_pr=sl_pr=tp_pr=coeff=0.; //--- if(open_ord_type==ORDER_TYPE_BUY) { open_pr=last_tick.ask; coeff=1.; } else if(open_ord_type==ORDER_TYPE_SELL) { open_pr=last_tick.bid; coeff=-1.; } sl_pr=open_pr-coeff*InpStopLoss*_Point; tp_pr=open_pr+coeff*InpStopLoss*_Point; //--- to normalize prices open_pr=NormalizeDouble(open_pr,_Digits); sl_pr=NormalizeDouble(sl_pr,_Digits); tp_pr=NormalizeDouble(tp_pr,_Digits); //--- open the position if(!this.m_trade.PositionOpen(_Symbol,open_ord_type,InpTradeLot,open_pr, sl_pr,tp_pr)) { //--- to log? if(InpIsLogging) Print("Failed to open the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+5 event ushort curr_id=5; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } } //--- break; } //--- 4) check close signal case CHARTEVENT_CUSTOM+4: { if(!this.m_trade.PositionClose(_Symbol)) { //--- to log? if(InpIsLogging) Print("Failed to close the position: "+_Symbol); } else { //--- pause Sleep(InpTradePause); //--- reset the event object this.ResetEvent(); //--- create an order event object this.m_ptr_event=new COrderEvent(); if(CheckPointer(this.m_ptr_event)==POINTER_DYNAMIC) { SEventData data; data.lparam=(long)this.m_magic; data.dparam=(double)this.m_trade.ResultDeal(); //--- generate CHARTEVENT_CUSTOM+6 event ushort curr_id=6; if(!this.m_ptr_event.Generate(curr_id,data)) PrintFormat("Failed to generate an event: %d",curr_id); } } //--- break; } //--- 5) position opening case CHARTEVENT_CUSTOM+5: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_IN) { //--- to log? if(InpIsLogging) { Print("\nNew position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 6) position closing case CHARTEVENT_CUSTOM+6: { ulong ticket=(ulong)_data.dparam; ulong deal=(ulong)_data.dparam; //--- datetime now=TimeCurrent(); //--- check the deals & orders history if(HistorySelect(now-PeriodSeconds(PERIOD_H1),now)) if(HistoryDealSelect(deal)) { double deal_vol=HistoryDealGetDouble(deal,DEAL_VOLUME); ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal,DEAL_ENTRY); //--- if(deal_entry==DEAL_ENTRY_OUT) { //--- to log? if(InpIsLogging) { Print("\nClosed position for: "+_Symbol); PrintFormat("Volume: %0.2f",deal_vol); } } } //--- break; } //--- 7) stop trading case CHARTEVENT_CUSTOM+7: { datetime stop_time=(datetime)_data.dparam; //--- this.m_is_trade=false; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is stopped at: %s", TimeToString(stop_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } //--- 8) resume trading case CHARTEVENT_CUSTOM+8: { datetime resume_time=(datetime)_data.dparam; this.m_is_trade=true; //--- to log? if(InpIsLogging) PrintFormat("Expert trading is resumed at: %s", TimeToString(resume_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); //--- break; } } } } }-->
두 가지 파트로 구성되죠. 첫 번째 파트는 '버튼' 객체를 이용한 이벤트 핸들링을 담당합니다. 클릭하면 외부 커스텀 이벤트가 발생하며 이는 차후 핸들링됩니다.
두 번째는 생성된 커스텀 이벤트 프로세싱을 담당하죠. 두 개의 블록으로 구성되며 관련 이벤트가 핸들된 후 새로운 블록이 생성됩니다. '오프닝 시그널 수신' 이벤트는 첫 번째 블록에서 프로세스됩니다. 성공적으로 핸들링될 경우 새로운 주문 이벤트인 '포지션 오픈'이 생성됩니다. '청산 시그널 수신' 이벤트는 두 번째 블록에서 프로세스되는데요. 신호가 제대로 핸들링되면 '포지션 청산' 이벤트가 발생합니다.
CustomEventProcessor.mq5은 CEventProcessor 클래스를 이용한 EA의 아주 좋은 예제입니다. 해당 EA는 이벤트를 생성하고 해당 이벤트에 알맞게 대응하도록 고안되었습니다. OPP 패러다임 덕분에 소스 코드를 최소한의 라인으로 제한할 수 있었습니다. EA 소스 코드는 본문의 첨부 파일에 포함되어 있습니다.
제 생각에는 커스텀 이벤트 메커니즘을 항상 참조할 필요는 없는 것 같아요. 전략적인 면에서 크게 중요하지 않은 다른 형태의 이벤트도 많거든요.
결론
MQL5 환경에서 커스텀 이벤트를 다루는 방법을 설명했습니다. 개발자 여러분의 숙련도와 상관없이 관심을 가질만 한 내용이길 바랍니다.
MQL5 언어가 성장하고 있어 참 기쁩니다. 아마 곧 클래스 템플릿과 함수 포인터도 생길 겁니다. 그러면 임의의 객체 메소드에 대한 대리자도 작성할 수 있겠죠.
아래의 소스 파일은 프로젝트 폴더에 넣을 수 있습니다. 제 경우에는 MQL5\Projects\ChartUserEvent에 저장했죠.
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/1163
![랜덤 포레스트로 추세 예측하기](https://c.mql5.com/2/11/Random_Forest_MetaTrader5.png)
![MQL5 쿡북: TradeTransaction 이벤트 프로세싱](https://c.mql5.com/2/11/MQL5_Recipes_OnTradeTransaction_MetaTrader5.png)
![일반 VPS보다 MetaTrader4, MetaTrader5 가상 호스팅이 더 나은 이유](https://c.mql5.com/2/11/Virtual_hosting.png)
![3세대 신경망: 심층 신경망](https://c.mql5.com/2/12/Deep_neural_network_MetaTrader5__2.png)