MQL5 Cookbook: MetaTrader 5 전략 테스터의 포지션 속성 분석
소개
이 글에서는 이전 글 "MQL5 Cookbook: Position Properties on the Custom Info Panel"에서 생성된 Expert Advisor를 수정하고 다음 문제를 해결할 것입니다.
- 현재 기호에서 새 바 이벤트를 확인하는 중입니다.
- 바에서 데이터 가져오기;
- 표준 라이브러리의 트레이드 클래스를 파일에 포함합니다.
- 거래 신호를 검색하는 기능 만들기;
- 거래 작업을 실행하기 위한 기능 만들기;
- OnTrade() 함수에서 거래 이벤트를 결정합니다.
사실, 위의 문제들 각각은 그 자체로 글이 될 만한 가치가 있을 수 있지만, 제 생각에는 그러한 접근 방식은 언어 연구를 복잡하게 만들 뿐입니다.
이러한 기능을 구현하는 방법을 보여주기 위해 매우 간단한 예를 사용합니다. 즉, 위에 나열된 각 작업의 구현은 문자 그대로 하나의 간단하고 간단한 기능에 맞습니다. 시리즈의 미래 글에서 특정 아이디어를 개발할 때 우리는 이러한 기능을 당면한 작업에 필요한 만큼, 필요한 만큼 점차적으로 더 복잡하게 만들 것입니다.
먼저 모든 기능이 필요하므로 이전 문서의 Expert Advisor를 복사해 보겠습니다.
Expert Advisor 개발
표준 라이브러리의 CTrade 클래스를 파일에 포함하는 것으로 시작합니다. 이 클래스에는 거래 작업을 실행하는 데 필요한 모든 기능이 있습니다. 처음에는 내부를 보지 않고도 쉽게 사용할 수 있습니다. 이것이 우리가 할 일입니다.
클래스를 포함하려면 다음을 작성해야 합니다.
//--- Include a class of the Standard Library #include <Trade/Trade.mqh>
나중에 쉽게 찾을 수 있도록 파일 맨 처음에 이 코드를 배치할 수 있습니다. #define 지시문 뒤에 있습니다. #include 명령은 Trade.mqh 파일을 <MetaTrader 5 terminal directory>\MQL5\Include\Trade\에서 가져와야 함을 나타냅니다. 동일한 접근 방식을 사용하여 기능을 포함하는 다른 파일을 포함할 수 있습니다. 이는 프로젝트 코드의 양이 커지고 탐색하기 어려워질 때 특히 유용합니다.
이제 모든 기능에 액세스하려면 클래스의 인스턴스를 만들어야 합니다. 클래스 이름 뒤에 인스턴스 이름을 쓰면 됩니다.
//--- Load the class
CTrade trade;
이 버전의 Expert Advisor에서는 CTrade 클래스에서 사용할 수 있는 모든 기능 중 하나의 거래 기능만 사용할 것입니다. 포지션을 여는 데 사용되는 PositionOpen() 함수입니다. 기존 오픈 포지션을 취소하는 데에도 사용할 수 있습니다. 클래스에서 이 함수를 호출하는 방법은 이 글의 뒷부분에서 거래 작업 실행을 담당하는 함수를 만들 때 보여줍니다.
또한 전역 범위에서 두 개의 동적 배열을 추가합니다. 이러한 배열은 바 값을 사용합니다.
//--- Price data arrays double close_price[]; // Close (closing prices of the bar) double open_price[]; // Open (opening prices of the bar)
다음으로, 거래 작업이 완료된 바에서만 실행되기 때문에 프로그램이 새로운 바 이벤트를 확인하는 데 사용하는 CheckNewBar() 함수를 만듭니다.
아래는 자세한 설명이 있는 CheckNewBar() 함수의 코드입니다.
//+------------------------------------------------------------------+ //| CHECKING FOR THE NEW BAR | //+------------------------------------------------------------------+ bool CheckNewBar() { //--- Variable for storing the opening time of the current bar static datetime new_bar=NULL; //--- Array for getting the opening time of the current bar static datetime time_last_bar[1]={0}; //--- Get the opening time of the current bar // If an error occurred when getting the time, print the relevant message if(CopyTime(_Symbol,Period(),0,1,time_last_bar)==-1) { Print(__FUNCTION__,": Error copying the opening time of the bar: "+IntegerToString(GetLastError())+""); } //--- If this is a first function call if(new_bar==NULL) { // Set the time new_bar=time_last_bar[0]; Print(__FUNCTION__,": Initialization ["+_Symbol+"][TF: "+TimeframeToString(Period())+"][" +TimeToString(time_last_bar[0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]"); return(false); // Return false and exit } //--- If the time is different if(new_bar!=time_last_bar[0]) { new_bar=time_last_bar[0]; // Set the time and exit return(true); // Store the time and return true } //--- If we have reached this line, then the bar is not new, return false return(false); }
위의 코드에서 볼 수 있듯이 CheckNewBar() 함수는 바가 새 바이면 true를 반환하고 새 바가 아직 없으면 false를 반환합니다. 이렇게 하면 거래/테스트 시 상황을 제어할 수 있으며 완료된 바에 대해서만 거래 작업을 실행할 수 있습니다.
함수의 맨 처음에 static 변수와 datetime 유형의 정적 배열을 선언합니다. 정적 지역 변수는 함수가 종료된 후에도 값을 유지합니다. 모든 후속 함수 호출에서 이러한 지역 변수는 함수의 이전 호출에서 취한 값을 포함합니다.
또한 CopyTime() 함수에 유의하세요. time_last_bar 배열에서 마지막 바의 시간을 얻는 데 도움이 됩니다. MQL5 참조에서 함수 구문을 확인하세요.
이전에 이 일련의 글에서 언급된 적이 없는 사용자 정의 TimeframeToString() 함수도 확인할 수 있습니다. 시간 프레임 값을 사용자에게 명확한 문자열로 변환합니다.
string TimeframeToString(ENUM_TIMEFRAMES timeframe) { string str=""; //--- If the passed value is incorrect, take the time frame of the current chart if(timeframe==WRONG_VALUE || timeframe == NULL) timeframe = Period(); switch(timeframe) { case PERIOD_M1 : str="M1"; break; case PERIOD_M2 : str="M2"; break; case PERIOD_M3 : str="M3"; break; case PERIOD_M4 : str="M4"; break; case PERIOD_M5 : str="M5"; break; case PERIOD_M6 : str="M6"; break; case PERIOD_M10 : str="M10"; break; case PERIOD_M12 : str="M12"; break; case PERIOD_M15 : str="M15"; break; case PERIOD_M20 : str="M20"; break; case PERIOD_M30 : str="M30"; break; case PERIOD_H1 : str="H1"; break; case PERIOD_H2 : str="H2"; break; case PERIOD_H3 : str="H3"; break; case PERIOD_H4 : str="H4"; break; case PERIOD_H6 : str="H6"; break; case PERIOD_H8 : str="H8"; break; case PERIOD_H12 : str="H12"; break; case PERIOD_D1 : str="D1"; break; case PERIOD_W1 : str="W1"; break; case PERIOD_MN1 : str="MN1"; break; } //--- return(str); }
CheckNewBar() 함수가 어떻게 사용되는지는 다른 모든 필요한 함수가 준비되면 이 글의 뒷부분에서 보여질 것입니다. 이제 요청된 바 수의 값을 취하는 GetBarsData() 함수를 살펴보겠습니다.
//+------------------------------------------------------------------+ //| GETTING BAR VALUES | //+------------------------------------------------------------------+ void GetBarsData() { //--- Number of bars for getting their data in an array int amount=2; //--- Reverse the time series ... 3 2 1 0 ArraySetAsSeries(close_price,true); ArraySetAsSeries(open_price,true); //--- Get the closing price of the bar // If the number of the obtained values is less than requested, print the relevant message if(CopyClose(_Symbol,Period(),0,amount,close_price)<amount) { Print("Failed to copy the values (" +_Symbol+", "+TimeframeToString(Period())+") to the Close price array! " "Error "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } //--- Get the opening price of the bar // If the number of the obtained values is less than requested, print the relevant message if(CopyOpen(_Symbol,Period(),0,amount,open_price)<amount) { Print("Failed to copy the values (" +_Symbol+", "+TimeframeToString(Period())+") to the Open price array! " "Error "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } }
위의 코드를 자세히 살펴보자. 먼저 mount 변수에서 데이터를 가져와야 하는 바의 수를 지정합니다. 그런 다음 ArraySetAsSeries() 함수를 사용하여 마지막(현재) 바의 값이 배열의 0 인덱스에 있도록 배열 인덱싱 순서를 설정합니다. 예를 들어 계산에서 마지막 바의 값을 사용하려는 경우 시작 가격으로 예시되는 경우 다음과 같이 작성할 수 있습니다. open_price[0]. 마지막에서 두 번째 바에 대한 표기법은 유사하게 open_price[1]입니다.
종가 및 시가를 가져오는 메커니즘은 마지막 바의 시간을 가져와야 했던 CheckNewBar() 함수의 메커니즘과 유사합니다. 이 경우 CopyClose() 및 CopyOpen() 함수를 사용하는 것뿐입니다. 유사하게, CopyHigh() 및 CopyLow()는 각각 높은 바 가격과 낮은 바 가격을 얻는 데 사용됩니다.
계속해서 포지션의 개시/반전을 위한 신호를 결정하는 방법을 보여주는 매우 간단한 예를 고려해 보겠습니다. 가격 배열은 두 개의 바(현재 바와 이전에 완료된 바)에 대한 데이터를 저장합니다. 완성된 바의 데이터를 사용합니다.
- 매수 신호는 종가가 시가보다 높을 때 발생합니다(강세 바).
- 매도 신호는 종가가 시가보다 낮을 때 발생합니다(약세 바).
이러한 간단한 조건을 구현하기 위한 코드는 다음과 같습니다.
//+------------------------------------------------------------------+ //| DETERMINING TRADING SIGNALS | //+------------------------------------------------------------------+ int GetTradingSignal() { //--- A Buy signal (0) : if(close_price[1]>open_price[1]) return(0); //--- A Sell signal (1) : if(close_price[1]<open_price[1]) return(1); //--- No signal (3): return(3); }
보시다시피 매우 간단합니다. 비슷한 방식으로 더 복잡한 조건을 처리하는 방법을 쉽게 알아낼 수 있습니다. 이 함수는 완성된 바가 위로 올라가면 0을 반환하고 완성된 바가 내려가면 1을 반환합니다. 어떤 이유에서든 신호가 없으면 함수는 3을 반환합니다.
이제 거래 활동을 구현하기 위해 TradingBlock() 함수를 생성하기만 하면 됩니다. 아래는 이 코드입니다.
//+------------------------------------------------------------------+ //| TRADING BLOCK | //+------------------------------------------------------------------+ void TradingBlock() { int signal=-1; // Variable for getting a signal string comment="hello :)"; // Position comment double start_lot=0.1; // Initial volume of a position double lot=0.0; // Volume for position calculation in case of reverse position double ask=0.0; // Ask price double bid=0.0; // Bid price //--- Get a signal signal=GetTradingSignal(); //--- Find out if there is a position pos_open=PositionSelect(_Symbol); //--- If it is a Buy signal if(signal==0) { //--- Get the Ask price ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); //--- If there is no position if(!pos_open) { //--- Open a position. If the position failed to open, print the relevant message if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,start_lot,ask,0,0,comment)) { Print("Error opening a BUY position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } //--- If there is a position else { //--- Get the position type pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- If it is a SELL position if(pos_type==POSITION_TYPE_SELL) { //--- Get the position volume pos_volume=PositionGetDouble(POSITION_VOLUME); //--- Adjust the volume lot=NormalizeDouble(pos_volume+start_lot,2); //--- Open a position. If the position failed to open, print the relevant message if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,lot,ask,0,0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } } //--- return; } //--- If there is a Sell signal if(signal==1) { //-- Get the Bid price bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); //--- If there is no position if(!pos_open) { //--- Open a position. If the position failed to open, print the relevant message if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,start_lot,bid,0,0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } //--- If there is a position else { //--- Get the position type pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- If it is a BUY position if(pos_type==POSITION_TYPE_BUY) { //--- Get the position volume pos_volume=PositionGetDouble(POSITION_VOLUME); //--- Adjust the volume lot=NormalizeDouble(pos_volume+start_lot,2); //--- Open a position. If the position failed to open, print the relevant message if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,lot,bid,0,0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } } //--- return; } }
포지션이 열릴 때까지는 모든 것이 명확해야 한다고 생각합니다. 위의 코드에서 볼 수 있듯이 (trade) 포인터 다음에 점이 오고 그 다음에 PositionOpen() 메소드가 옵니다. 이것이 클래스에서 특정 메소드를 호출하는 방법입니다. 점을 넣으면 모든 클래스 메소드가 포함된 목록이 표시됩니다. 목록에서 필요한 방법을 선택하기만 하면 됩니다.
그림 1. 클래스 메소드를 호출합니다.
TradingBlock() 함수에는 매수와 판매라는 두 가지 주요 블록이 있습니다. 신호의 방향을 결정한 직후 매수 신호의 경우 매도 가격을, 매도 신호의 경우 입찰가 가격을 얻습니다.
거래 주문에 사용된 모든 가격/수준은 NormalizeDouble() 함수를 사용하여 정규화되어야 합니다. 그렇지 않으면 포지션을 열거나 수정하려고 하면 오류가 발생합니다. 랏을 계산할 때도 이 기능을 사용하는 것이 좋습니다. 또한 손절매 및 이익 실현 매개변수의 값은 0입니다. 거래 수준 설정에 대한 자세한 내용은 시리즈의 다음 글에서 제공됩니다.
이제 모든 사용자 정의 함수가 준비되었으므로 올바른 순서로 정렬할 수 있습니다.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize the new bar CheckNewBar(); //--- Get position properties and update the values on the panel GetPositionProperties(); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Print the deinitialization reason to the journal Print(GetDeinitReasonText(reason)); //--- When deleting from the chart if(reason==REASON_REMOVE) //--- Delete all objects relating to the info panel from the chart DeleteInfoPanel(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- If the bar is not new, exit if(!CheckNewBar()) return; //--- If there is a new bar else { GetBarsData(); // Get bar data TradingBlock(); // Check the conditions and trade } //--- Get the properties and update the values on the panel GetPositionProperties(); }
고려해야 할 것은 OnTrade() 기능을 사용하여 거래 이벤트를 결정하는 것입니다. 여기에서는 일반적인 아이디어를 제공하기 위해 간략하게만 다루겠습니다. 우리의 경우 다음 시나리오를 구현해야 합니다. 포지션을 수동으로 열거나 닫거나 수정할 때 정보 패널의 포지션 속성 목록에 있는 값은 새로운 틱을 수신하는 것이 아니라 작업이 완료되는 즉시 업데이트해야 합니다. 이를 위해 다음 코드만 추가하면 됩니다.
//+------------------------------------------------------------------+ //| TRADE EVENT | //+------------------------------------------------------------------+ void OnTrade() { //--- Get position properties and update the values on the panel GetPositionProperties(); }
기본적으로 모든 것이 준비되었으며 테스트를 진행할 수 있습니다. Strategy Tester를 사용하면 시각화 모드에서 테스트를 빠르게 실행하고 오류가 있는 경우 오류를 찾을 수 있습니다. 전략 테스터를 사용하면 시장이 닫히는 주말에도 프로그램을 계속 개발할 수 있기 때문에 유익한 것으로 볼 수 있습니다.
전략 테스터를 설정하고 시각화 모드를 활성화한 다음 시작을 클릭하세요. Expert Advisor가 전략 테스터에서 거래를 시작하고 아래와 유사한 그림을 볼 수 있습니다.
그림 2. MetaTrader 5 전략 테스터의 시각화 모드.
언제든지 시각화 모드에서 테스트를 일시 중지하고 F12 키를 눌러 단계별로 테스트를 계속할 수 있습니다. 단계는 전략 테스터를 시가만 모드로 설정한 경우 바 1개, 모든 틱 모드를 선택한 경우 1틱과 같습니다. 테스트 속도를 제어할 수도 있습니다.
수동으로 포지션을 개설/청산하거나 손절매/이익 실현 레벨을 추가/수정한 직후 정보 패널의 값이 업데이트되도록 하려면 Expert Advisor가 다음과 같아야 합니다. 실시간 모드에서 테스트했습니다. 너무 오래 기다리지 않으려면 1분 단위로 Expert Advisor를 실행하여 1분마다 거래 작업이 실행되도록 하세요.
그 외에도 정보 패널의 포지션 속성 이름에 대한 다른 배열을 추가했습니다.
// Array of position property names string pos_prop_texts[INFOPANEL_SIZE]= { "Symbol :", "Magic Number :", "Comment :", "Swap :", "Commission :", "Open Price :", "Current Price :", "Profit :", "Volume :", "Stop Loss :", "Take Profit :", "Time :", "Identifier :", "Type :" };
이전 글에서 SetInfoPanel() 함수 코드를 줄이기 위해 이 배열이 필요하다고 언급했습니다. 아직 구현하지 않았거나 스스로 알아낸 경우 이 작업을 수행하는 방법을 알 수 있습니다. 포지션 속성과 관련된 개체 생성 목록의 새로운 구현은 다음과 같습니다.
//--- List of the names of position properties and their values for(int i=0; i<INFOPANEL_SIZE; i++) { //--- Property name CreateLabel(0,0,pos_prop_names[i],pos_prop_texts[i],anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[i],2); //--- Property value CreateLabel(0,0,pos_prop_values[i],GetPropertyValue(i),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[i],2); }
SetInfoPanel() 함수의 시작 부분에서 다음 행을 볼 수 있습니다.
//--- Testing in the visualization mode if(MQL5InfoInteger(MQL5_VISUAL_MODE)) { y_bg=2; y_property=16; }
프로그램이 현재 시각화 모드에서 테스트 중인 경우 정보 패널에 있는 개체의 Y 좌표를 조정해야 함을 프로그램에 전달합니다. 이는 Strategy Tester의 시각화 모드에서 테스트할 때 실시간처럼 차트 우측 상단에 Expert Advisor의 이름이 표시되지 않기 때문입니다. 따라서 불필요한 들여쓰기를 삭제할 수 있습니다.
결론
이제 끝났습니다. 다음 글에서는 거래 수준을 설정하고 수정하는 데 중점을 둘 것입니다. 아래에서 Expert Advisor, PositionPropertiesTesterEN.mq5의 소스 코드를 다운로드할 수 있습니다.
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/642