English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
보조 인디케이터 메모리 소모량 감소시키기

보조 인디케이터 메모리 소모량 감소시키기

MetaTrader 5지표 | 11 10월 2021, 15:06
84 0
ds2
ds2

1. 문제점

아마 기존에 보조 인디케이터를 활용하는 Expert Advisor나 인디케이터를 생성하거나 사용해보신 적 있을 것입니다.

예를 들어, 유명한 인디케이터인 MACD는 EMA (Exponential Moving Average, 지수이동평균) 인디케이터를 계산 과정 중에 두 개 사용합니다.


그런 복잡한 인디케이터도 사실 쪼개고 쪼개면 간단한 인디케이터 여러개로 구성되어있습니다. 예를 들어 앞에 언급한 MACD는 단일 EMA보다 3배 많은 메모리와 처리량을 요구하는데 이는 메인 인디케이터 버퍼와 보조 인디케이터 버퍼용 메모리를 할당해야하기 때문입니다.

MACD 외에도 2개 이상 보조 인디케이터를 사용하는 복잡한 인디케이터들이 많습니다..

게다가 이하의 경우에 메모리 소모량이 크게 늘어나는데,

  • 만약 인디케이터가 다중 타임프레임을 사용하는 경우 (예를들어 여러 타임프레임의 병행처리 추적시) 각 타임프레임에 해당되는 별개의 보조 인디케이터 인스턴스를 생성해야합니다.
  • 이 인디케이터는 다중처리입니다.
  • 만약 트레이더가 인디케이터를 여러 화폐쌍을 대상으로 매매하는데에 사용하는 경우 (제 경험상 20개 이상의 화폐쌍을 대상으로 거래하는 사람들을 알고있습니다).

이러한 조건들이 맞물려떨어지는 경우 컴퓨터의 메모리가 고갈될 여지가 있습니다 (실제 상황에서 그러한 인디케이터들을 가동시키는데에 수 기가바이트의 메모리가 사용된 적이 있습니다). MetaTrader 5 클라이언트 터미널에서 메모리가 고갈되는 예시입니다.


이러한 상황에서는 터미널이 인디케이터를 올바르게 차트 위에 배치하거나 제대로 계산하는 것이 어려워집니다 (만약 인디케이터 코드가 메모리 할당을 제대로 처리하지 않는다면). 심한 경우엔 그냥 꺼질 수도 있습니다.

다행이게도 터미널은 하드 디스크에 데이터 일부를 저장하는 등, 가상 메모리를 활용하여 메모리 부족을 보완할 수 있습니다. 프로그램이 실행은 될 테지만, 몹시 느려집니다...


2. 시험용 복합 인디케이터

본 문서에서 다루고자하는 바에서 보다 깊게 들어가기 위하여 MACD보다 더욱 복잡한 복합 인디케이터를 생성할 것입니다.

이를 트렌드 시작을 추적하는 인디케이터로 삼습니다. 이 인디케이터는 5개의 타임프레임, 예를 들어: H4, H1, M15, M5, M1에서 발생하는 시그널을 합칠 것입니다. 이 과정에서 예상의 신뢰성을 높일 수 있도록 작은 상승 트렌드들 간의 공명을 수정할 것입니다. 각 타임프레임의 시그널 소스로서 우리는 MetaTrader 5에 내장된 이치모쿠(Ichimoku)프라이스 채널(Price_Channel) 인디케이터를 사용할 것입니다.

  • 만약 이치모쿠 인디케이터의 텐칸(Tenkan, 빨간색) 선이 키준(Kijun, 파란색) 선 위에 있다면 트렌드가 상승중이며 반대 경우엔 하강중입니다.


  • 만약 매매가가 프라이스 채널의 중간 선 위에 있다면 트렌드가 상승ㅈ중이며 반대 경우엔 하강중ㅇ비니다.


5개 타임프레임 각 2개씩 하여, 도합 10개의 보조 인디케이터가 사용됩니다. 우리가 만들 인디케이터를 트렌더(Trender)라고 명명하겠습니다.

이것이 전체 소스 코드 입니다 (문서에도 첨부되어 있습니다).

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
#property indicator_minimum -1
#property indicator_maximum  1

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  DarkTurquoise

// The only buffer of the indicator
double ExtBuffer[];

// Timeframes of auxiliary indicators
ENUM_TIMEFRAMES TF[5] = {PERIOD_H4, PERIOD_H1, PERIOD_M15, PERIOD_M5, PERIOD_M1};

// Handles of auxiliary indicators for all timeframes
int h_Ichimoku[5], h_Channel[5];

//+------------------------------------------------------------------+
void OnInit()
  {
   SetIndexBuffer(0, ExtBuffer);
   ArraySetAsSeries(ExtBuffer, true);
   
   // Create auxiliary indicators
   for (int itf=0; itf<5; itf++)
     {
      h_Ichimoku[itf] = iCustom(Symbol(), TF[itf], 
                                "TestSlaveIndicators\\Ichimoku",
                                9, 26, 52
                               );
      h_Channel [itf] = iCustom(Symbol(), TF[itf],
                                "TestSlaveIndicators\\Price_Channel",
                                22
                               );
     }
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int 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 int& spread[])
  {
   ArraySetAsSeries(time, true);
  
   int limit = prev_calculated ? rates_total - prev_calculated : rates_total -1;

   for (int bar = limit; bar >= 0; bar--)
     {
      // Time of the current bar
      datetime Time  = time [bar];
      
      //--- Gather signals from all timeframes
      double Signal = 0; // total signal
      double bufPrice[1], bufTenkan[1], bufKijun [1], bufMid[1], bufSignal[1];
      for (int itf=0; itf<5; itf++)
        {
         //=== Bar price
         CopyClose(Symbol(), TF[itf], Time, 1, bufPrice);
         double Price = bufPrice[0];

         //=== The Ichimoku indicator         
         CopyBuffer(h_Ichimoku[itf], 0, Time, 1, bufTenkan);
         double Tenkan = bufTenkan[0];
         CopyBuffer(h_Ichimoku[itf], 1, Time, 1, bufKijun );    
         double Kijun  = bufKijun [0];
           
         if (Tenkan > Kijun) Signal++;
         if (Tenkan < Kijun) Signal--;
          
         //=== The channel indicator
         CopyBuffer(h_Channel [itf], 2, Time, 1, bufMid);
         double Mid = bufMid[0];

         if (Price > Mid) Signal++;
         if (Price < Mid) Signal--;
        }
        
      ExtBuffer[bar] = Signal/10;
     }

   return(rates_total);
  }
//+------------------------------------------------------------------+

이 인디케이터는 시그널을 추출하는 타임프레임들 안에서도 가장 작은 타임프레임의 차트에 사용해야합니다. 이는 그것만이 작은 트렌드를 전부 볼 수 있는 방법이기 때문입니다. 우리가 선택한 타임프레임 안에서는 M1입니다. 인디케이터는 이런식으로 보일 것입니다.


이제부터는 각 인디케이터가 사용하는 메모리량을 계산하는 제일 중요한 파트로 들어갑니다.

이치모쿠 인디케이터(전체 코드는 첨부 쪽에서 확인하십시오)와

#property indicator_buffers 5

프라이스_채널 인디케이터 (전체 코드는 마찬가지로 첨부 쪽에서 확인하십시오)를 한 번 보세요.

#property indicator_buffers 3

8개의 버퍼가 생성된 것을 볼 수 있으실 것입니다. 이를 타임 프레임의 수인 5로 곱하십시오. 그리고 트렌더 인디케이터 자체의 버퍼 1개를 추가. 다 합쳐서 41 버퍼가 생성되는 것입니다! 꽤나 간단해보이는 (차트상으론) 인디케이터 치고는 상당히 많은 수가 사용되고 있음을 볼 수 있습니다.

클라이언트 터미널의 기본 설정 기준으로 버퍼 하나는 하나당 8 바이트를 차지하는 double타입 약 100000 개로 되어있습니다. 따라서 41개 버퍼 총합 31Mb의 메모리를 차지하게 됩니다. 게다가 이것은 값만 따졌을 때 이 정도라는 것으로, 버퍼들 안에 다른 서비스 정보가 있을 수도 있다는 점을 고려하지 않은 것입니다.

"31 Mb 정도면 별거 아니네"라고 하실지도 모르겠습니다. 하지만 트레이더가 다양한 화폐쌍을 사용하기 시작하는 시점부터 양이 문제가 되기 시작합니다. 인디케이터 뿐만 아니라 차트 자체가 많은 메모리량을 차지하기 때문입니다. 인디케이터와 달리 각 막대는 OHLC, 시간, 및 볼륨 같은 다양한 값을 지니고 있습니다. 컴퓨터 하나에서 어떻게 해야 처리할 수 있을까요?


3. 문제의 해결법

당연히 컴퓨터의 메모리를 늘리는 방법이 있기는 합니다. 하지만 이 방법은 기술적으로건, 경제적인 이유에서건, 어떠한 이유로 인해 선택하기 어려운 방법일 수가 있습니다. 혹은 이미 설치할 수 있는 양의 메모리를 전부 설치하여 더 이상 박을 수 없는 경우에 그러한 상황에 놓일 수도 있습니다. 따라서 메모리를 많이 잠식하는 인디케이터를 찾아내어 문제를 해결해야만 합니다.

그를 위해선... 학교에서 배운 기하학을 떠올려 보십시오. 우리가 만든 복합 인디케이터의 버퍼들이 직사각형이라고 생각해보십시오.


이 직사각형의 너비가 이미 할당된 메모리입니다. 이 너비를 줄이려면 길이나 높이를 줄이면 됩니다.

이 경우, 길이는 인디케이터가 그려진 막대의 숫자이며 높이는 인디케이터 버퍼의 숫자입니다.


4. 막대의 수 줄이기

4.1. 간단한 해법

MetaTrader 설정을 변경하는데에는 딱히 프로그래머적 지식이 필요하지 않습니다.



"차트 내의 막대 최대한도(Max bards in chart)"의 값을 낮추는 것으로 이들 창에 있는 인디케이터 버퍼의 사이즈를 줄일 수 있습니다. 이는 간단하고 효과적이면서 누구건 할 수 있는 방법입니다. (만약 트레이더가 매매시에 긴 매매 이력을 필요로 하지 않는 경우).

4.2. 다른 솔루션은 없나요?

MQL5 프로그래머라면 인디케이터 버퍼는 사전 정의된 사이즈가 없는 동적 어레이로 선언된다는 것을 알고있습니다. 예를들어 이치모쿠의 5 버퍼들을 보죠.

double    ExtTenkanBuffer[];
double    ExtKijunBuffer[];
double    ExtSpanABuffer[];
double    ExtSpanBBuffer[];
double    ExtChinkouBuffer[];

MetaTrader 5 클라이언트 터미널 자체에서 열람가능한 과거 이력을 기준으로 정해지기 때문에 어레이 사이즈는 정해져 있지 않습니다.

OnCalculate 함수 안에서도 똑같습니다.

int OnCalculate (const int rates_total,      // size of the array price[]
               const int prev_calculated,  // number of bars processed at the previous call
               const int begin,            // the start of reliable data
               const double& price[]       // array for the calculation
   );

이 함수 안에서 프라이스 버퍼는 인디케이터로 넘겨집니다. 메모리는 미리 터미널에서 할당되었기 때문에 프로그래머가 딱히 크기를 조정할 수가 없습니다.

또한, MQL5에서는 인디케이터의 버퍼를 다른 인디케이터의 프라이스 버퍼로 사용할 수 있게 해줍니다 ( "다른 인디케이터 기반으로 인디케이터 그리기"). 그러나 여기에서도 개발자가 사이즈 제한을 걸 수가 없습니다. 그냥 인디케이터 핸들을 넘길 수 있을 뿐입니다.

고로 MQL5에서는 인디케이터 버퍼의 사이즈를 제한할 방법이 존재하지않습니다.


5. 버퍼 수 줄이기

이 시점에서 프로그래머에게는 다양한 선택지가 존재합니다. 복합 인디케이터의 버퍼 수를 줄이는 몇 가지 간단하고 원론적인 방법을 생각해냈습니다. 하지만 이 모든 방법의 공통점은 보조 인디케이터 버퍼 수를 줄이는 것이었습니다. 메인 인디케이터 안에서 모든 버퍼가 필요했기 때문인데요.

이 방식의 장점과 단점, 그리고 실제로 원하는대로 바뀔지 들여다봅시다.

5.1. "Need" 방법

보조 버퍼에 많은 버퍼가 있다면 이 버퍼 전부가 메인 인디케이터에서 필요하지는 않을 수도 있습니다. 따라서 우리는 사용되지 않는 인디케이터를 비활성화시켜 이 인디케이터들이 점유하고 있는 메모리를 비울 것입니다. 그럴 위해서는 보조 메모리의 소스 코드를 수정하여야합니다.

Price_Channel 보조 인디케이터로 해봅시다. 이 인디케이터에는 3개의 버퍼가 있는데 트렌더는 그 중 하나만을 읽습니다. 그러므로 나머지 둘은 불필요하다고 할 수 있습니다.

Price_Channel (초기 인디케이터) 및 Price_Channel-Need (리메이크 버전) 인디케이터의 코드는 이 문서의 첨부파일에서 확인하실 수 있습니다. 먼저, 수정한 내용에 대해서 설명할 것입니다.

먼저 decrease 카운터를 3에서 1로 낮춥니다.

//#property indicator_buffers 3
  #property indicator_buffers 1
//#property indicator_plots   2
  #property indicator_plots   1

그 후 불필요한 버퍼 어레이 두 개를 삭제합니다.

//--- indicator buffers
//double    ExtHighBuffer[];
//double    ExtLowBuffer[];
 double    ExtMiddBuffer[];

이제 이 인디케이터를 컴파일하려면 이 어레이들이 호출되는 열을 보여줄 것입니다.


이 방법을 통해 우리가 바꿔야할 것이 어디있는지 빠르게 찾아낼 수 있게됩니다. 인디케이터 코드가 몹시 방대할수록 편리한 방법입니다.

우리의 경우엔 4개의 "undeclared identifier" 줄이 있습니다. 수정하도록 할게요.

예상했듯이 그 중 2개는 OnInit에 있습니다. 그러나 그것과 더불어서 ExtMiddBuffer가 있는 줄을 제거해야합니다 그러느니 버퍼의 다른 인덱스를 활용해 그냥 비슷한걸 만들어 추가할 것입니다. 인덱스 2 버퍼가 없으니 이제 0만 쓸 수 있습니다.

//   SetIndexBuffer(0,ExtHighBuffer,INDICATOR_DATA);
//   SetIndexBuffer(1,ExtLowBuffer,INDICATOR_DATA);
//   SetIndexBuffer(2,ExtMiddBuffer,INDICATOR_DATA);
     SetIndexBuffer(0,ExtMiddBuffer,INDICATOR_DATA);

만약 시각 모드에서 "cut" 인디케이터를 사용할 예정이시라면 버퍼 인덱스 수정에 맞춰 디자인 세팅도 수정하셔야합니다 우리의 경우

//#property indicator_type1   DRAW_FILLING
  #property indicator_type1   DRAW_LINE

만약 시각화가 필요하시지 않다면 디자인 변경은 무시하셔도 됩니다 에러가 발생하진 않습니다.

"undeclared identifier" 리스트를 마저 다뤄봅시다 마지막으로 바꿔야할 2개는 인디케이터 버퍼를 채우는 OnCalculate에 있습니다.. ExtMiddBuffer가 이미 삭제한 ExtHighBuffer와 ExtLowBuffer를 호출하기때문에 중간 변수들이 대체되었습니다.

   //--- the main loop of calculations
   for(i=limit;i<rates_total;i++)
     {
//      ExtHighBuffer[i]=Highest(High,InpChannelPeriod,i);
        double      high=Highest(High,InpChannelPeriod,i);
//      ExtLowBuffer[i]=Lowest(Low,InpChannelPeriod,i);
        double      low=Lowest(Low,InpChannelPeriod,i);
//      ExtMiddBuffer[i]=(ExtHighBuffer[i]+ExtLowBuffer[i])/2.0;;
        ExtMiddBuffer[i]=(   high         +   low         )/2.0;;
     }

보시다시피 이 "수술"에 딱히 어려운 것은 없습니다. 뭘 바꿔야하는지 금세 찾을 수 있었고 몇가지 "오점"과 두 버퍼가 제거되었습니다. 트렌더 복합 인디케이터 전체에서 봤을때 이로서 10 버퍼 (2*5 타임프레임)분이 절약됩니다.

Price_Channel 및 Price_Channel-Need 를 각각 열어서 사라진 버퍼를 비교해볼 수 있습니다.


Price_Channel-Need 를 트렌더 인디케이터에서 사용하려면 보조 인디케이터명 "Price_Channel"을 "Price_Channel-Need"로 트렌더 코드 안에서 바꿔야합니다. 또한 사용되는 버퍼 인덱스를 2에서 0으로 바꿔야합니다. 제가 미리 준비해둔 Trender-Need를 본문에 첨부하여 두었습니다.


5.2. "Aggregate" 방법

메인 인디케이터가 보조 인디케이터의 두 개 이상의 버퍼 데이터를 읽은 다음 이를 사용하여 집계 작업을 수행하는 경우(예: 더하기 또는 비교하기) 주 인디케이터에서 이 작업을 수행할 필요가 없습니다. 보조 인디케이터로 만든 다음 메인 인디케이터에 결과를 전달하면 됩니다. 따라서 버퍼가 여러 개 있을 필요는 없습니다. 버퍼가 모두 하나로 바뀝니다.

우리의 경우엔 그 방식을 이치모쿠에 적용시킬 수 있습니다. 트렌더는 2개의 버퍼(0 - Tenkan 및 1 - Kijun)를 사용합니다.

         CopyBuffer(h_Ichimoku[itf], 0, Time, 1, bufTenkan);
         double Tenkan = bufTenkan[0];
         CopyBuffer(h_Ichimoku[itf], 1, Time, 1, bufKijun );    
         double Kijun  = bufKijun [0];
           
         if (Tenkan > Kijun) Signal++;
         if (Tenkan < Kijun) Signal--;

이치모쿠의 0과 1 버퍼를 하나의 시그널 버퍼로 집계하면 위에 표시된 트렌더 일부분이 다음과 같은 버퍼로 대체됩니다.

         CopyBuffer(h_Ichimoku[itf], 0, Time, 1, bufSignal);
         
         Signal += bufSignal[0];

트렌더-Aggregate의 전체 소스는 본 문서에 첨부되어 있습니다.

이제 이치모쿠에서 바꿔야할 중요 파트를 보도록 합시다.

이 인디케이터에는 미사용 버퍼가 존재합니다. 따라서 "Aggregate" 방법에 더하여 "Need" 방법 또한 적용할 수 있습니다. 그를 통해 이치모쿠에는 5개 중 1개의 버퍼만이 남습니다 - 필요한 모든 것을 합치는 버퍼만요.:

//#property indicator_buffers 5
  #property indicator_buffers 1
//#property indicator_plots   4
  #property indicator_plots   1

이 버퍼에 이름을 붙이도록 합시다.

//--- indicator buffers
//double    ExtTenkanBuffer[];
//double    ExtKijunBuffer[];
//double    ExtSpanABuffer[];
//double    ExtSpanBBuffer[];
//double    ExtChinkouBuffer[];
  double    ExtSignalBuffer[];

새 이름은 실용적인 의미를 담습니다. 인디케이터에서 과거에 사용된 버퍼들을 제거하는 것이 가능합니다. 이를 통해 ("Need" 방법에서 다룬 방식으로) 변경해야할 줄들을 빠르게 찾아낼 수 있습니다.

차트에서 인디케이터를 시각화하려면 모양 설정을 변경해야 합니다. 또한 우리의 경우 집계 버퍼가 사용하는 두 버퍼와 다른 값의 범위를 가지고 있다는 점을 고려해야 합니다. 아직은 가격 변동을 보여주진않지만 두 개의 버퍼 중 어느 것이 더 큰지 알 수 있습니다. 차트 아래의 별도 창에 이러한 결과를 표시하는 것이 더 편리합니다.

//#property indicator_chart_window
  #property indicator_separate_window

이제 OnInit을 바꿔봅시다.

//--- indicator buffers mapping
//   SetIndexBuffer(0,ExtTenkanBuffer,INDICATOR_DATA);
//   SetIndexBuffer(1,ExtKijunBuffer,INDICATOR_DATA);
//   SetIndexBuffer(2,ExtSpanABuffer,INDICATOR_DATA);
//   SetIndexBuffer(3,ExtSpanBBuffer,INDICATOR_DATA);
//   SetIndexBuffer(4,ExtChinkouBuffer,INDICATOR_DATA);
     SetIndexBuffer(0,ExtSignalBuffer,INDICATOR_DATA);

OnCalculate에서도 재밌는 부분을 변경시킬 것입니다. 참고: 불필요한 버퍼 3개는 간단히 삭제되고("Need" 방법을 사용하여), 필요한 ExtTenkanBuffer 및 ExtKijunBuffer는 임시 변수 Tenkan 및 Kijun으로 대체됩니다. 이러한 변수는 주기가 끝날 때 집계 버퍼 ExtSignalBuffer를 계산하는 데 사용됩니다.

   for(int i=limit;i<rates_total;i++)
     {
//     ExtChinkouBuffer[i]=Close[i];
      //--- tenkan sen
      double high=Highest(High,InpTenkan,i);
      double low=Lowest(Low,InpTenkan,i);
//     ExtTenkanBuffer[i]=(high+low)/2.0;
       double  Tenkan    =(high+low)/2.0;
      //--- kijun sen
      high=Highest(High,InpKijun,i);
      low=Lowest(Low,InpKijun,i);
//     ExtKijunBuffer[i]=(high+low)/2.0;
       double  Kijun    =(high+low)/2.0;
      //--- senkou span a
//     ExtSpanABuffer[i]=(ExtTenkanBuffer[i]+ExtKijunBuffer[i])/2.0;
      //--- senkou span b
      high=Highest(High,InpSenkou,i);
      low=Lowest(Low,InpSenkou,i);
//     ExtSpanBBuffer[i]=(high+low)/2.0;

       //--- SIGNAL
       double Signal = 0;
       if (Tenkan > Kijun) Signal++;
       if (Tenkan < Kijun) Signal--;
       ExtSignalBuffer[i] = Signal;
     }

총 4개가 줄었습니다. 만약 우리가 "Need" 방법 만을 적용했다면 3개만 줄일 수 있었을 것입니다..

트렌더 내에서 총 20 (4* 5 타임프레임) 버퍼를 줄일 수 있었습니다.

이치모쿠-Aggregate 전체 코드는 본 문서에 첨부되어 있습니다. 본 인디케이터를 원래 것에 비교하려면, 둘 다 같은 차트 상에서 여십시오. 기억하시겠다시피 수정된 인디케이터는 차트 아래에 별도 창으로서 표시됩니다.


5.3. "Include" 방법

버퍼 수를 줄이는 가장 화끈한 방법은 모든 보조 인디케이터를 제거해버리는 것입니다.. 그렇게 되면 주 인디케이터에 속하는 버퍼 하나 외에는 전부 사라집니다. 이 이상 줄이는 방법은 존재치 않습니다.

보조 인디케이터의 코드를 주 인디케이터에 합치는 것으로 동일한 결과를 얻을 수 있습니다. 때로는 이게 시간을 너무 잡아먹는 것 처럼 보일지 모르지만, 그 결과는 충분히 그럴 가치가 있습니다. 제일 어려운 부분은 인디케이터에서 가져온 코드를 적용시키는 부분입니다. 원래 코드는 다른 인디케이터 코드 안에서 작동하는 것이 상정되어있지 않기 때문인데요.

이하가 그와 관련해서 발생할 수 있는 문제들입니다.

  • 이름 중복 변수, 함수 (특히 OnCalculate같은 시스템 함수) 의 이름이 겹치는 상황
  • 버퍼 부재 일부 인디케이터들에서는 버퍼 안에 데이터를 저장하고, 그 안에서 가공하는 것이 알고리즘에서 몹시 중요하게 다루어지고있습니다. 그 경우엔 문제가 될 수 있습니다. 이 경우, 버퍼를 단순 어레이로 교체하는 것은 만병통치약이 아닙니다. 메모리 사용을 줄이는 것이 목표이기 때문입니다. 엄청나게 큰 과거 데이터를 메모리에 저장하지 않는 것이 중요합니다.

이러한 문제를 효과적으로 해결할 수 있는 방법을 시연해 보겠습니다.

모든 보조 인디케이터는 클래스로 작성되어야합니다. 그렇게 하면 인디케이터의 모든 변수와 함수는 항상 (클래스 내에서) 고유한 이름을 가지며 다른 인디케이터와 충돌하지 않습니다.

여러 인디케이터를 이동시키는 경우 클래스를 사용할 때 혼동을 피하기 위해 해당 클래스의 표준화를 고려할 수 있습니다. 이를 위해 기본 인디케이터 클래스를 만들고 이 클래스에서 모든 보조 인디케이터의 클래스를 상속합니다.

아래의 클래스를 작성했습니다.

class CIndicator
  {
protected:
   string symbol;             // currency pair
   ENUM_TIMEFRAMES timeframe;  // timeframe

   double Open[], High[], Low[], Close[]; // simulation of price buffers
   int BufLen; // necessary depth of filling of price buffers

public:
   //--- Analogs of standard functions of indicators
   void Create(string sym, ENUM_TIMEFRAMES tf) {symbol = sym; timeframe = tf;};
   void Init();
   void Calculate(datetime start_time); // start_time - address of bar that should be calculated
  };

이걸 기반으로 이치모쿠 인디케이터를 작성해봅시다. 먼저, 속성 형식으로 입력 패러미터를 원래 이름으로 작성합니다. 나중에 인디케이터 코드를 변경하지 않으려면,

class CIchimoku: public CIndicator
  {
private:
   // Simulation of input parameters of the indicator
   int InpTenkan;
   int InpKijun;
   int InpSenkou;

모든 버퍼의 이름을 보관해두세요. 네, 제대로 들으신 것 맞습니다. 이 인디케이터의 버퍼 5개를 모두 선언합니다. 하지만 이것들은 더미죠. 이 버퍼들은 하나의 막대로만 구성됩니다.

public:   
   // Simulation of indicator buffers
   double ExtTenkanBuffer [1];
   double ExtKijunBuffer  [1];
   double ExtSpanABuffer  [1];
   double ExtSpanBBuffer  [1];
   double ExtChinkouBuffer[1];   

왜 그래야하는가? 나중에 코드 내에서 너무 크게 수정해야하는 일을 줄이기 위해서죠. 나중에 알게 되실 겁니다. 상속된 CIchimoku 메소드를 다시 정의합니다. 이치모쿠에서 가져온 OnCalculate 함수의 코드를 채워 계산하십시오.

이 메소드를 이동시킬 때 히스토리 막대의 루프가 제거되었는지 주의하십시오. 이제 지정된 시간의 단 하나의 막대만이 여기서 계산됩니다. 계산 주요 코드는 그대로고요. 그래서 모든 버퍼 이름과 인디케이터 패러미터를 세심하게 보관한 것입니다.

또한 가격 버퍼는 Calculate 메소드 맨 처음에 값으로 채워져 있다는 점에 유의해야 합니다. 하나의 막대를 계산하는 데 필요한 만큼의 값이 있습니다.

   void Calculate(datetime start_time)
     {
      CopyHigh (symbol,timeframe,start_time,BufLen,High);
      CopyLow  (symbol,timeframe,start_time,BufLen,Low );
      CopyClose(symbol,timeframe,start_time,1     ,Close);

//    int limit;
      //---
//    if(prev_calculated==0) limit=0;
//    else                   limit=prev_calculated-1;
      //---
//    for(int i=limit;i<rates_total;i++)
      int i=0;
        {
         ExtChinkouBuffer[i]=Close[i];
         //--- tenkan sen
         double high=Highest(High,InpTenkan,i);
         double low=Lowest(Low,InpTenkan,i);
         ExtTenkanBuffer[i]=(high+low)/2.0;
         //--- kijun sen
         high=Highest(High,InpKijun,i);
         low=Lowest(Low,InpKijun,i);
         ExtKijunBuffer[i]=(high+low)/2.0;
         //--- senkou span a
         ExtSpanABuffer[i]=(ExtTenkanBuffer[i]+ExtKijunBuffer[i])/2.0;
         //--- senkou span b
         high=Highest(High,InpSenkou,i);
         low=Lowest(Low,InpSenkou,i);
         ExtSpanBBuffer[i]=(high+low)/2.0;
        }
      //--- done
//    return(rates_total);     
     };

물론 우리는 원래 코드 대로 가도 됩니다. 하지만 이렇게 하게 되면 먼저 코드의 작동 논리를 이해해야 하고, 그 뒤에 많은 부분을 다시 써야 합니다. 우리가 선택한 방법대로 가면 인디케이터는 간략하게 남고 이해하기도 쉬울겁니다. 만약 인디케이터가 복잡하다면? 그 경우 어찌해야하는지 보여드린 바 있습니다.

먼저 CIchimoku.Init 메소드를 메웁시다; 전부 간단합니다.

   void Init(int Tenkan = 9, int Kijun = 26, int Senkou = 52)
     {
      InpTenkan = Tenkan; InpKijun = Kijun; InpSenkou = Senkou;
      BufLen = MathMax(MathMax(InpTenkan, InpKijun), InpSenkou);
     };

이치모쿠에는 CIchimoku 클래스로 가져가야 하는 두 가지 함수가 있습니다. Highest 그리고 Lowest. 이 함수들은 지정된 프라이스 버퍼에서 각각 최대 최소 값을 찾는 함수입니다.

이 프라이스 버퍼는 실제 버퍼가 아닙니다. 크기가 매우 작습니다(위의 Calculate 메소드에서 해당 버퍼가 채워지는 것을 보셨을겁니다). 이 때문에 Highest와 Lowest 함수의 알고리즘을 조금 수정해야합니다.

이러한 상황에서는 저는 변경하는 내용은 최소화하기로 결정했습니다. 모든 수정사항은 버퍼의 막대 인덱싱을 전역 행(버퍼 길이가 사용 가능한 전체 기록인 경우)에서 로컬 행(현재 가격 버퍼에는 표시기 막대 계산에 필요한 값만 포함)으로 추가하는 작업으로 구성됩니다.

   double Highest(const double&array[],int range,int fromIndex)
     {
       fromIndex=MathMax(ArraySize(array)-1, 0);
      double res=0;
   //---
      res=array[fromIndex];
      for(int i=fromIndex;i>fromIndex-range && i>=0;i--)
        {
         if(res<array[i]) res=array[i];
        }
   //---
      return(res);
     }

Lowest 메소드 역시 똑같이 처리됩니다.

Price_Channel 인디케이터 역시 비슷하게 처리되지만 CChannel 클래스로 표현됩니다. 저 두 클래스의 전체 코드는 이 파일에 첨부된 Trender-Include 파일에서 찾아보실 수 있습니다.

코드 이전의 대부분은 이제 설명이 끝난 것 같습니다. 대부분의 인디케이터에서 이 메소드들로 충분할 것으로 생각됩니다.

표준 세팅이 아닌 인디케이터는 다른 문제를 발생시킬 수 있습니다. 예를 들어 Price_Channel에는 아래와 같이 중요치 않은 줄 들이 있는데,

   PlotIndexSetInteger(0,PLOT_SHIFT,1);
   PlotIndexSetInteger(1,PLOT_SHIFT,1);

인디케이터 차트가 1 막대에서 밀렸다는걸 의미합니다. 우리 경우엔 CopyBufferCopyHigh 함수가 패러미터엔 같은 막대 좌표 (시간)을 다룸에도 불구하고 각기 다른 막대를 쓸 때의 상황을 나타냅니다.

이 문제는 트렌더-Include에서 해결됩니다 (CIchimoku 클래스와 달리 이 문제가 발생하지 않는 CChannel 클래스의 필수적 파트에 "1"들이 추가됩니다). 따라서 만약에 "독창적인" 인디케이터를 찾고있다면 이걸 쓰면 됩니다.

코드 수정을 끝마쳤으니 이제 두 인디케이터는 트렌더-Include 인디케이터 내부의 두 개의 클래스로 재편되었습니다. 이제 이 인디케이터들을 호출하는 방법을 바꾸면됩니다. 트렌더 안에는 핸들 어레이가 있고, 트렌더-Include에서는 객체 어레이로 대체됩니다.

// Handles of auxiliary indicator for all timeframes
//int h_Ichimoku[5], h_Channel[5];
// Instances of embedded auxiliary indicators
CIchimoku o_Ichimoku[5]; CChannel o_Channel[5];

OnInit 내에서 보조 인디케이터는 다음과 같이 만들어집니다.

   for (int itf=0; itf<5; itf++)
     {
      o_Ichimoku[itf].Create(Symbol(), TF[itf]);
      o_Ichimoku[itf].Init(9, 26, 52);
      o_Channel [itf].Create(Symbol(), TF[itf]);
      o_Channel [itf].Init(22);
     }

OnCalculate 내의 CopyBuffer는 객체 속성을 직접 호출하는 것으로 대체됩니다.

         //=== The Ichimoku indicator
         o_Ichimoku[itf].Calculate(Time);

         //CopyBuffer(h_Ichimoku[itf], 0, Time, 1, bufTenkan);
         //double Tenkan = bufTenkan[0];
         double Tenkan = o_Ichimoku[itf].ExtTenkanBuffer[0];

         //CopyBuffer(h_Ichimoku[itf], 1, Time, 1, bufKijun );    
         //double Kijun  = bufKijun [0];
         double Kijun  = o_Ichimoku[itf].ExtKijunBuffer [0];
           
         if (Tenkan > Kijun) Signal++;
         if (Tenkan < Kijun) Signal--;
          
         //=== The Channel indicator
         o_Channel[itf].Calculate(Time);

         //CopyBuffer(h_Channel [itf], 2, Time, 1, bufMid);
         //double Mid = bufMid[0];
         double Mid = o_Channel[itf].ExtMiddBuffer[0];

         if (Price > Mid) Signal++;
         if (Price < Mid) Signal--;

버퍼가 40개 줄었습니다. 고생한 보람이 있습니다.

앞에서 설명한 대로 "Need"와 "Aggregate" 메소드를 따라 트렌더를 수정하는 과정에서 저는 시각 모드로 인디케이터를 테스트해보았습니다.

이 테스트를 같이 해봅시다. 초기 인디케이터(트렌더)와 수정한 인디케이터(트렌더-Include)를 차트 상에 여십시오. 두 인디케이터의 선들이 제대로 겹치면 전부 제대로 되었다고 말할 수 있는 것입니다.


5.4. 하나 하나 처리 가능한가요?

지금까지 보조 인디케이터의 버퍼를 줄이는 3가지 방법에 대해 논의해보았습니다. 하지만 만약 버퍼 수를 줄이는게 아니라, 버퍼가 메모리 안에 동시에 점유하는 양을 줄이는 방식으로 접근해보면 어떻게 될까요? 다르게 말하자면 인디케이터를 메모리에 한번에 올릴게 아니라 하나 하나 할당하는 것이 어떤가 하는 것입니다. "우회로"를 구성해야 합니다. 모든 시간대를 살펴볼 때까지 보조 인디케이터 하나 만들기, 데이터 읽기, 삭제, 다음 인디케이터 만들기 등입니다. Ichimoku 인디케이터가 5개로 가장 많은 버퍼를 가지고 있습니다. 따라서 이론적으로 최대 5개의 버퍼를 메모리에 동시에 보관할 수 있으며(메인 인디케이터의 버퍼 1개 추가), 총 절약량은 35개의 버퍼입니다!

가능할까요? MQL5에는 인디케이터를 지우는 데에 쓰이는 특수 함수 IndicatorRelease가 있습니다.

그러나 보이는 것처럼 쉽지는 않습니다. MetaTrader 5는 MQL5 프로그램의 고속 작동에 신경을 쓰기 때문에 다른 EA, 인디케이터 또는 스크립트가 필요할 경우 호출된 모든 타임 프레임을 캐시에 보관합니다. 오랜 시간 동안 호출되지 않은 경우에만 메모리를 비우기 위해 언로드됩니다. 타임아웃은 최대 30분이나 걸릴 수 있습니다.

따라서 지표를 지속적으로 생성 및 삭제해도 메모리를 즉시 크게 절약할 수 없습니다. 그러나 컴퓨터가 생성될 때마다 전체 가격 기록에 대한 인디케이터가 계산되기 때문에 컴퓨터의 속도가 현저히 느려질 수 있습니다. 주요 인디케이터의 각 막대에서 이러한 작업을 수행하는 것이 얼마나 합리적인지 생각해 보십시오.

그럼에도 불구하고, "인디케이터 우회"에 대한 아이디어는 "브레인스토밍"으로서는 꽤 흥미로웠습니다. 인디케이터 메모리의 최적화에 대한 다른 독창적인 아이디어가 떠오르면, 문서에 의견을 적어주세요. 아마도 그들은 이 주제에 대한 다음 문서들 중 하나에 반영될 수 있습니다..


6. 실질 메모리 점유량 측정하기

앞 챕터에서는 보조 인디케이터의 버퍼 수를 줄이는 3가지 방법을 구현해보았습니다. 이제 메모리에서 실제 소모량을 줄이는 방법을 분석해봅시다.

MS 윈도우즈의 "작업관리자"를 사용하여 터미널이 사용하는 메모리 크기를 측정하려고 합니다. "프로세스" 탭에서 클라이언트 터미널에서 사용하는 RAM 및 가상 메모리의 크기를 볼 수 있습니다. 예를 들어


터미널에서 이하의알고리즘을 통해 메모리 점유율이 가장 낮은 메모리를 확인합니다 (인디케이터가 실질적으로 소비하는 메모리와 비슷한 양입니다)..

  1. MetaQuotes-Demo 서버에서 상세 가격 기록을 다운로드하십시오(기록 자동 다운로드용 기호에서 테스트 실행으로 충분함).
  2. 다음 측정(필수 차트 및 인디케이터 열기)을 위한 단자를 설정하고 다시 시작하여 메모리에서 불필요한 정보를 비웁니다.
  3. 다시 시작된 단자가 모든 인디케이터 계산을 완료할 때까지 기다립니다. 프로세서를 0으로 로드하면 알 수 있습니다.
  4. 작업 표시줄에서 터미널을 최소화합니다(터미널 오른쪽 상단 모서리에 있는 표준 버튼 "최소화"를 클릭하여 터미널을 최소화합니다. 현재 계산에 사용되지 않는 여유 메모리(위의 스크린샷에서는 여전히 최소화된 상태의 메모리 사용 예를 볼 수 있습니다. 즉, 가상 메모리보다 훨씬 적은 RAM을 볼 수 있습니다.)
  5. "작업 관리자"에서 "메모리 사용량"(RAM) 및 "VM 크기"(가상 메모리) 열을 요약합니다. Windows XP에서는 이렇게 부릅니다. 다른 버전의 OS에서는 이름이 약간 다를 수 있습니다.

측정 패러미터

  • 더 정확한 측정을 위해, 우리는 하나의 가격표(즉, 22 M1 차트) 대신 MetaQuotes 데모 계정에서 사용 가능한 모든 화폐쌍을 사용할 것입니다. 그런 다음 평균값을 계산합니다.
  • 4.1장에 설명된 "차트의 최대 막대" 옵션에는 표준 값 - 100000이 있습니다.
  • OS - 윈도우즈 XP, 32 비트

측정 결과 어떤게 보일까요? 두가지 언급하고싶은 것이 있습니다.

  1. Trender 표시기가 41개의 버퍼를 사용한다고 해서 41*100000개의 바가 소모되는 것은 아닙니다. 그 이유는 버퍼가 5개의 시간대에 분산되어 있고 큰 시간대에 포함된 막대가 작기 때문입니다. 예를 들어, EURUSD의 M1 히스토리에는 약 400만 개의 막대가 포함되어 있으며 H1 히스토리는 70000개의 막대로만 구성되어 있습니다(40000/60). 따라서 트렌더의 버퍼 수를 줄인 후에도 메모리 소비량이 동일하게 감소하지 않아야 합니다.
  2. 메모리는 인디케이터 자체뿐만 아니라 인디케이터가 사용하는 프라이스 시리즈에 의해서도 소비됩니다. 트렌더엔 5개의 타임프레임이 있습니다. 따라서 버퍼 수를 몇 배 줄이면 메모리 총 소비량이 그만큼 줄어들지 않습니다. 왜냐하면 메모리에 있는 5개의 가격 시리즈가 모두 사용될 것이기 때문입니다.

소비량을 측정할 때 메모리 소비량에 영향을 미치는 다른 요인에 직면할 수 있습니다. 인디케이터의 최적화를 통해 실제 경제를 보기 위해 이렇게 합니다..

아래에서 모든 측정 결과가 포함된 표를 찾을 수 있습니다. 우선, 저는 빈 단말기가 사용하는 메모리 크기를 측정했습니다. 다음 측정치에서 해당 값을 빼서 하나의 차트에서 사용된 메모리의 크기를 계산할 수 있습니다. 다음 측정치에서 터미널에서 소비되는 메모리와 차트 하나를 빼면 각 인디케이터에서 소비되는 메모리의 크기를 구할 수 있습니다.

메모리 이용자
인디케이터 버퍼
타임 프레임 수
메모리 점유량
클라이언트 터미널
0
0
38 Mb - 터미널
차트
0
1
12 Mb - 빈 차트
트렌더 인디케이터
41
5
46 Mb - 단일 인디케이터
트렌더-Need 인디케이터
31
5
42 Mb - 단일 인디케이터
트렌더-Aggregate 인디케이터 21
5
37 Mb - 단일 인디케이터
트렌더-Include 인디케이터 1
5
38 Mb - 단일 인디케이터


이 결론은 측정 결과를 기반으로 만들어졌습니다.

  • 인디케이터 버퍼 수가 감소하면 표시기에 사용되는 메모리가 동일하게 감소하지 않습니다.
이게 왜 발생하는가에 대해서는 이 장의 위쪽에서 설명드렸습니다. 인디케이터가 더 적은 시간대를 사용하는 경우 버퍼 수를 줄이는 효과가 더 클 수 있습니다.
  • 보조 인디케이터의 코드를 메인 인디케이터로 이동한다고 해서 항상 최상의 결과가 나오는 것은 아닙니다.

그렇다면 Include 방법이 Aggregate만큼 효과적이지 않은 이유는 무엇일까요? 그 이유를 밝히기 위해 우리는 이 인디케이터들의 코드의 주요 차이점을 기억해야 합니다. Aggregate에서 계산에 필요한 가격 시리즈는 OnCalculate에서 입력 어레이로 터미널을 통과합니다. Include에서는 모든 데이터 (모든 타임 프레임의)가 CopyHigh, CopyLow 그리고 CopyClose를 통해 능동적으로 요청됩니다. 이러한 기능을 사용할 때 가격 시계열의 캐싱 특성으로 인해 메모리가 추가로 소비될 수 있습니다.


마치며

그래서 이 글은 보조 인디케이터의 메모리 소비를 줄이는 3가지 작업 방법과 클라이언트 단말기를 조정하여 메모리를 절약하는 1가지 방법에 대해 설명합니다.

적용해야 할 방법은 상황에 따라 수용 가능성과 적절성에 따라 달라집니다. 저장된 버퍼 및 메가바이트 수는 작업하는 표시기에 따라 다릅니다. 일부 표시기에는 "잘라내기"가 가능하고 다른 표시기에는 아무것도 할 수 없습니다.

메모리를 저장하면 터미널에서 동시에 사용되는 통화 쌍의 수를 늘릴 수 있습니다. 이로서 매매 포트폴리오의 신뢰도를 높일 수 있습니다. 이러한 간단한 관리만으로도 당신의 컴퓨터 리소스가 자산으로 환원될 수 있습니다.


첨부파일

본 문서 중에서 다룬 인디케이터들을 첨부해두었습니다. 제대로 기동되려면 이 파일들은 "MQL5\Indicators\TestSlaveIndicators" 폴더에 위치하여야합니다. 트렌더 인디케이터(Trender-Include 제외)의 모든 버전에서 보조 인디케이터를 찾으러 저 폴더를 체크하기 때문입니다.


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

MQL5에서의 엘리엇 파동(Elliot Waves) 자동 분석 도입 MQL5에서의 엘리엇 파동(Elliot Waves) 자동 분석 도입
시장 분석에서 가장 인기 좋은 방법 중 하나는 엘리엇 파동(Elliot Wave) 이론입니다. 하지만 이 과정은 꽤나 복잡하기때문에 다른 툴을 사용해야합니다. 그런 툴 중 하나는 자동 마커입니다. 본 문서에서는 MQL5로 엘리엇 파동 자동 분석기를 만드는 법에 대해서 다뤄보겠습니다.
MQL5 마법사에 나만의 Expert Advisor 만들기 MQL5 마법사에 나만의 Expert Advisor 만들기
이제는 프로그래밍 언어를 몰라도 매매 봇을 만들 수 있게 되었습니다. 옛날에는 프로그래밍을 할 줄 모르면 자신의 매매 전략을 도입하여 봇을 만들기가 무척 어려웠습니다만, MQL5 마법사가 도입되면서 상황은 급반전하였습니다. 이제 신규 트레이더들은 프로그래밍 경험이 없다고해서 두려워할 필요가 없어졌습니다. 새로운 MQL5 마법사와 함께라면 Expert Advisor를 짜는데에 프로그래밍 경험은 필요 없습니다.
MQL5에서 WinInet 사용하기 파트 2: POST 리퀘스트 및 파일 MQL5에서 WinInet 사용하기 파트 2: POST 리퀘스트 및 파일
본 문서에서는 우리는 인터넷을 HTTP 리퀘스트를 다루는 법에 대하여 계속하여 알아보고 실제 서버와 정보 교환을 해볼 것입니다. CMqlNet 클래스 내의 신규 함수를 다뤄보고 POST 리퀘스트를 통해 정보를 보내는 메소드, 그리고 Cookies를 이용해 홈페이지 로그인 인증을 처리하는 것을 다뤄봅니다.
Expert Advisor를 주문하고 원하는 결과를 얻는 방법 Expert Advisor를 주문하고 원하는 결과를 얻는 방법
요구사항 사양을 올바르게 작성하는 방법은 무엇입니까? Expert Advisor 또는 지표를 주문할 때 프로그래머에게 무엇을 기대해야 하고 기대해서는 안 됩니까? 대화를 유지하는 방법, 특별히 주의해야 할 순간은 무엇입니까? 이 글은 이것들과 많은 다른 질문들에 대한 해답을 주는데, 이것은 많은 사람들에게 명백해 보이지 않는 경우가 많습니다.