초보자를 위한 간편 스타트 가이드
개요
여러분, 안녕하세요! 엑스퍼트 어드바이저 생성 방식이나 인디케이터 활용법을 쉽고 빠르게 이해할 수 있도록 돕고자 이번 글을 씁니다. 이 글은 초보자를 대상으로 하며 복잡하거나 난해한 예제는 포함하지 않습니다. 엑스퍼트 어드바이저 사용법을 이미 알고 계시는 분들께는 별로 유익하지 않을 거예요.
엑스퍼트 어드바이저와 엑스퍼트 어드바이저 구조
엑스퍼트 어드바이저는 MQL 언어로 쓰인 프로그램입니다.
EA 구조는 수많은 블록으로 이루어져 있다고 생각하면 되는데, 이해를 돕기 위해 MetaEditor에서 생성된 매우 간단한 예를 들어 볼게요.
전체 EA는 각각의 기능을 갖는 네 가지 파트로 나누어집니다.
그림 1. 메인 EA 블록
- 매개 변수 블록은 터미널의 EA 핸들을 돕기 위한 정보를 포함합니다. 가장 흔한 매개 변수는 EA의 버전, 제조회사명, 그리고 간단한 설명이 되겠죠.
- OnInit() 블록은 터미널에 있는 EA를 컨트롤합니다. EA 초기화에 관련된 다양한 데이터를 포함하죠. 변수와 배열을 선언한다든지, 인디케이터 핸들을 얻는다든지 하는 것들 말이예요. 따라서 거래에 직접적으로 관련된 기능은 전혀 없습니다.
- OnDeinit() 블록은 OnInit() 블록과 반대로 작동하는데요. EA가 연산(EA/터미널 셧다운 혹은 EA 초기화 실패)을 완료하고 나면 호출됩니다. 이 블록의 주된 기능 중 하나는 EA가 사용하던 메모리 공간이 더이상 필요하지 않게 되었을 때 반환하는 거죠. 다시 말해, 변수, 배열 및 인디케이터 핸들 등의 삭제를 처리합니다.
- OnTick() 블록은 서버로부터 심볼(통화쌍)에 대한 새로운 정보가 수신될 때마다 호출되는데요. 거래 조건과 거래 자체에 사용되는 함수들을 규정하죠.
그림 2. MetaEditor에서 자동으로 생성된 새 문서 예시
위의 예시를 이용해서 설명해 볼게요. '빈' 엑스퍼트 어드바이저 코드가 있습니다. 일종의 채워야 할 엑스퍼트 어드바이저 템플릿이라고 볼 수도 있겠네요. 다음을 확인할 수 있는데요.
- 처음 다섯 줄(라인1~5)은 EA의 이름(파일명), 제조사명 및 웹사이트를 포함하고 있습니다. 여기에는 원하는 정보를 입력할 수 있어요. 해당 텍스트는 어디에도 나타나지 않을 것이며 건너뛰어도 무방합니다. 이는 오직 개발자만을 위한 정보입니다.
- 그 다음 세 줄은(라인6~8)은 매개 변수 블록입니다. 해당 정보는 EA를 터미널에서 실행시킬 때 확인할 수 있죠.
- 그 다음은 OnInit() 함수(라인12~19)가 나타납니다. 이를 OnInit() 블록이라고 합니다. 이 함수는 매개 변수를 갖지는 않지만 초기화 코드를 반환합니다. 안 할 수도 있고요.
- 다음으로 OnDeinit(상수 초기화 리즌) 함수(라인22~26)가 나옵니다. 이를 OnDeinit() 블록이라고 하죠. EA의 셧다운 이유를 규정하는 하나의 매개 변수를 갖습니다. EA 초기화에 실패한 경우, 이 함수는 해당 코드를 매개 변수로 받게 됩니다.
- 마지막으로 OnTick() 함수(라인30~34)입니다. 앞서 OnTick() 블록에 쓰였는데요. 트레이딩과 관련된 모든 함수가 여기에 포함되므로 EA의 '브레인'으로 불리는 블록입니다.
전에도 말했지만 이 구조는 훨씬 더 복잡해질 수 있고 꽤 많은 블록을 포함하게 될 수도 있습니다. 간단한 이번 예시와는 완전히 다르죠. 따라서 뭔가 더 필요한 것 같으면 여러분이 직접 블록을 추가하면 됩니다.
인디케이터와 인디케이터 핸들 방식
인디케이터는 MQL로 쓰인 작은 프로그램입니다. 가격 차트 또는 가격 차트 하단의 별개의 창에 나타나며 시장의 기술적 분석을 가능케 하죠.
모든 인디케이터는 추세 추종 인디케이터와 오실레이터 두 가지로 나뉩니다.
추세 추종 인디케이터는 항상 가격 차트에 그려지며 추세의 방향을 판단합니다. 오실레이터는 가격 차트 하단에 위치하며 진입 지점을 판단하는 데에 쓰이죠.
대부분의 인디케이터가 특정 시점의 읽기 데이터를 포함하는 최소 한 개의 버퍼(인디케이터 버퍼)를 갖고 있습니다. EA처럼 인디케이터만의 심볼과 연산이 진행되는 타임프레임이 있다는 거죠.
인디케이터 버퍼는 마지막 엘리먼트가 누계값인 하나의 큐라고 볼 수 있습니다.
그림 3. 이동평균 인디케이터 예시
인디케이터 버퍼는 첫 번째 엘리먼트(인덱스 0)가 우측 끝에 위치한 캔들스틱의 데이터를 올림하고 그 다음 엘리먼트가 우측 두 번째 캔들스틱의 데이터(인덱스 1)를 올림하는 식의 배열입니다. 이런 배치를 시간열이라고 합니다.
다음의 예시를 보도록 하죠.
통화쌍이 EUR/USD이고 타임프레임은 한 시간이라고 가정할게요.
우선 인디케이터를 EA에 추가해서 핸들을 얻도록 합니다.
핸들은 프로그램 어디서든 해당 인디케이터를 처리할 수 있게 해주는 인디케이터 고유의 포인터입니다.
int iMA_handle; iMA_handle=iMA("EURUSD",PERIOD_H1,10,0,MODE_SMA,PRICE_CLOSE);더 자세히 살펴 볼까요?
첫 번째 라인은 인디케이터 핸들을 저장하는 변수를 정의합니다. 두 번째 라인은 인디케이터를 요청(이 경우 이동평균 인디케이터)하고 해당 인디케이터의 매개 변수를 설정하며 핸들을 다음에 다시 사용할 수 있도록 변수로 저장합니다.
그림 4. 이동평균 인디케이터 매개 변수 툴팁의 예시
다음의 매개 변수가 왼쪽에서 오른쪽으로 나열되어 있죠.
- 심볼 이름(툴팁에서 굵게 표시) 텍스트 매개 변수이며 통화쌍(심볼)을 나타냅니다.
- 타임프레임
- 인디케이터 주기(이 경우 평균화 주기)
- 차트 시프트 양수는 차트가 N개 만큼 앞으로 이동함을, 음수는 차트가 N개 만큼 뒤로 이동함을 의미합니다.
- 평균화 방법
- 적용 가격 또는 다른 인디케이터의 핸들
인디케이터마다 정해진 변수 세트와 형식이 있습니다. 처음 보는 인디케이터의 정보는 도움말을 참고하세요. 예를 들어 iMA라고 입력 후 F1키를 누르면 도움말 창이 나타나 해당 인디케이터에 대한 정보와 인디케이터의 모든 속성에 대한 자세한 설명을 제공합니다.
그림 5. F1키를 통한 인디케이터 설명 도움말 호출 예시
코드를 작성하고 터미널에서 EA를 실행하여 EA가 가격 차트 상단 오른쪽 코너에 나타나면 차트에 인디케이터가 보이지 않을 겁니다. 오류가 아닙니다. 의도된 것이죠. 인디케이터를 표시하려면 라인을 하나 추가해야 합니다.
ChartIndicatorAdd(ChartID(),0,iMA_handle);
이제 어떻게 되는지 볼까요? 커서를 ChartIndicatorAdd 명령 위에 두고 F1키를 눌러 도움말에서 명령의 목적에 대한 정보를 얻습니다. 다음과 같이 쓰여 있죠.
특정 차트창에 특정 핸들을 가진 인디케이터를 추가합니다.
값이 0인 두 번째 매개 변수는 보조창의 개수입니다. 일반적으로 보조창은 가격 차트 하단에서 오실레이터를 표시합니다. 기억하시죠? 엄청 많을 수도 있어요. 보조창에서 인디케이터를 나타내려면 보조창 개수를 이미 존재하는 보조창 개수보다 1만큼 크게 설정하기만 하면 됩니다. 즉, 직전 숫자의 다음 숫자를 입력하는 거죠.
라인을 다음과 같이 바꾸겠습니다.
ChartIndicatorAdd(ChartID(),1,iMA_handle);
그러면 가격 차트 하단의 보조창에 인디케이터가 나타납니다.
이제 인디케이터에서 데이터를 얻어볼 차례입니다. 편의성을 위해 배열 인덱싱을 시간열로 정렬하는 동적 배열을 선언하고 인디케이터 값을 해당 배열에 복사합니다.
double iMA_buf[]; ArraySetAsSeries(iMA_buf,true); CopyBuffer(iMA_handle,0,0,3,iMA_buf);
위의 예시에서 동적 배열인 iMA_buf[]를 2차원 배열로 선언했는데요. 이는 이동평균 인디케이터가 가격을 기반으로 하며 가격은 분수를 가지기 때문입니다.
다음 라인은 더 작은 인덱스를 가진 엘리먼트가 이전 값을 저장하고, 더 큰 인덱스를 가진 엘리먼트가 최근 값을 저장하도록 배열 인덱싱을 설정합니다. 이는 모든 인디케이터의 인디케이터 버퍼가 시계열로 인덱싱이 되어 발생하는 혼동을 피하기 위함입니다.
마지막 라인은 인디케이터 값을 iMA_buf[] 배열로 복사하는 데에 사용됩니다. 이제 모든 데이터를 사용할 수 있게 되었습니다.
주문, 거래 및 포지션
주문을 먼저 살펴봅시다.
- 주문은 거래 서버에서 수락되는 거래 요청입니다. 잘못된 요청은 거절됩니다. 거래 요청 작성의 어려움을 피하기 위해 사용할 수 있는 스탠다드 라이브러리에 대해서 후술하겠습니다. 주문은 시장 주문(즉시 실행)과 대기 주문 2가지로 나뉩니다.
시장 주문은 현재 시장가로 특정 수량만큼의 특정 금융상품을 매도하거나 매수하는 명령어입니다. 대기 주문은 특정 조건에서 거래를 실행하는 명령어이죠. 유효 기간이 지난 대기 주문은 삭제됩니다.
- 거래는 주문 실행의 결과(거래 실행 명령어)입니다. 하나의 주문으로 여러 개의 거래를 실행할 수 있는 반면 각각의 거래는 하나의 주문을 기반으로 합니다. 예를 들어 10랏을 매수하는 주문은 연속적인 부분 거래를 통해 실행될 수 있죠. 거래는 늘 거래 기록에 저장되며 수정될 수 없습니다. 터미널에서 거래는 '기록' 탭에 나타납니다.
- 포지션은 작동 중인 주문의 결과입니다. 하나의 심볼에는 롱 또는 숏의 오직 하나의 포지션만 오픈 가능합니다.
예를 들어 설명하겠습니다. 1랏에 대한 롱 포지션을 오픈한다는 것은 현재 시장가(예)로 1랏의 매수 주문을 넣는 것을 의미합니다. 요청이 유효한 경우, 서버에 전달되어 프로세싱됩니다. 프로세싱 완료 직후 터미널의 '거래' 탭에 주문 매개 변수를 갖는 포지션이 나타날 겁니다. 1랏 만큼의 또 다른 롱 포지션을 오픈한다고 가정해 봅시다. 프로세싱이 완료되면 '거래' 탭에 두 건의 주문이 아니라 2랏에 대한 하나의 포지션이 나타납니다. 즉, 포지션은 여러 주문의 실행 결과입니다.
연습을 해볼게요. 다음 구조의 필드를 채워 요청을 실행하겠습니다.
struct MqlTradeRequest { ENUM_TRADE_REQUEST_ACTIONS action; // Type of action ulong magic; // Expert Advisor ID (magic number) ulong order; // Order ticket string symbol; // Trade instrument double volume; // Requested trade size in lots double price; // Price double stoplimit; // StopLimit level of the order double sl; // Stop Loss level of the order double tp; // Take Profit level of the order ulong deviation; // Maximum allowed deviation from the requested price ENUM_ORDER_TYPE type; // Order type ENUM_ORDER_TYPE_FILLING type_filling; // Order type by execution ENUM_ORDER_TYPE_TIME type_time; // Order type by duration datetime expiration; // Order expiration time (for orders of the ORDER_TIME_SPECIFIED type) string comment; // Comment to the order };
여러 건의 주문이 있기 때문에 각각의 주문 형식은 서로 다른 필수 매개 변수 세트를 갖습니다. 여기에 대해서는 길게 이야기하지 않겠습니다. 필요한 정보는 웹사이트에서 충분히 찾아보실 수 있을 겁니다. 만약 하나의 필수 매개 변수라도 정의되지 않거나 틀리게 정의되는 경우, 요청은 실패하게 됩니다.
위의 구조는 필드를 채울 때 어려운 부분을 보다 자세히 설명하기 위해 제작되었습니다.
손절매 및 이익실현
손절매와 이익실현은 '대비책'의 역할을 하는 특수 주문입니다. 엑스퍼트 어드바이저에 오픈된 포지션에 손실이 발생하거나 실수를 한 경우 손절매를 통해 미리 정의된 가격으로 손실을 멈출 수 있습니다.
이익실현 또한 같은 방식으로 작용하지만 이익을 멈춘다는 점에서 차이가 있습니다. 포지션 청산에 대한 걱정을 덜어주죠. 특정 가격에 도달하면 포지션이 청산됩니다. 다시 말해, 이 주문들은 시장이 우리를 배신하거나 우리가 이익을 실현하고 싶을 때 사용할 수 있는 일종의 '보험'이라고 할 수 있습니다.
이러한 주문 형식은 단독으로 실행될 수 없으며 이미 존재하는 포지션에 대한 정정 방법으로만 사용됩니다.
스탠다드 라이브러리 사용하기
드디어 스탠다드 라이브러리에 대해 이야기하게 됐군요. 터미널에 기본으로 탑재되어 있는 라이브러리이기 때문에 스탠다드 라이브러리라는 이름을 붙였습니다. 라이브러리는 거래 요청 생성 등 EA의 프로그래밍을 용이하게 하고 부분적으로 복잡한 프로세스를 수행하는 함수들을 포함하고 있습니다.
트레이드 라이브러리(트레이드 클래스도 참조)는 Include\Trade\에 위치하며 #include 지시자를 이용해 추가할 수 있습니다.
예시
#include <Trade\Trade.mqh> #include <Trade\PositionInfo.mqh>
대부분의 엑스퍼트 어드바이저가 이 두 가지 클래스(라이브러리)만을 사용하여 작성되므로 위의 클래스는 아주 기초적이라고 볼 수 있습니다. 저는 클래스를 라이브러리라고 불러요.
- 첫 번째 라이브러리는 주문을 실행하고 수정하기 위해 고안되었습니다.
- 두 번째 라이브러리는 기존의 포지션에 대한 정보를 얻기 위해 사용되죠.
또 다른 라이브러리가 유용하게 쓰이는 경우도 있는데요.
#include <Trade\OrderInfo.mqh>
주문과 관련한 함수를 포함하는 라이브러리를 대기 주문이 필요한 전략에 사용하는 경우입니다. 제대로 사용하려면 공부가 좀 필요한 수많은 매개 변수로 가득한 거래 요청 구조, 기억하시죠?
이번에는 라이브러리를 이용한 거래 요청 예시를 보여 드릴게요.
CTrade m_Trade; m_Trade.Sell(lot,symbol_name,price,sl,tp,comment);
6개의 매개 변수가 있으며 이중 필수 매개 변수는 하나(첫 번째 매개 변수인 주문량)뿐입니다.
매개 변수를 하나하나 설명해 볼게요.
- lot은 실행할 주문의 크기입니다.
- symbol_name은 주문이 적용되는 심볼(통화쌍)입니다(특정된 통화쌍이 없는 경우 현재 엑스퍼트 어드바이저의 심볼 사용).
- price는 시가(거래 오픈에 사용되는 함수이므로 가격이 지정되지 않을 수 있으며 이 경우 가격 차트에서 자동으로 가격 정보 획득)를 나타내고요.
- sl는 가격이 하락하는 경우 포지션 청산이 실행되는 가격입니다(손절매를 사용하지 않는 전략의 경우 생략 가능).
- tp는 가격이 지정된 방향으로 나아가는 경우 포지션 청산이 실행되는 가격입니다(이익실현을 사용하지 않는 전략의 경우 생략 가능).
- comment는 주문의 목적 등을 설명하는 주석입니다.
포지션을 청산하는 방법은 여러가지가 있는데요.
- 전체 포지션 청산
CPositionInfo m_Position; m_Position.Select(symbol_name); m_Trade.PositionClose(symbol_name);
- 동일한 크기의 리버스 주문을 이용한 포지션 청산
CTrade m_Trade; m_Trade.Buy(lot,symbol_name,price,sl,tp,comment);
- 모든 오픈 포지션 검색 후 필요한 매개 변수(심볼, 형식, 매직 넘버, 포지션 식별자 등)을 충족하는 포지션을 선택하는 보다 복잡한 방법을 이용한 포지션 청산
초보자들에게는 어렵기 때문에 관련 예시는 다루지 않겠습니다.
배운 내용 적용하기
이제 새로 배운 내용을 엑스퍼트 어드바이저에 적용해 볼 시간입니다.
//+------------------------------------------------------------------+ //| fast-start-example.mq5 | //| Copyright 2012, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> //include the library for execution of trades #include <Trade\PositionInfo.mqh> //include the library for obtaining information on positions int iMA_handle; //variable for storing the indicator handle double iMA_buf[]; //dynamic array for storing indicator values double Close_buf[]; //dynamic array for storing the closing price of each bar string my_symbol; //variable for storing the symbol ENUM_TIMEFRAMES my_timeframe; //variable for storing the time frame CTrade m_Trade; //structure for execution of trades CPositionInfo m_Position; //structure for obtaining information of positions //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int OnInit() { my_symbol=Symbol(); //save the current chart symbol for further operation of the EA on this very symbol my_timeframe=PERIOD_CURRENT; //save the current time frame of the chart for further operation of the EA on this very time frame iMA_handle=iMA(my_symbol,my_timeframe,40,0,MODE_SMA,PRICE_CLOSE); //apply the indicator and get its handle if(iMA_handle==INVALID_HANDLE) //check the availability of the indicator handle { Print("Failed to get the indicator handle"); //if the handle is not obtained, print the relevant error message into the log file return(-1); //complete handling the error } ChartIndicatorAdd(ChartID(),0,iMA_handle); //add the indicator to the price chart ArraySetAsSeries(iMA_buf,true); //set iMA_buf array indexing as time series ArraySetAsSeries(Close_buf,true); //set Close_buf array indexing as time series return(0); //return 0, initialization complete } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { IndicatorRelease(iMA_handle); //deletes the indicator handle and deallocates the memory space it occupies ArrayFree(iMA_buf); //free the dynamic array iMA_buf of data ArrayFree(Close_buf); //free the dynamic array Close_buf of data } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int err1=0; //variable for storing the results of working with the indicator buffer int err2=0; //variable for storing the results of working with the price chart err1=CopyBuffer(iMA_handle,0,1,2,iMA_buf); //copy data from the indicator array into the dynamic array iMA_buf for further work with them err2=CopyClose(my_symbol,my_timeframe,1,2,Close_buf); //copy the price chart data into the dynamic array Close_buf for further work with them if(err1<0 || err2<0) //in case of errors { Print("Failed to copy data from the indicator buffer or price chart buffer"); //then print the relevant error message into the log file return; //and exit the function } if(iMA_buf[1]>Close_buf[1] && iMA_buf[0]<Close_buf[0]) //if the indicator values were greater than the closing price and became smaller { if(m_Position.Select(my_symbol)) //if the position for this symbol already exists { if(m_Position.PositionType()==POSITION_TYPE_SELL) m_Trade.PositionClose(my_symbol); //and this is a Sell position, then close it if(m_Position.PositionType()==POSITION_TYPE_BUY) return; //or else, if this is a Buy position, then exit } m_Trade.Buy(0.1,my_symbol); //if we got here, it means there is no position; then we open it } if(iMA_buf[1]<Close_buf[1] && iMA_buf[0]>Close_buf[0]) //if the indicator values were less than the closing price and became greater { if(m_Position.Select(my_symbol)) //if the position for this symbol already exists { if(m_Position.PositionType()==POSITION_TYPE_BUY) m_Trade.PositionClose(my_symbol); //and this is a Buy position, then close it if(m_Position.PositionType()==POSITION_TYPE_SELL) return; //or else, if this is a Sell position, then exit } m_Trade.Sell(0.1,my_symbol); //if we got here, it means there is no position; then we open it } } //+------------------------------------------------------------------+
다음의 매개 변수로 테스트를 진행하겠습니다.
- 심볼 - EURUSD
- 타임프레임 - H1
- 거래 모드 - '시초가 한정'
첫 번째 바(제로 바가 현재 활성화된 바)의 인디케이터 값과 종가부터 사용할 것이므로 차트가 다시 그려지지는 않을 겁니다. 따라서 '시초가 한정' 거래 모드를 사용할 수 있죠. 이는 테스트의 질적 측면에는 영향을 끼치지 않으면서 속도를 개선시킵니다.
다음은 과거 데이터를 이용한 테스트 결과입니다.
그림 6. 엑스퍼트 어드바이저 테스트 결과
아무래도 하락률이 눈에 띄죠. 이 글의 목적이 하락률을 최소화하여 잠재적 수익을 증가시키는 '슈퍼 엑스퍼트 어드바이저' 프로그래밍이 아니므로 괜찮습니다. 기초 지식으로도 쉽게 EA를 만들 수 있다는 걸 보여주는 거죠.
라인이 100개도 되지 않는 코드로 엑스퍼트 어드바이저를 만들었네요.
결론
이번 글에서는 EA 프로그래밍 시 고려해야 할 기본 법칙을 다루었습니다. MetaEditor5 도움말을 사용해 다양한 함수에 대한 정보를 얻는 방법을 배웠고, 주문과 포지션에 대해 알아보았으며 스탠다드 라이브러리 사용 방법 또한 살펴보았습니다.
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/496