English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
MQL5 Cookbook: MetaTrader 5 전략 테스터의 포지션 속성 분석

MQL5 Cookbook: MetaTrader 5 전략 테스터의 포지션 속성 분석

MetaTrader 5 | 2 9월 2021, 16:57
61 0
Anatoli Kazharski
Anatoli Kazharski

소개

이 글에서는 이전 글 "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. 클래스 메소드를 호출합니다.

그림 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 전략 테스터의 시각화 모드.

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

MQL5 Cookbook: 거래 수준을 설정/수정할 때 오류를 피하는 방법 MQL5 Cookbook: 거래 수준을 설정/수정할 때 오류를 피하는 방법
"MQL5 Cookbook: MetaTrader 5 Strategy Tester의 포지션 속성 분석" 시리즈의 이전 글에서 Expert Advisor에 대한 작업을 계속하면서 많은 유용한 기능으로 기존의 기능들과 더불어 이를 개선하고 최적화할 것입니다. Expert Advisor는 이번에 MetaTrader 5 전략 테스터에서 최적화할 수 있는 외부 매개변수를 가지며 어떤 면에서는 단순한 거래 시스템과 유사합니다.
MQL5 Cookbook: 사용자 지정 정보 패널의 포지션 속성 MQL5 Cookbook: 사용자 지정 정보 패널의 포지션 속성
이번에는 현재 기호에 대한 포지션 속성을 가져와 수동 거래 중에 사용자 지정 정보 패널에 표시하는 간단한 Expert Advisor를 만들 것입니다. 정보 패널은 그래픽 개체를 사용하여 생성되며 표시된 정보는 틱마다 새로 고쳐집니다. 이것은 "MQL5 Cookbook: Get Position Properties" 시리즈의 이전 글에서 설명한 스크립트를 수동으로 실행해야 하는 모든 시간보다 훨씬 더 편리할 것입니다.
MQL5 Cookbook: 거래의 역사 및 직위 속성 가져오기를 위한 기능 라이브러리 MQL5 Cookbook: 거래의 역사 및 직위 속성 가져오기를 위한 기능 라이브러리
포지션 속성에 대한 이전 글에서 제공한 정보를 간략하게 요약할 시간입니다. 이 글에서는 거래 내역에 액세스한 후에만 얻을 수 있는 속성을 가져오는 몇 가지 추가 함수를 만듭니다. 또한 보다 편리한 방법으로 포지션 및 기호 속성에 액세스할 수 있는 데이터 구조에 익숙해질 것입니다.
MQL5 Cookbook: 포지션 속성 가져오기 MQL5 Cookbook: 포지션 속성 가져오기
이 글에서는 모든 포지션 속성을 가져와 대화 상자에서 사용자에게 표시하는 스크립트를 만들 것입니다. 스크립트를 실행하면 외부 매개변수의 드롭다운 목록에서 사용 가능한 두 가지 모드 중에서 선택할 수 있습니다. 현재 심볼에서만 포지션 속성을 보거나 모든 심볼에서 포지션 속성을 보는 것입니다.