English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
인디케이터 데이터 교환: 쉬워요!

인디케이터 데이터 교환: 쉬워요!

MetaTrader 5 | 5 7월 2021, 13:00
137 0
Alexey Subbotin
Alexey Subbotin

개요

뉴비들은 대체 검색할 줄을 모르는 것 같아요. 'FAQ'니, '뉴비 전용'이니, '이것도 모르면 그냥 나가 뒤지세요' 같은 자료가 얼마나 많은데요. 항상 '이건 어떻게 하나요', '이건 할 수 있나요' 같은 질문이나 던지고 '절대 안됨' 아니면 '불가능함'이라는 답을 받죠.

하지만 '절대 안된다고는 하지 말라'는 말이 과학자나 엔지니어, 프로그래머들 사이에서는 새로운 것을 고안하고 창조하는 데에 항상 자극이 되었죠.

1. 문제 정의하기

MQL4 커뮤니티 포럼에 올라온 글을 인용해 볼게요(러시아어 원문 번역).

 ...인디케이터가 두 개 있습니다(각각 A, B로 지칭). 인디케이터 A는 가격 차트에서 직접 데이터를 가져 오고, 인디케이터 B는 인디케이터 A의 데이터를 이용합니다. 여기서 문제를 하나 낼게요. iCustom("indicator A", ...)을 사용하지 않고 A의 데이터(이미 추가된 상태)를 이용해 B의 연산을 하려면 어떻게 해야 할까요? 참고로 A의 설정을 변경하는 경우 B에 그 결과가 반영되어야 합니다.

혹은

이동평균을 차트에 추가한다고 가정하겠습니다. 데이터 버퍼에 직접 접근할 수 있는 방법이 뭘까요?

아니면

... iCustom을 이용해 인디케이터를 호출하는 경우, 이전에 로드된 인디케이터도 다시 로드됩니다. 이를 방지할 방법이 있을까요?

이런 질문은 끝도 없고, 뉴비들만 묻는 것도 아닙니다. 일반적인 경우, MetaTrader에서 iCustom(MQL4) 또는 iCustom과 CopyBuffer 바인딩(MQL5)을 사용하지 않고는 커스텀 인디케이터 데이터에 접근할 방법이 없다는 것이 문제가 되는데요. 그래서 특정 차트에서 데이터를 가져 오거나 해당 데이터를 이용해 연산을 할 수 있는 MQL 함수가 개발되면 너무 좋을 것 같아요.

아무래도 위에 언급된 기본 함수만 가지고 작업하려면 좀 불편하니까요. 예를 들어 MQL4의 경우 iCustom을 호출하면 각 호출자별 인디케이터 복사본이 생성됩니다. MQL5에서는 핸들 덕분에 이런 문제가 약간 개선되었죠. 이제 모든 연산은 원본 파일에서 단 한번 실행됩니다. 남은 문제도 있죠. 참조하는 인디케이터가 많은 양의 계산 리소스를 소모하는 경우 터미널을 재초기화할 때마다 약 12초간 리소스가 고갈될 겁니다.

출처에 액세스하는 방법으로는 다음을 포함한 여러가지가 있습니다.

  • 가상 메모리 또는 물리적 디스크에 파일 매핑
  • DLL을 통한 데이터 공유
  • 클라이언트 터미널 내 전역 변수를 이용한 데이터 교환 및 저장

소켓이나 메일슬롯 등을 이용한 이색적인 방법은 물론 위의 방법을 변형한 방법들도 있습니다. 인디케이터 연산을 엑스퍼트 어드바이저 코드로 직접 전송하는 꽤 급진적인 방법도 있지만 이번 글에서 다룰 내용과는 거리가 좀 있죠.

원문의 저자는 각각의 장점이 있다고 생각하지만 공통적인 단점도 있죠. 데이터가 분산되기 전에 먼저 어딘가에 복사되어야 한다는 겁니다. 이는 CPU 사용율을 증가시킬 뿐만 아니라 전송된 데이터의 적합성과 관련된 또 다른 문제를 야기하죠. 여기에 대해서는 다음에 기회가 되면 이야기할게요.

이번에 다룰 문제는 다음과 같습니다.

우리는 차트에 이미 추가되어 있는 인디케이터의 데이터에 대한 액세스가 가능한 동시에 다음의 특성을 갖는 환경을 원합니다.

  • 데이터 복사 불필요(그러니까 적합성 문제도 없겠죠?)
  • 기존 코드 필요 시 수정 최소화
  • MQL 코드 선호(물론 DLL을 사용하긴 하지만 C++ 문자열로 만들 거예요)

저자는 C++빌더를 이용해 DLL을 생성했으며 MetaTrader4와 MetaTrader5 클라이언트 터미널을 이용했습니다. 아래의 소스코드는 MQL5로 작성되었으며 MQL4 코드는 본문에 포함되어 있습니다. 둘 사이의 주요 차이점도 알아볼 거예요.

2. 배열

우선 이론을 좀 살펴보죠. 인디케이터 버퍼를 이용하려면 데이터가 어떻게 저장되는지 알아야 하니까요. 제대로된 별도의 관련 자료가 없네요.

MQL의 동적 배열은 크기가 다양합니다. 배열의 크기는 커졌는데 필요한 메모리가 남아있지 않은 경우 MQL은 흥미로운 방식으로 데이터를 재분배하는데요. 다음의 두 가지 방법이 있습니다.

  1. 새로운 데이터를 사용 가능한 메모리에 추가 재할당(참조 목록 등을 이용해 해당 배열의 주소 저장)
  2. 할당 가능한 메모리로 배열 전체 이동

첫 번째 방법은 추가적인 문제를 발생시킬 수 있습니다. MQL 컴파일러에서 생성된 자료 구조를 확인해야 하니까요. 두 번째 방법은 더 오래 걸립니다. 동적 배열이 외부 함수, 즉 DLL로 보내질 경우 외부 함수는 데이터의 첫 번째 요소를 포인터 변수로 선언하며, 배열의 나머지 요소들은 첫 번째 요소를 따라 순서대로 위치합니다.

참조 전달 시 배열 데이터가 변경될 수 있으므로 첫 번째 방법을 사용하면 전체 배열을 별개의 메모리 영역에 복사하고 외부 함수로 전송한 다음에야 소스 배열에 결과를 추가할 수 있다는 점이 문제가 됩니다. 결국 두 번째 방법과 동일한 작업을 수행하는 거죠.

비록 논리적으로 100% 맞는 결론은 아니지만 그래도 신뢰할 만한 결론입니다(우리 제품으로 검증되었기도 하고요).

그러니까 다음이 옳다고 가정할게요.

  • 매 순간마다 동적 배열 데이터는 메모리에 순차적으로 위치하게 됩니다. 배열은 메모리의 다른 부분에 재배치될 수 있지만 반드시 전체 배열을 재배치해야 합니다.
  • 동적 배열이 매개 변수로 외부 함수에 참조 전달될 때 첫 번째 배열 요소의 주소가 DLL로 전달됩니다.

다음과 같은 가정도 취할 수 있죠. '매 순간'이란 상응하는 인디케이터의 OnCalculate()(MQL4의 경우 ()로 시작) 함수가 호출될 때이며, 간편함을 위해 데이터 버퍼가 동일한 차원에 존재하며 더블 형식을 갖는다고 가정합니다.

3. 씨네쿼넌(Sine Qua Non)

MetaTrader사가 늘 강조하지만 MQL은 포인터를 지원하지 않습니다(객체 포인터가 있지만 엄밀히 말해 포인터는 아니죠). 정말일까요?

포인터란 뭘까요? 그저 별표가 달린 식별자만은 아니고요. 메모리 내 셀의 주소입니다. 그렇다면 셀 주소란 뭘까요? 특정 시작 값을 갖는 시퀀스 번호예요. 마지막으로, 번호, 즉 숫자란 뭘까요? 컴퓨터 메모리의 특정 셀에 해당하는 정수입니다. 그렇다면 왜 포인터를 정수처럼 사용할 수 없는 걸까요? 사실, 사용할 수 있습니다. MQL 프로그램에서 정수는 자유롭게 사용되니까요!

그런데 어떻게 포인터를 정수로 변환하죠? DLL의 도움을 받아 C++ 형 변환을 할 거예요. C++ 포인터는 4바이트 자료형이므로 정수를 사용하기 딱 좋겠네요.

부호 비트는 중요하지 않으므로 신경 쓰지 않겠습니다(부호 비트가 1이면 정수가 음수라는 의미). 중요한 건 포인터 비트를 그대로 사용할 수 있다는 거죠. 물론 부호가 없는 정수형을 사용할 수도 있습니다만, MQL4에서는 지원되지 않으니 MQL5와 MQL4에서 모두 쓸 수 있도록 비슷하게 만드는 편이 좋겠죠.

그러니까...

extern "C" __declspec(dllexport) int __stdcall GetPtr(double *a)
{
        return((int)a);
}

오우, 이제 배열의 시작 주소를 값으로 하는 4비트 정수형 변수(long)를 만들 수 있게 되었군요! 이제 i 번째 배열 요소 값을 읽는 법을 배워야겠네요.

extern "C" __declspec(dllexport) double __stdcall GetValue(int pointer,int i)
{
        return(((double*) pointer)[i]);
}

그리고 값을 쓰는 방법도요(아직 필요하지 않긴 하지만 그래도...).

extern "C" __declspec(dllexport) void __stdcall SetValue(int pointer,int i,double value)
{
        ((double*) pointer)[i]=value;
}

이게 다에요. 이제 MQL로 포인터를 사용할 수 있어요.

4. 래핑

그럼 핵심은 잡았으니 이제 어떻게하면 MQL 프로그램에서 사용하기 편하게 만들 수 있을지 고민해 보죠. 심미성을 중시하시는 분들을 위한 내용도 있을테니 미리 기분 상하지 않으셨으면 좋겠네요.

사실 방법은 한도 끝도 없어요. 우리는 아래의 방법을 따르도록 할게요. 클라이언트 터미널이 서로 다른 MQL 프로그램 간의 데이터 교환을 가능하게 하는 특별한 기능을 갖고 있다는 걸 잊지 말자고요. 바로 전역 변수라는 거였죠. 전역 변수는 인디케이터 버퍼에 포인터를 저장하는 용도로 가장 많이 쓰입니다. 이런 변수들을 디스크립터 테이블로 생각할게요. 각각의 디스크립터는 아래와 같이 문자열로 된 이름을 가질 거예요.

string_identifier#buffer_number#symbol#period#buffer_length#indexing_direction#random_number,

그리고 그 값은 메모리에 저장된 버퍼 포인터에 해당하는 정수가 됩니다.

이제 디스크립터 필드를 좀 살펴보죠.

  • string_identifier–필요한 포인터 서치에 사용되는 모든 문자열(예: 인디케이터 명칭으로 short_name 변수 사용 가능; 일부 인디케이터는 동일한 식별자를 갖는 디스크립터를 등록하므로 필드를 사용하여 구분함)
  • buffer_number–버퍼를 구분합니다.
  • buffer_length–버퍼 길이에 제한을 두기 위해 필요하죠. 클라이언트 터미널이 갑자기 멈추거나 블루스크린이 뜨면 안되니까요.
  • symbol, period–특정 차트 창 지정을 위한 심볼과 기간을 입력합니다.
  • ordering_direction–배열 요소의 순서를 결정합니다: 0 - 정방향 배열, 1 - 역방향 배열(AS_SERIES 플래그는 참 값을 갖습니다).
  • random_number–클라이언트 터미널에 추가된 다른 창 또는 다른 매개 변수를 갖는 인디케이터의 사본이 여러 개인 경우 사용됩니다(첫 번째와 두 번째 필드에 동일한 값을 지정할 수 있으므로 구분이 필요). 최선은 아닐지 몰라도 일단 쓸 만해요.

우선 디스크립터를 등록하거나 삭제하기 위한 함수가 필요합니다. 함수의 첫 번째 문자열을 보죠. 전역 변수 리스트에서 이전의 디스크립터를 삭제하기 위해 UnregisterBuffer() 함수를 호출합니다.

새로운 바가 생성될 때마다 버퍼 사이즈가 1씩 증가하므로 RegisterBuffer() 함수를 호출해야 합니다. 또한 버퍼 사이즈가 변하는 경우 테이블에 새로운 디스크립터(이름에 크기 정보 반영)가 생성됩니다. 기존의 디스크립터 또한 테이블에 남아 있습니다. 그러니까 필요하겠죠?

void RegisterBuffer(double &Buffer[], string name, int mode) export
{
   UnregisterBuffer(Buffer);                    //first delete the variable just in case
   
   int direction=0;
   if(ArrayGetAsSeries(Buffer)) direction=1;    //set the right ordering_direction

   name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction;
   int ptr=GetPtr(Buffer);                      // get the buffer pointer

   if(ptr==0) return;
   
   MathSrand(ptr);                              //it's convenient to use the pointer value instead
                                                //of the current time for the random numbers base 
   while(true)
   {
      int rnd=MathRand();
      if(!GlobalVariableCheck(name+"#"+rnd))    //check for unique name - we assume that 
      {                                         //nobody will use more RAND_MAX buffers :)
         name=name+"#"+rnd;                     
         GlobalVariableSet(name,ptr);           //and write it to the global variable
         break;
      }
   }   
}
void UnregisterBuffer(double &Buffer[]) export
{
   int ptr=GetPtr(Buffer);                      //we will register by the real address of the buffer
   if(ptr==0) return;
   
   int gt=GlobalVariablesTotal();               
   int i;
   for(i=gt-1;i>=0;i--)                         //just look through all global variables
   {                                            //and delete our buffer from all places
      string name=GlobalVariableName(i);        
      if(GlobalVariableGet(name)==ptr)
         GlobalVariableDel(name);
   }      
}

더 자세한 설명은 군더더기일뿐입니다. 첫 번째 함수가 전역 변수 목록에 위의 형식의 새로운 디스크립터를 생성한다는 것과 두 번째 함수가 전역 변수를 검색 및 삭제하며 포인터와 동일한 값을 가진 디스크립터를 삭제한다는 것만 알면 됩니다.

이제 인디케이터에서 데이터를 가져올 차례입니다. 데이터에 대한 직접 액세스의 구현에 앞서 그에 상응하는 디스크립터를 찾아야 합니다. 다음의 함수를 이용해 해결할 수 있죠. 알고리즘은 다음과 같습니다. 모든 전역 변수를 살펴보고 해당 디스크립터에 지정된 값을 갖는 필드 값을 찾는 것이죠.

필드 값을 찾으면 마지막 매개 변수로 전달된 배열에 그 이름을 추가합니다. 결과적으로 함수는 검색 조건에 맞는 모든 메모리 주소를 반환하게 됩니다. 반환된 값은 발견된 디스크립터의 개수입니다.

int FindBuffers(string name, int mode, string symbol, int period, string &buffers[]) export
{
   int count=0;
   int i;
   bool found;
   string name_i;
   string descriptor[];
   int gt=GlobalVariablesTotal();

   StringTrimLeft(name);                                    //trim string from unnecessary spaces
   StringTrimRight(name);
   
   ArrayResize(buffers,count);                              //reset size to 0

   for(i=gt-1;i>=0;i--)
   {
      found=true;
      name_i=GlobalVariableName(i);
      
      StringExplode(name_i,"#",descriptor);                 //split string to fields
      
      if(StringFind(descriptor[0],name)<0&&name!=NULL) found=false; //check each field for the match
                                                                    //condition
      if(descriptor[1]!=mode&&mode>=0) found=false;
      if(descriptor[2]!=symbol&&symbol!=NULL) found=false;
      if(descriptor[3]!=period&&period>0) found=false;
      
      if(found)
      {
         count++;                                           //conditions met, add it to the list
         ArrayResize(buffers,count);
         buffers[count-1]=name_i;
      }
   }
   
   return(count);
}

함수 코드에서 확인할 수 있듯, 일부 검색 조건은 생략 가능합니다. 예를 들어 NULL을 이름으로 전달하는 경우, string_identifier에 대한 확인을 생략할 수 있겠죠. 다른 필드도 마찬가지입니다. mode<0, symbol:=NULL 또는 period<=0 같은 경우죠. 디스크립터 테이블에서 검색 옵션을 확장할 수 있습니다.

예를 들어 이동평균 인디케이터를 모든 차트창에서 검색할 수도 있고 기간이 M15인 EURUSD 차트에서만 검색할 수도 있는 거죠. 추가 설명을 덧붙이자면 string_identifier 검사는 동등성 검정이 아닌 StringFind() 함수로 수행됩니다. 디스크립터의 일부만을 가지고 검색할 수 있도록 하기 위함이죠(복수 개의 인디케이터가 'MA(xxx)'형의 문자열을 설정한 경우 하위 문자열 'MA'로 검색을 수행해 등록된 모든 이동평균 검색 가능).

또한 StringExplode(string s, string separator, string &result[]) 함수도 사용했습니다. 구분 기호를 이용해 특정 문자열 s를 하위 문자열로 분할하고 결과 배열에 결과를 작성합니다.

void StringExplode(string s, string separator, string &result[])
{
   int i,pos;
   ArrayResize(result,1);
   
   pos=StringFind(s,separator); 
   if(pos<0) {result[0]=s;return;}
   
   for(i=0;;i++)
   {
      pos=StringFind(s,separator); 
      if(pos>=0)
      {
         result[i]=StringSubstr(s,0,pos);
         s=StringSubstr(s,pos+StringLen(separator));
      }
      else break;
      ArrayResize(result,ArraySize(result)+1);
   }
}

이제 필요한 전체 디스크립터 목록이 있으니 인디케이터에서 데이터를 얻을 수 있습니다.

double GetIndicatorValue(string descriptor, int shift) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //check that the descriptor is valid
   {                
      ptr = GlobalVariableGet(descriptor);             //get the pointer value
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //split its name to fields
         size = fields[4];                             //we need the current array size
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //if the ordering_direction is reverse
         if(shift>=0&&shift<size)                      //check for its validity - to prevent crashes
            return(GetValue(MathAbs(ptr),shift));      //ok, return the value
      }   
   } 
   return(EMPTY_VALUE);                                //overwise return empty value 
}

보시다시피 DLL의 GetValue() 함수를 래핑한 것이죠. 디스크립터의 유효성과 배열 경계를 검사하고 인디케이터 버퍼 순서를 고려해야 합니다.. 실패할 경우 함수는 EMPTY_VALUE를 반환합니다.

인디케이터 버퍼 수정 방법도 이와 비슷하죠.

bool SetIndicatorValue(string descriptor, int shift, double value) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //check for its validity
   {                
      ptr = GlobalVariableGet(descriptor);             //get descriptor value
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //split it to fields
         size = fields[4];                             //we need its size
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //the case of the inverse ordering
         if(shift>=0&&shift<size)                      //check index to prevent the crash of the client terminal
         {
            SetValue(MathAbs(ptr),shift,value);
            return(true);
         }   
      }   
   }
   return(false);
}

모든 값이 유효한 경우 DLL에서 SetValue() 함수를 호출합니다. 반환된 값은 수정 결과입니다. 성공한 경우 참, 오류가 발생한 경우 거짓이죠.

5. 작동 확인하기

이제 확인을 좀 해야 하겠습니다. 스탠다드 패키지의 평균 실제 범위(ATR)를 타겟으로 삼아 다른 인디케이터 창에 값을 복사할 수 있도록 하려면 코드를 어떻게 수정해야 하는지 보겠습니다. 버퍼 데이터 수정도 테스트할 것이고요.

우선 OnCalculate() 함수 코드에 라인을 조금 추가합니다.

//+------------------------------------------------------------------+

//| Average True Range                                               |
//+------------------------------------------------------------------+
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 &TickVolume[],
                const long &Volume[],
                const int &Spread[])
  {
   if(prev_calculated!=rates_total)
      RegisterBuffer(ExtATRBuffer,"ATR",0);
…

새로운 바가 생길 때마다 버퍼 레지스터가 작동하는 것을 확인할 수 있습니다. 일반적으로 시간이 흐르거나 추가 기록 데이터를 다운로드할 때 발생하는 경우입니다.

게다가 인디케이터 오퍼레이션이 끝나면 테이블의 디스크립터를 삭제해야 하기도 하고요. 그렇기 때문에 deinit 이벤트 핸들러에 코드를 추가해야 합니다(반환 값이 없어야 하고 하나의 입력 변수인 const int reason이 있어야 함을 잊지 마세요).

void OnDeinit(const int reason)
{
   UnregisterBuffer(ExtATRBuffer);
}

클라이언트 터미널 차트 창의 수정된 인디케이터(그림 1.)와 전역 변수 목록의 디스크립터(그림 2.)입니다.

 

그림 1. 평균 실제 범위

그림 2. 클라이언트 터미널 전역 변수 목록에 생성된 디스크립터

그림 2. 클라이언트 터미널 전역 변수 목록에 생성된 디스크립터

다음 차례는 ATR 데이터에 액세스하는 것입니다. 다음의 코드로 새로운 인디케이터(이름은 test)를 생성해 봅시다.

//+------------------------------------------------------------------+
//|                                                         test.mq5 |
//|                                             Copyright 2009, alsu |
//|                                                 alsufx@gmail.com |
//+------------------------------------------------------------------+
#property copyright "2009, alsu"
#property link      "alsufx@gmail.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#include <exchng.mqh>

//---- plot ATRCopy
#property indicator_label1  "ATRCopy"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Red
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- indicator buffers
double ATRCopyBuffer[];

string atr_buffer;
string buffers[];

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,ATRCopyBuffer,INDICATOR_DATA);
//---
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//---
   int found=FindBuffers("ATR",0,NULL,0,buffers);   //search for descriptors in global variables
   if(found>0) atr_buffer=buffers[0];               //if we found it, save it to the atr_buffer
   else atr_buffer=NULL;
   int i;
   for(i=prev_calculated;i<rates_total;i++)
   {
      if(atr_buffer==NULL) break;
      ATRCopyBuffer[i]=GetIndicatorValue(atr_buffer,i);  //now it easy to get data
      SetIndicatorValue(atr_buffer,i,i);                 //and easy to record them
   }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

어렵지 않죠? 하위 문자열을 이용해 디스크립터 리스트를 얻고 함수를 이용해 인디케이터 버퍼 값을 읽으세요. 마지막으로 아무거나 좀 써 봅시다(예제에서는 배열 요소 인덱스에 상응하는 값을 입력).

이제 test 인디케이터를 작동시켜 보죠(타겟 ATR이 여전히 차트에 추가된 상태). 그림 3.에서 확인할 수 있듯 ATR 값은 하단 하위 창(테스트 인디케이터 내)에 표시되며 대신 직선이 나타나는데 이는 실제로 배열 인덱스에 해당하는 값으로 채워져 있습니다.

 

그림 3. Test.mq5 오퍼레이션 결과

손만 까딱하면 된다니까요.

6. 하위 호환성

MQ4 기준에 최대한 가까운 MQL5 라이브러리를 생성하고자 했습니다. 최소한의 수정만 거치면 MQL4에서 사용할 수 있도록 말입니다.

MQL5에만 있는 내보내기 키워드는 삭제하고 간접적 형 변환이 포함된 코드는 수정합니다(MQL4는 유연성이 별로 없어요). 사실 인디케이터 내 함수 사용법은 완전히 동일하며 유일한 차이점은 새로운 바의 메소드와 기록 추가 제어의 경우에만 나타납니다.

결론

장점에 대해서 조금 이야기해 보죠.

지금까지 이야기한 메소드는 단 하나의 목적만을 갖고 있지 않습니다. 고속 캐스케이드 인디케이터 구축 외에도 라이브러리는 기록 테스트를 포함하여 엑스퍼트 어드바이저에 성공적으로 적용될 수 있습니다. 다른 애플리케이션에 대해서는 알려진 바가 없지만 아마 우리 독자분들이 시험해 보실 것 같네요.

다음과 같은 기술적 도약이 필요할 것 같아요.

  • 디스크립터 테이블 개선(특히 윈도우 표시)
  • 디스크립터 생성 순서를 나타내는 추가 테이블 개발(매매 시스템 안정성에 기여 가능)

라이브러리 개선에 도움이 되는 의견은 언제든 환영입니다.

필요한 모든 파일은 본문에 첨부되어 있습니다. MQL5 및 MQL4 전용 코드와 exchng.dll 라이브러리 소스 코드도 있고요. 해당 파일들은 클라이언트 터미널 폴더와 동일한 경로에 저장됩니다.

경고

본문에 언급된 프로그래밍 기술을 부주의하게 사용할 시 컴퓨터에서 실행 중인 소프트웨어에 손상을 일으킬 수 있습니다. 첨부파일 다운로드에 대한 책임은 귀하에게 있으며 아직 발생된 적은 없지만 부작용을 초래할 수 있습니다.

감사의 말

MQL4.커뮤니티 포럼인 https://forum.mql4.com에 다음의 사용자들이 제시한 의견의 도움을 받았습니다. igor.senych, satop, bank, StSpirit, TheXpert, jartmailru, ForexTools, marketeer, IlyaA혹시 빠진 분이 계시면 사과드립니다.

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

파일 첨부됨 |
cpp.zip (0.36 KB)
mql4.zip (39.29 KB)
mql521x.zip (46.87 KB)

이 작가의 다른 글

MQL5로 방출형 인디케이터 그리기 MQL5로 방출형 인디케이터 그리기
이 글에서는 새로운 시장 조사 접근법인 방출형 인디케이터에 대해 알아보겠습니다. 방출은 서로 다른 인디케이터의 교차점을 기반으로 계산됩니다. 각각의 틱 다음에 형형색색의 점이 나타나죠. 이 점들이 모여 성운, 구름, 궤도, 직선, 포물선 등의 형태를 갖는 클러스터를 형성합니다. 클러스터의 모양에 따라 시장 가격의 변화에 영향을 미치는, 눈에는 보이지 않는 원동력을 어느 정도 감지할 수 있죠.
데이터 교환 방법: 10 분 안에 MQL5 용 DLL 데이터 교환 방법: 10 분 안에 MQL5 용 DLL
이제 많은 개발자가 간단한 DLL을 작성하는 방법과 다른 시스템 바인딩의 특수 기능이 무엇인지 기억하지 못합니다. 몇 가지 예제를 사용하여 간단한 DLL 생성의 전체 프로세스를 10 분 안에 보여주고 바인딩 구현에 대한 몇 가지 기술적인 세부 사항을 논의하려고 합니다. 다양한 유형의 변수 (숫자, 배열, 문자열 등)를 교환하는 예제와 함께 Visual Studio에서 DLL을 만드는 단계별 프로세스를 보여 드리겠습니다. 게다가 사용자 지정 DLL의 충돌로부터 클라이언트 터미널을 보호하는 방법을 설명합니다.
WCF 서비스를 통해 MetaTrader5에서 .NET 애플리케이션으로 인용문 내보내기 WCF 서비스를 통해 MetaTrader5에서 .NET 애플리케이션으로 인용문 내보내기
MetaTrader5에서 다른 애플리케이션으로 인용문을 내보내고 싶으신가요? MQL5와 DLL이 함께라면 문제 없어요! 이 글은 MetaTrader5에서 .NET 애플리케이션으로 인용문을 내보내는 방법에 대한 설명입니다. 개인적으로는 .NET 플랫폼을 이용하는 게 훨씬 재밌고, 합리적이고, 간편하네요. 안타깝게도 MetaTrader5는 아직 .NET 형식을 지원하지 않기 때문에 예전과 같이 .NET 형식을 지원하는 win32 dll을 중간층으로 사용하겠습니다.
가격 히스토그램 (시장 프로필) 및 MQL5에서 구현 가격 히스토그램 (시장 프로필) 및 MQL5에서 구현
시장 프로필은 정말 뛰어난 사상가인 Peter Steidlmayer가 개발했습니다. 그는 완전히 다른 모델 세트로 이어지는 "수평" 및 "수직"시장 이동에 대한 정보의 대체 표현을 사용할 것을 제안했습니다. 그는 시장의 근본적인 맥박이나 균형과 불균형의 순환이라는 근본적인 패턴이 있다고 가정했습니다. 이 기사에서는 시장 프로필의 단순화된 모델인 가격 히스토그램을 고려하고 MQL5에서의 구현에 대해 설명합니다.