English Русский Deutsch 日本語 Italiano Türkçe
preview
MQL5에서의 이벤트 기반 아키텍처: Expert Advisor를 완전한 기능을 갖춘 트레이딩 시스템으로 전환하는 방법

MQL5에서의 이벤트 기반 아키텍처: Expert Advisor를 완전한 기능을 갖춘 트레이딩 시스템으로 전환하는 방법

MetaTrader 5 |
45 6
MetaQuotes
MetaQuotes

소개

MQL5로 EA를 개발할 때 많은 사람들은 가장 분명한 솔루션으로 시작합니다: 모든 로직을 OnTick 메서드에 넣는다는 것입니다. 이렇게 시작하는 게 확실히 쉽습니다. 하지만 이러한 접근 방식에는 숨겨진 비용이 있습니다. 프로젝트가 커짐에 따라 매매 규칙, 조건 확인, 주문 처리, 데이터 업데이트, 인터페이스, 계산 및 로깅이 모두 하나의 핸들러에 묶입니다. 결과적으로 코드는 더 이상 감당하기 어려운 상태로 비대해 지게 되고 사실상 순전히 운에 의해서만 유지되는 지경에 이르게 됩니다. 한 곳에서 변경이 있으면 시스템의 전혀 다른 부분에 영향을 미치기 시작합니다. 시각화 패널을 수정했더니 갑자기 매매 시나리오가 제대로 작동하지 않게 됩니다. 진입 필터를 변경하면 백그라운드 검사에서 오류가 발생합니다. 그러한 EA는 금세 취약한 단일 구조로 되 버리며 개발자의 자신감보다 빠르게 코드는 복잡해집니다.

MetaTrader 5는 단순히 시세 정보를 제공하는 플랫폼이 아닙니다. 그 이상의 역할을 하며 이벤트에 기반을 두고 있습니다. 터미널은 틱, 타이머 신호, 사용자 작업, 매매 상태 변화 및 시장 심도 이벤트를 지속적으로 수신합니다. 이러한 메시지는 별도로 처리되어야 합니다. 이를 위해 MQL5는 각각 고유한 책임 영역을 가진 여러 핸들러를 제공합니다. OnTick은 시장 업데이트를 담당합니다. OnTimer — 주기적 및 백그라운드 작업을 담당합니다. OnChartEvent — GUI 및 사용자 동작에 대한 반응을 담당합니다. 논리가 목적에 따라 배분되면 코드가 더 이상 복잡해지지 않게 되고 각각의 모듈이 제 역할을 수행하며 이웃 모듈과 간섭하지 않는 잘 구성된 엔지니어링 시스템을 닮아가기 시작합니다.

이러한 설계는 EA가 단일 심볼을 넘어 여러 함수들을 동시에 수행하기 시작할 때 특히 중요합니다. 우리는 시장을 모니터링하고 인터페이스를 유지하고 버튼 입력에 응답하고 내부 상태를 동기화하고 구성 요소 간에 신호를 전송하고 때로는 여러 종목을 관리해야 합니다. 이 시점에서 보면 이벤트 아키텍처는 아름다운 이론처럼 보이지 않습니다. 실질적인 필수적인 요소가 되는 것입니다. 백그라운드 작업을 OnTimer로 옮기고 사용자 동작에 대한 응답을 OnChartEvent 로 처리하면 메인 매매 회로의 불필요한 부하가 줄어듭니다. 이렇게 하면 시스템의 동작을 더 잘 예측할 수 있고 유지 관리가 훨씬 간편해집니다.

사용자 정의 이벤트 및 서비스는 이러한 아키텍처에서 별도의 역할을 수행합니다. 사용자 정의 이벤트를 사용하면 모듈 간의 내부 메시지 버스를 구성할 수 있으며 서비스를 사용하면 보조 로직을 특정 차트의 외부로 이동할 수 있습니다. 이것은 더 이상 단순한 EA가 아닙니다. 다른 것을 하나의 함수로 통합하지 않고도 명령과 신호를 교환할 수 있는 구성 요소들의 시스템입니다. 이를 통해 제어판, 백그라운드 분석, 모듈 간 교환 및 명확한 역할 분리 기능을 갖춘 트레이딩 애플리케이션 수준의 솔루션을 구축할 수 있습니다.

또한 이벤트 기반 접근 방식은 테스트 가능성을 크게 향상시킵니다. 로직이 여러 핸들러에 분산되어 있으면 부분적으로 테스트하기가 더 쉽습니다. 타이머, UI 이벤트 및 매매 내역에 대한 반응을 각각 분석할 수 있게 됩니다. 이는 실수로 인해 시간과 비용의 손실이 일어나는 작업에서 특히 중요합니다. 구조가 좋을수록 시스템 동작을 제어하고 오류의 원인을 신속하게 찾아내기가 더 쉬워집니다.

이 글에서는 모든 것을 OnTick에 두는 모델에서 좀더 성숙한 이벤트 아키텍처로 전환하는 방법을 살펴보겠습니다. 미리 정의된 핸들러와 사용자 지정 이벤트 그리고 차트에 연결되지 않은 서비스들의 역할을 살펴보겠습니다. 또한 실제 작업이 시작되기도 전에 아키텍처를 망가뜨리는 일반적인 오류들에 대해서도 좀 더 자세히 살펴보겠습니다. 핵심은 간단합니다. MQL5를 본래 목적대로 사용하면 트레이딩 로봇 뿐만 아니라 완전한 기능을 갖춘 애플리케이션 시스템까지 구축할 수 있다는 것입니다.

MQL5 이벤트



미리 정의된 이벤트

MQL5에서 미리 정의된 이벤트는 프로그램 전체 로직의 기반이 되는 프레임워크입니다. 이는 단순히 핸들러 함수들의 집합이 아니라 환경 변화에 대한 반응을 엄격하게 정의한 모델입니다. 개발자가 이들을 OnTick에 추가되는 요소 로 인식하지 않을수록 코드는 더 빨리 아키텍처를 갖추기 시작할 것입니다.

모든 프로그램의 생명주기는 OnInit으로 시작하여 OnDeinit으로 끝납니다. 이 둘은 일종의 진입점과 출구점입니다. OnInit은 기초를 다집니다. 타이머는 EventSetTimer을 통해 설정되고 그래픽 객체가 생성되며 내부 구조와 외부 구성 요소가 초기화됩니다. 이곳에서 서비스 메커니즘을 실행하고 환경을 준비하는 방법도 편리합니다.

int OnInit()
  {
   EventSetTimer(30); // poll every 30 seconds
   return(INIT_SUCCEEDED);
  }

OnDeinit은 규율을 요구합니다. 생성된 모든 것이 반드시 올바르게 해제되어야 합니다: 객체는 삭제되고 타이머는 EventKillTimer를 통해 비활성화되며 리소스는 닫혀야 합니다. 이러한 대칭성을 무시하는 것은 런타임 환경을 지저분하게 만들고 미묘한 오류를 유발하는 전형적인 실수입니다.

void OnDeinit(const int reason)
  {
   EventKillTimer(); // disable the timer when unloading
  }

OnTick은 전통적으로 EA의 핵심으로 여겨집니다. 이는 부분적으로 사실입니다. OnTick의 목적은 특정 종목의 시장 변화에 반응하는 것입니다. 이곳이 바로 신호를 계산하고 진입 조건을 확인하고 포지션을 관리하는 곳입니다. 하지만 핵심은 OnTick의 존재가 필수 사항이 아니라는 점입니다. 로직을 다른 이벤트로 옮기면 EA는 OnTick 없이도 작동할 수 있습니다. 성숙한 아키텍처에서 OnTick우주의 중심이 되기보다는 경량 라우터의 역할을 하는 경우가 많습니다.

OnTimer는 많은 사람들이 과소평가하는 도구입니다. 터미널은 지정된 시간 간격으로 이러한 이벤트를 생성합니다. 이를 통해 백그라운드 작업과 시장과 관련한 작업을 구분할 수 있습니다. 통계 수집, 캐시 업데이트, 여러 심볼 폴링, 지표 재계산 등 — 이 모든 작업은 논리적으로 타이머에 포함됩니다. 이러한 접근 방식은 OnTick의 부담을 줄여주고 특히 변동성이 높은 기간 동안 시스템의 동작을 더욱 안정적으로 만듭니다. 간단하지만 중요한 규칙을 기억하셔야 합니다: 타이머는 OnInit에서 설정되어야 하고 OnDeinit에서 반드시 제거되어야 합니다.

OnChartEvent는 인터랙티브 애플리케이션의 세계로 가는 문을 열어줍니다. OnChartEvent는 GUI 이벤트, 즉 마우스 클릭, 키 입력, 객체와의 상호 작용, 그리고 가장 중요한 사용자 지정 이벤트를 처리하는 핸들러로서 제어판, 버튼 및 모드 전환 스위치가 이를 통해 만들어집니다. 이곳은 다른 프로그램에서 오는 외부 신호에 대한 응답을 구현하는 곳입니다. 이벤트 기반 아키텍처의 맥락에서 보면 이것은 더 이상 단순한 UI 핸들러가 아니라 완전한 기능을 갖춘 통신 채널입니다.

OnTradeTransaction은 거래 상태 제어 지점입니다. 간소화된 접근 방식은 연산 결과만 확인하는 데 비해 여기서는 전체 MqlTradeTransaction 구조체를 사용할 수 있습니다. 이를 통해 주문 수명 주기를 정확하게 추적할 수 있습니다: 주문 전송부터 실행 및 후속 적인 수정에 이르기까지. 이러한 수준의 세부 정보는 매매의 목표뿐만 아니라 목표 달성 과정 또한 중요한 복잡한 포지션 관리 시나리오에서 특히 중요합니다.

OnBookEvent는 사용 빈도가 낮지만 특정 작업에서는 대체 불가능한 존재입니다. 이는 시장 심도의 변화에 반응하며 MarketBookAdd를 통해 활성화됩니다. 이는 가격 뿐만 아니라 유동성 구조까지 고려한 더욱 정교한 전략의 영역입니다. 고빈도 분석 방식이나 미시적 시장 움직임을 분석할 때 이 핸들러는 핵심적인 신호 소스가 됩니다.

여기서 핵심은 간단합니다: MQL5 내의 모든 프로그램은 고유한 이벤트 세트를 수신하며 이러한 이벤트를 통해 프로그램의 동작이 결정된다는 것입니다. 이 모델을 무시하고 모든 것을 단일 핸들러로 축소하려는 시도는 필연적으로 복잡함만 늘어날 뿐입니다. 반대로 이벤트 전반에 걸쳐 논리를 적절하게 분배하면 명확하고 예측 가능하며 확장 가능한 시스템이 구축됩니다 — 이는 앞으로 나아가기 위한 필수적인 토대입니다.



사용자 정의 이벤트

MQL5의 사용자 정의 이벤트는 이벤트 아키텍처를 구축하는 데 가장 실용적인 도구 중 하나입니다. 이를 통해 프로그램은 더 이상 닫혀 있는 프로그램이 아니게 되는 것입니다. 마치 단일 시스템의 노드들이 서로 서비스 메시지를 교환하는 것처럼 사용자 정의 이벤트 또한 자연스럽게 구성 요소들 간에 신호를 전송할 수 있는 능력을 갖게 됩니다. 이는 EventChartCustom 함수를 통해 구현되며 이 함수를 사용하면 지정된 차트에 자체 이벤트를 프로그래밍 방식으로 보내고 OnChartEvent 에서 이를 처리할 수 있습니다. 이것은 내부 메시지 버스인 것입니다. 이는 특히 단일 프로그램이 시장 요구에 대응하고 또한 인터페이스, 서비스 모듈 및 매매 로직의 작동을 조정해야 하는 경우에 매우 유용합니다.

이 접근 방식의 주요 장점은 모듈들 간의 책임을 분리하는 데에 도움이 된다는 것입니다. 한 구성 요소는 시장 분석을 하고 있습니다. 다른 하나는 - 패널 상태를 표시하고 있습니다. 세 번째 것은 - 매매를 하고 있습니다. 네 번째 것은 - 백그라운드 계산 서비스를 제공하고 있습니다. 이 모든 것을 직접 호출과 공유 전역 변수로 연결하려고 하면 하자 마자 코드가 금방 복잡해지기 시작할 것입니다. 유지보수는 마치 모든 톱니바퀴가 서로 맞물려 있는 기계를 수리하는 것과 같아질 것입니다. 사용자 정의 이벤트는 이러한 혼란을 방지합니다: 모듈은 다른 것의 내부 로직에 간섭하지 않고 단순히 신호를 받아야 할 곳으로 깔끔한 신호를 보냅니다.

표적 이벤트 전송 기능은 별도로 주목할 만한 가치가 있습니다. EventChartCustom에서는 현재 차트 뿐만 아니라 다른 창의 chartID도 지정할 수 있습니다. 이는 다양한 EA, 지표 및 서비스 간의 소통을 위한 길을 열어줍니다. 이 메커니즘은 특히 여러 심볼과 여러 차트를 사용하는 시스템에서 유용합니다. 구성 요소 하나는 정보를 수집하거나 생성하고 다른 구성 요소는 정보를 수신하여 작업을 수행합니다. 이 경우 사용자 지정 이벤트는 서로 다른 부분을 하나의 아키텍처로 통합하는 연결 고리가 됩니다.

더욱이 이벤트 교환이라는 개념은 초기화 단계에서 특히 두드러지게 나타납니다. 때때로 OnInit 메서드에 내부 객체 생성, 데이터 로딩, 캐시 구축, 인터페이스 구성, 서비스 실행과 같은 준비 작업을 추가해야 할 때가 있습니다. 이 모든 작업을 초기화 단계에서 즉시 수행하면 메서드가 무거워지고 프로그램 시작 속도가 느려지기 시작합니다. 게다가 초기화에 시간이 너무 오래 걸리면 시간 제한에 걸리거나 시작이 느리고 버벅거릴 위험이 있습니다. OnInit 에는 최소한의 준비 작업만 남겨두고 많은 작업이 필요한 부분을 별도의 핸들러로 옮기는 사용자 지정 이벤트를 생성하는 것이 훨씬 더 합리적입니다. 그러면 프로그램이 빠르게 시작되고 주요 준비 작업은 이벤트 모드에서 진행됩니다.

MQL5에서는 함수를 장기적으로 실행하면 다른 이벤트 처리를 차단하기 때문에 이 점도 중요합니다. 여러 개의 작은 함수를 연속해서 호출하는 것은 문제를 해결하지 못합니다: 함수 호출 체인이 완료될 때까지는 어떤 것도 이벤트 흐름에 개입할 수 없기 때문입니다. 이는 사용자 인터페이스가 있는 프로그램에서 특히 두드러집니다. 사용자가 패널과 상호 작용하고 응답을 기다리지만 프로그램이 긴 스크립트를 실행 중이기 때문에 응답하지 않습니다. 마치 시스템이 멈춘 듯한 느낌이 들지만 실제로는 시스템이 이벤트 핸들러로 제어권을 넘길 시간이 부족한 것뿐입니다. 바로 이러한 이유 때문에 이벤트 모델이 아키텍처적으로 필수적인 것입니다. 이렇게 하면 인터페이스를 차단하거나 프로그램이 응답하지 않게 만들지 않고 단계를 순차적으로 실행하는 이벤트 핸들러에 그 지휘를 넘겨줄 수 있습니다.

이러한 유연성에는 단점이 있습니다: 시스템 없이는 사용자 지정 이벤트를 보낼 수 없습니다. 명확한 프로토콜이 필요합니다: 즉 각각의 ID가 무엇을 의미하는지, 메시지 코드는 어디에 저장되는지, 이벤트가 해당 애플리케이션에 속하는지 여부를 어떻게 판단하는지 등을 명확히 해야 합니다. 일반적으로 이러한 목적을 위해 사용자 지정 ID 범위를 설정하고 문자열 매개변수에 명확한 레이블을 사용하며 이벤트가 실제로 이 모듈을 대상으로 하는지 확인해야 합니다. 그렇지 않으면 관련 없는 신호가 발생하여 논리 구조에 혼란이 생길 수 있습니다.

흔히 간과되는 또 다른 규칙이 있습니다: 정말 필요한 경우가 아니라면 너무 자주 이벤트를 만들지 마십시오. 터미널 이벤트 큐는 불필요한 노이즈를 좋아하지 않습니다. 이벤트가 너무 자주 전송되면 메시지 큐가 과부하 되어 신호 처리 성능이 저하됩니다. 데이터 준비 완료, 신호 발생, 인터페이스 업데이트 명령, 작업 실행 확인과 같은 중요한 상태만 전송하는 것이 좋습니다. 그러면 이벤트 처리는 혼란스러운 전보가 오고 가는 것이 아니라 깔끔하게 정리된 메모처럼 진행될 것입니다.

이것이 바로 성숙한 시스템에서 사용자 지정 이벤트가 특히 유용한 이유입니다. 이러한 기능은 엄격한 결합 없이 모듈을 연결하고 명확한 상호 작용 프로토콜을 구축할 수 있도록 하며 MQL5를 완전한 이벤트 기반의 개발 환경으로 전환합니다.



서비스

MQL5에서 서비스는 백그라운드 터미널 서비스입니다. 차트도 없고 심볼 연동도 없고 틱 대기도 없습니다. 일단 작동시키면 마치 무엇을 해야 할지 알려줄 필요가 없는 깔끔한 비서처럼 스스로 작동합니다.

모든 로직은 OnStart 함수에 있습니다. 대부분의 경우 그 주기는 내부에 구성되어 있습니다: 상태 확인, 작업 수행, 절전 모드를 통한 일시 정지, 그리고 다음 단계로 이어지는 일련의 과정. 이 리듬은 단순하면서도 안정적입니다. 시장 상황을 필요로 하지 않으며 차트 활동에 의존하지도 않습니다. 서비스는 자신만의 자체 속도로 세팅 되어 진행됩니다.

실질적인 이점은 분명합니다. 외부 소스에서 정기적으로 데이터를 다운로드해야 하는 경우 서비스가 이를 담당합니다. 사용자 지정 심볼에 대한 틱을 생성해야 하나요? 서비스는 그것도 할 수 있습니다. 몇 초마다 신호를 보내거나 시스템 상태를 업데이트해야 하나요? 이것 또한 서비스가 합니다. 반복되는 모든 것 시장의 변동에 좌우되어서는 안 되는 모든 것은 논리적으로 보면 여기에 모여 있어야 합니다.

이 서비스를 특히 편리하게 만들어주는 또 다른 한 가지 세부 사항이 있습니다. 수동으로 중지하지 않으면 다음에 터미널을 시작할 때 자동으로 시작됩니다. 이를 통해 시스템 시작 시 상태 확인, 데이터 준비, 동기화 등 반복적인 작업을 위한 도구로 활용할 수 있으며 이 모든 작업이 사용자의 개입 없이 수행될 수 있습니다. 일단 설정이 완료되면 서비스는 매일 정확하게 작동합니다.

아키텍처 적으로 보면 이는 매우 바람직해 보입니다. 차트에 표시된 EA는 시장 상황에 반응합니다. 동시에 인터페이스는 사용자 중심적입니다. 한편 해당 서비스는 백그라운드에서 조용하고 꾸준하게 작업을 수행합니다. 번거로움도 없고 OnTick에 과부하가 걸리지도 않습니다. 이러한 역할 분담은 시스템의 불필요한 스트레스를 줄이고 더욱 안정적으로 만듭니다. 각 부분이 제 역할을 다할 때 전체 구조는 눈에 띄게 더 깨끗하게 작동하기 시작합니다.



프로그램 간 통신

트레이딩 솔루션이 여러 구성 요소로 이루어져 있을 경우 구성 요소 간에 메시지를 주고받을 수 있는 메커니즘이 필요합니다. 하나의 모듈이 데이터를 수집할 수 있습니다. 두 번째 모듈은 데이터를 패널에 보여줍니다. 세 번째 모듈은 매매 결정을 내립니다. 네 번째 모듈은 백그라운드 로직을 처리합니다. 만약 각각의 모듈이 자신만의 영역에 갇히기 시작한다면 그 결과는 더이상 아키텍처가 아니라 플래그, 전역 변수, 그리고 무작위 검사들이 뒤엉킨 혼란이 될 것입니다. 바로 이러한 이유 때문에 이벤트 기반 구조가 여기에 특히 효과적입니다.

이벤트 기반 아키텍처의 깔끔한 버전은 지표가 신호의 원천이 되는 경우인데 기존 방식(버퍼에 값을 요청하고 받는 방식)이 아니라 완전한 이벤트 생성기로서의 기능을 하는 경우입니다.

아이디어는 간단합니다. 각 지표는 가벼운 어댑터로 감싸져 있습니다. 내부적으로는 정확히 한 가지 일만 수행합니다: 신호 발생 시점을 모니터링하고 조건이 충족되면 사용자 지정 이벤트를 발생시킵니다. 모든 준비가 끝난 것입니다. 외부의 끊임없는 요청도 없고 불필요한 소음도 없습니다.

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   if(prev_calculated==rates_total)
     return prev_calculated;
//---
   if(BarsCalculated(handle) < rates_total)
      return(prev_calculated);
   vector<double> sig, main;
   if(!main.CopyIndicatorBuffer(handle, 0, 1, 2) ||
      !sig.CopyIndicatorBuffer(handle, 1, 1, 2))
      return(prev_calculated);
   if(sig[0] > main[0] && sig[1] <= main[1])
     {
      if(!EventChartCustom(ChartID(), BuyID, MagicNumber, main[0], IndComment))
         return(prev_calculated);
     }
   if(sig[0] < main[0] && sig[1] >= main[1])
     {
      if(!EventChartCustom(ChartID(), SellID, MagicNumber, main[0], IndComment))
         return(prev_calculated);
     }
//--- return value of prev_calculated for the next call
   return(rates_total);
  }

예를 들어 두 가지 고전적인 지표를 살펴보겠습니다 — MACDRSI. 각각은 독립적으로 자체적인 시간표에 따라 작동합니다. 그들은 새로운 바 만을 담당합니다. MACD는 히스토그램과 시그널 라인의 교점을 확인합니다. RSI는 과매수 및 과매도 수준의 교차점입니다. 신호가 발생하면 이들은 미리 정의된 사용자 지정 이벤트를 생성합니다. 이것이 핵심입니다. 지표는 버퍼에 신호를 저장하는 것이 아니라 이벤트를 기록합니다.

그러면 EA가 동작하기 시작하지만 이제 완전히 다른 역할을 하게 됩니다. EA가 매 틱 마다 지표를 폴링하지는 않습니다. 과거 기록을 살펴보지 않고 버퍼를 동기화하지 않으며 신호가 있었는지 여부를 추측하려고 시도하지도 않습니다. EA는 OnChartEvent에서 발생하는 이벤트를 수신하기만 합니다.

void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(lparam != MagicNumber)
      return;
   switch(id - CHARTEVENT_CUSTOM)
     {
      case MACD1Buy:
         indSignals[0] = 1;
         break;
      case MACD1Sell:
         indSignals[0] = -1;
         break;
      case MACD2Buy:
         indSignals[1] = 1;
         CloseSellSignal = true;
         CloseBuySignal = false;
         break;
      case MACD2Sell:
         indSignals[1] = -1;
         CloseBuySignal = true;
         CloseSellSignal = false;
         break;
      case RSI1CrossOverBoughtDown:
         indSignals[2] = -1;
         break;
      case RSI1CrossOverSoldUp:
         indSignals[2] = 1;
         break;
      case RSI2CrossOverBoughtDown:
         indSignals[3] = -1;
         CloseBuySignal = true;
         CloseSellSignal = false;
         break;
      case RSI2CrossOverSoldUp:
         indSignals[3] = 1;
         CloseSellSignal = true;
         CloseBuySignal = false;
         break;
     }
   BuySignal = true;
   SellSignal = true;
   for(uint i = 0; i < indSignals.Size() && (BuySignal || SellSignal); i++)
     {
      BuySignal = BuySignal && (indSignals[i] == 1);
      SellSignal = SellSignal && (indSignals[i] == -1);
     }
  }

EA는 수신되는 이벤트를 기록합니다. 아무것도 도착하지 않으면 아무것도 하지 않습니다.

이는 부하의 성격을 극적으로 변화시킵니다. 지표를 폴링하는 기존 구조에서는 매 틱 마다 동일한 작업이 수행됩니다 - 버퍼를 요청하고 값을 확인하고.... 때로는 중요한 신호를 놓치지 않기 위해 히스토리를 되짚어볼 필요가 있을 수도 있습니다. 여러 지표와 서로 다른 기간이 있을 경우 이는 특히 어려워집니다. 신호 사이에는 항상 시간 간격이 존재하며 우리는 그 간격을 포착하기 위해 끊임없이 뒤를 돌아봐야 합니다.

이벤트 모델은 이러한 불필요한 노력을 간단히 제거해 줍니다. 각 신호는 이미 완성되어 준비된 이벤트입니다. 신호를 검색하거나 재계산하거나 사후적으로 재구성할 필요가 없습니다. 그것은 출현하는 순간에 나타납니다.

그 결과 전체 흐름은 깔끔하고 신속한 구조로 정리됩니다:

  • 지표는 자체 계산 및 자체 신호에 대해서만 책임을 집니다;
  • 이벤트는 상태 변화의 순간을 기록합니다;
  • EA는 신호들을 종합하여 결정을 내립니다.

이는 특히 다중 지표 전략에서 두드러지게 나타납니다. 예를 들어 하나의 신호는 M5에서 오고 다른 신호는 M30에서 옵니다. 고전적인 모델에서는 데이터를 동기화 하고 시간 지연을 고려하고 히스토리 데이터를 확인해야 합니다. 여기서는 모든 것이 더 간단합니다: 각 지표가 정확한 순간에 신호를 보내주었고 EA는 그 상태를 저장한 후 모든 조건이 충족될 때까지 기다렸습니다.

성능 면에서 이는 상당한 이점을 제공합니다. 지표에 대한 지속적인 참조도 없고 매 틱 마다 빈 체크 표시도 없습니다. OnTick은 사용하기 쉽고 빠르기 때문에 시장 변화에 더 빠르게 대응할 수 있습니다. 매매에서 이는 단순한 이론이 아니라 완전히 실질적인 이점입니다: 지연이 줄어들고 논리가 명확 해지며 실행이 더욱 정확 해집니다.

void OnTick()
  {
//---
   if(BuySignal)
     {
      cSymbol.Refresh();
      cSymbol.RefreshRates();
      if(!cTrade.Buy(InpLot, cSymbol.Name(), cSymbol.Ask(),
                     cSymbol.Ask() - SL * cSymbol.Point(),
                     cSymbol.Ask() + TP * cSymbol.Point(), "Event Example"))
        {
         PrintFormat("Error open Buy position: %d", GetLastError());
         return;
        }
      BuySignal = false;
      ArrayFill(indSignals, 0, indSignals.Size(), 0);
     }
//---
   if(SellSignal)
     {
      cSymbol.Refresh();
      cSymbol.RefreshRates();
      if(!cTrade.Sell(InpLot, cSymbol.Name(), cSymbol.Bid(),
                      cSymbol.Bid() + SL * cSymbol.Point(),
                      cSymbol.Bid() - TP * cSymbol.Point(), "Event Example"))
        {
         PrintFormat("Error open Sell position: %d", GetLastError());
         return;
        }
      SellSignal = false;
      ArrayFill(indSignals, 0, indSignals.Size(), 0);
     }
//---
   if(CloseBuySignal)
     {
      if(cPosition.SelectByMagic(cSymbol.Name(), MagicNumber) &&
         cPosition.PositionType() == POSITION_TYPE_BUY)
        {
         if(!cTrade.PositionClose(cPosition.Ticket()))
           {
            PrintFormat("Error close Buy position: %d", GetLastError());
            return;
           }
        }
     }
//---
   if(CloseSellSignal)
     {
      if(cPosition.SelectByMagic(cSymbol.Name(), MagicNumber) &&
         cPosition.PositionType() == POSITION_TYPE_SELL)
        {
         if(!cTrade.PositionClose(cPosition.Ticket()))
           {
            PrintFormat("Error close Sell position: %d", GetLastError());
            return;
           }
        }
     }
//---
  }

또한 중요한 점은 코드가 더욱 투명 해진다는 점입니다. 신호는 더 이상 해석이 필요한 버퍼 값이 아니라 발생 - 등록 - 처리된 명확한 이벤트로 바뀝니다. 이 경우가 바로 아키텍처가 시스템의 가독성과 동작을 직접적으로 개선하는 경우인 것입니다.

MetaTrader 5 전략 테스터에서 EA를 실행해 보니 흥미로운 결과가 나타났습니다. 이 시스템은 확실한 특징을 가지고 있습니다 - 적극적으로 수익을 창출하면서도 동시에 위험을 합리적인 범위 내로 유지합니다.

테스트 결과 테스트 결과

1,000달러를 예치했을 때 3년 만에 2,427.52달러, 즉 143%의 수익률을 기록했습니다. 특히 하락폭이 크지 않았다는 점을 보면 결과는 매우 긍정적입니다. 평가 금액의 곡선은 깔끔해 보입니다. 성장은 단계적으로 이루어지고 하락세도 있지만 장기간의 침체는 없습니다. 최대 드로다운은 약 12~13%이며 회복률은4.6입니다 — 시스템은 손실 구간에 갇히지 않고 회복할 수 있습니다. 이는 지속가능성의 중요한 지표입니다.

전형적인 수익 구조는 제대로 작동하고 있습니다 총 18건의 거래, 승률 50%. 하지만 평균 이익은 평균 손실보다 거의 4배나 큽니다. 이를 통해 Profit factor3.9 수준에 도달할 수 있었습니다. 이 전략은 거래의 빈도를 통해서가 아니라 신호의 질과 포지션 유지를 통해 수익을 창출합니다. 하지만 여기에 한계가 있습니다. 거래 건수가 적어 통계 자료를 신뢰할 수 없습니다.

평가 금액의 곡선은 급격한 변동 없이 상당히 매끄럽습니다. 이는 신호가 무작위적이지 않다는 것을 간접적으로 확인시켜 줍니다.

그리고 여기에 아키텍처적으로 중요한 포인트가 있습니다. 이벤트 기반 모델은 지표에 대한 지속적인 폴링을 없애 줍니다. EA는 신호에만 반응합니다. 불필요한 작업이 줄어들면 대응 속도가 빨라집니다. 테스트 환경에서는 사소한 차이에 불과하지만 실제 거래에서는 이점이 됩니다.

결과는 간단합니다. 이 모델은 위험 대비 보상 비율이 우수하고 아키텍처가 잘 설계된 신호 모델입니다. 하지만 지금은 시제품 단계입니다. 표본 외에서 테스트해보고 확장된 통계 분석이 필요합니다.



흔히 저지르는 실수

아키텍처가 이벤트 기반으로 바뀌면 코드가 더 깔끔 해지고 속도도 빨라집니다. 하지만 동시에 오류의 성격도 바뀝니다. 오류는 더 이상 표면에만 머무르지 않고 핸들러들 간의 연결과 자원을 활용하는 작업 방식 속에서 드러납니다.

가장 먼저 떠오르는 시급한 문제는 바로 기존의 OnTick의 과부하 문제입니다. 이벤트 모델에 익숙해진 후에도 만일의 사태에 대비해 로직의 일부를 그대로 남겨두고 싶은 유혹이 있을 것입니다. 그 결과는 하이브리드 방식입니다. 타이머와 이벤트가 존재하지만 대부분의 작업은 여전히 단일 메서드에 의해 수행됩니다. 이러한 타협은 우리를 금세 원점으로 되돌려 놓습니다. 즉 무겁고 제대로 관리되지 않는 코드라로 말입니다. 경험에 비추어 볼 때 우리가 이미 역할을 분담하도록 했다면 끝까지 그렇게 가야 합니다.

다음 단계의 오류는 자원 관리와 관련이 있습니다. 이벤트 모델에는 더 많은 요소가 포함됩니다: 타이머, 객체, 구독, 보조 구조. 그리고 만약 제대로 관리하지 않으면 시스템에서 문제가 발생 하기 시작합니다. EventKillTimer를 잊어버리거나 제거되지 않은 그래픽 요소, 그대로 남아 있는 전역 변수 등은 프로그램이 즉시 오류를 일으키지는 않지만 점차 예측 성능을 떨어뜨립니다. 결과적으로 오류가 발생한 곳이 아닌 다른 곳에 오류가 나타나는 전형적인 상황이 발생합니다.

void OnDeinit(const int reason)
  {
//---
   for(uint i = 0; i < handles.Size(); i++)
      if(handles[i] != INVALID_HANDLE)
         IndicatorRelease(handles[i]);
  }

이런 이벤트의 본질적인 특성상 특별한 주의가 필요합니다. 핸들러는 순차적으로 실행되며 하나의 이벤트 내에서 시간이 오래 걸리는 작업은 다른 이벤트의 실행을 차단합니다. 이러한 현상은 특히 인터페이스에서 두드러지게 나타납니다: 사용자는 패널과 상호 작용하지만 프로그램은 반응하지 않습니다. 계산 중이라 응답하지 못하기 때문입니다. 여기서도 아키텍처의 핵심 아이디어가 분명하게 드러납니다: 즉 실행을 단계별로 나누고 스레드가 실제로 필요한 시간 이상으로 계속 바쁘게 작동하지 않도록 하는 것이 필수적입니다.

시스템에 여러 이벤트 소스(표시 장치, 패널, 서비스)가 나타날 경우 식별 원칙이 매우 중요 해집니다. 객체 이름과 이벤트 코드에 대한 명확한 규칙을 도입하지 않으면 혼란이 발생합니다. 신호가 겹치고, 핸들러가 잘못된 것들에 반응하게 됩니다. 접두사와 ID 범위를 사용하는 간단한 규칙은 중요한 부분은 아니라고 보일수도 있지만 시스템이 커지기 시작할 때 질서를 유지하는 데 필수적인 요소입니다.

#define  MACD1Buy                   1
#define  MACD1Sell                  2
#define  MACD2Buy                   3
#define  MACD2Sell                  4
#define  RSI1CrossOverBoughtUp      5
#define  RSI1CrossOverBoughtDown    6
#define  RSI1CrossOverSoldUp        7
#define  RSI1CrossOverSoldDown      8
#define  RSI2CrossOverBoughtUp      9
#define  RSI2CrossOverBoughtDown    10
#define  RSI2CrossOverSoldUp        11
#define  RSI2CrossOverSoldDown      12

그리고 마지막으로 가장 까다로운 부분은 프로그램의 시작입니다. 이벤트 기반의 모델에서는 초기화가 완전히 완료되기 전에도 이벤트가 즉시 발생할 수 있습니다. 이 시점에서 구조가 준비되지 않으면 빈 데이터나 잘못된 상태에 접근하게 될 가능성이 높습니다. 따라서 초기화는 핸들러가 시작되기 전에 엄격하게 완료되어야 하거나 또는 (종종 더 편리한 방식인) 동일한 사용자 지정 이벤트를 통해 준비 상태를 제어하면서 단계별로 나누어 처리되어야 합니다.

결론적으로 이벤트 그 자체만으로는 시스템의 신뢰성을 보장할 수 없다는 것이 분명합니다. 이벤트들은 도구만 제공할 뿐입니다. 신뢰성은 이러한 도구에 엔지니어링 원칙이 더해질 때 확보됩니다. 만약 그러한 시스템이 존재한다면 이벤트 기반 아키텍처는 단순히 편리한 기술을 넘어 안정적이고 예측 가능한 트레이딩 시스템의 기반이 될 것입니다.



결론

이벤트 기반으로 프로그램 로직이 구축되면 플랫폼은 단순히 EA를 위한 플래폼의 역할이 아니라 완전한 애플리케이션 솔루션의 기반이 됩니다. 이 모델에서는 프로그래밍이 기존 데스크톱 애플리케이션을 개발하는 것과 유사해집니다: 버튼과 패널이 있는 인터페이스가 나타나고 백그라운드 서비스가 실행되며 다양한 유형의 이벤트에 대한 별도의 핸들러가 생성됩니다. 모든 것이 제자리에 있게 되고 따라서 시스템 유지 관리가 훨씬 더 안정적으로 됩니다.

이벤트 기반의 접근 방식은 일반적인 EA가 모든 상황에 대한 단일 파일의 역할을 더 이상 처리할 수 없을 때 특히 유용합니다. 이를 통해 다중 심볼 지원 장치, 유연한 제어 패널 및 상태의 의미 있는 변화에 반응하는 복잡한 컨트롤러를 구축할 수 있습니다. 이 아키텍처는 새로운 기능을 추가하기가 더 쉽습니다: 전체 코드를 다시 작성할 필요 없이 다른 이벤트나 핸들러를 도입하기만 하면 됩니다.

이것이 바로 이벤트 기반 아키텍처의 주요 장점입니다: 유연성을 잃지 않으면서도 질서 있게 만든다는 것입니다. 핸들러, 서비스 및 통신 채널을 적절히 분리하면 안정적이고 확장 가능하며 진정으로 역동적인 트레이딩 시스템을 구축할 수 있습니다. MQL5는 전통적인 EA를 넘어 복잡한 트레이딩 애플리케이션을 위한 환경으로 확장됩니다. 여기서는 시장에 대한 반응 뿐만 아니라 프로그램 전체의 성숙한 엔지니어링 조직화 또한 중요합니다.


이 글에서 사용된 프로그램

# 이름 타입 설명
1 EventExample.mq5 Expert Advisor 이벤트 처리 EA
2 EventMACD.mq5 지표 MACD 라인이 교차할 때 이벤트를 생성하는 지표
3 EventRSI.mq5 지표 레벨 교차 시 이벤트가 발생하는 RSI 지표

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

파일 첨부됨 |
MQL5.zip (5.12 KB)
최근 코멘트 | 토론으로 가기 (6)
fxsaber
fxsaber | 5 5월 2026 에서 15:00
//+------------------------------------------------------------------+
//| 거래 함수                                                   |
//+------------------------------------------------------------------+
void OnTrade()
  {
   Sleep(0);
//---
  }
//+------------------------------------------------------------------+
//| TradeTransaction 함수                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   Sleep(0);
//---
  }
왜요?
fxsaber
fxsaber | 5 5월 2026 에서 15:02
            if(!cTrade.PositionClose(cPosition.Ticket()))
              {
               PrintFormat("Error close Sell position: %d", GetLastError());
               return;
              }
아마도 cTrade에는 오류 발생 시를 대비한 처리 코드가 포함되어 있는 것 같습니다.
fxsaber
fxsaber | 5 5월 2026 에서 15:33
fxsaber #:
버퍼가 없는 지표를 사용하여 어드바이저에서 작업하는 것—아마도 예시로 삼기에 적합할 것입니다.


이 예제에서는 심볼 데이터가 엄격하게 갱신됩니다(MQL_TESTER를 사용하는 것이 좋습니다).

void OnTick()
  {
//---
   if(BuySignal)
     {
      cSymbol.Refresh();
      cSymbol.RefreshRates();


하지만 이벤트와 틱을 통해 계산된 신호의 최신성 여부에 대한 검증이 이루어지지 않습니다. 그리고 이것이 실제 문제입니다.


비동기식 OrderSend를 통해 이 문제를 완화할 수는 있지만, 완전히 해결할 수는 없습니다. 따라서 이러한 예시에서도 ChartEvent 이벤트 내에서 이벤트가 발생한 틱의 데이터를 추가로 전달해야 합니다.

Chacha Ian Maroa
Chacha Ian Maroa | 11 5월 2026 에서 12:46
정말 좋네요. MQL5 프로그램 의 ‘서비스’ 유형은 너무 과소평가되고 있어요.
Picazzo Research and Development
Picazzo Research and Development | 11 5월 2026 에서 14:27
정말 좋은 예시네요, 감사합니다.
새로운 기능: MQL5의 커스텀 인디케이터 새로운 기능: MQL5의 커스텀 인디케이터
MetaTrader5와 MQL5의 새로운 기능 전체를 나열하지는 않겠습니다. 종류도 많은 데다가, 별도의 설명이 필요한 기능들도 있거든요. 객체 지향 프로그래밍을 이용한 코드 작성법 또한 다음에 알아보도록 하겠습니다. 다른 기능들과 함께 설명하기에는 조금 어려운 이야기일 수 있으니까요. 이 글에서는 인디케이터와 인디케이터의 구조, 드로잉 타입과 프로그래밍 디테일을 MQL4와 비교해 볼게요. 초보자 분들께 많은 도움이 되면 좋겠고 기존에 사용하시던 개발자 분들도 뭔가 새로운 걸 얻어 가실 수 있길 바랍니다.
MQL5에서 CPU에서 GPU로: 연구, 최적화 및 패턴 분석 가속화를 위한 실용적인 OpenCL 프레임워크 MQL5에서 CPU에서 GPU로: 연구, 최적화 및 패턴 분석 가속화를 위한 실용적인 OpenCL 프레임워크
OpenCL을 사용하여 MQL5에서 CPU에서 GPU로의 전환 경로를 설계하는 방법을 알아보세요. 우리는 컨텍스트 초기화, 버퍼 구성, 대용량 배치 처리, 커널 시작 및 데이터 교환을 최소화하는 데에 중점을 둘 것입니다. 이 과정에서 흔히 발생하는 오류와 이를 방지하는 방법도 함께 살펴볼 것입니다. 캔들스틱 패턴을 사용한 예시는 이 접근 방식의 실질적인 이점을 보여줍니다.
새 MetaTrader 와 MQL5를 소개해드립니다 새 MetaTrader 와 MQL5를 소개해드립니다
본 문서는 MetaTrader5의 간략 리뷰입니다. 짧은 시간 내에 시스템의 모든 세부 사항을 안내해드리기는 어렵습니다 - 테스트는 2009.09.09에 시작되었습니다. 이는 상징적인 일자로, 전 이것이 행운의 숫자가 될거라 믿어 의심치않습니다. 제가 새 MetaTrader 5 터미널과 MQL5 베타버전을 받은지 며칠이 지났습니다. 아직 모든 기능을 사용해본 것은 아니지만, 벌써부터 감명깊네요.
MetaTrader 5와 MQL5 경제 달력: 뉴스를 재현 가능한 있는 트레이딩 시스템으로 구현하는 방법 MetaTrader 5와 MQL5 경제 달력: 뉴스를 재현 가능한 있는 트레이딩 시스템으로 구현하는 방법
이 글에서는 MetaTrader 5에 내장된 경제 달력를 활용한 뉴스 트레이딩에 대한 체계적인 접근 방식에 대해 알아봅니다: 구체적으로는 데이터 구조, API 함수, 시간 동기화 규칙 및 이벤트 필터링에 대해 알아봅니다. 서버에 과부하를 일으키지 않고 캐싱 및 점진적인 업데이트를 하는 방법이 다루어집니다. 또한 동일한 알고리즘을 사용하여 결정적 테스트를 위해 히스토리를 .EX5 리소스로 내보내는 작동 메커니즘도 제공합니다.