English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
미지의 확률 밀도 함수에 대한 커널 밀도 추정

미지의 확률 밀도 함수에 대한 커널 밀도 추정

MetaTrader 5통계 및 분석 | 11 10월 2021, 16:09
244 0
Victor
Victor

개요

MQL5의 성능이 업그레이드되고 PC의 생산성도 증가하면서MetaTrader5 플랫폼 사용자들은 시장 분석에 보다 정교하고 진보된 수학적 방법을 적용할 수 있게 됐습니다. 이런 방법은 경제학, 계량 경제학 및 통계를 기반으로 하는데요. 어떤 것을 이용하든 확률 밀도 함수를 다루게 됩니다.

일반적인 분석 방법들은 데이터 분포의 정상성 또는 사용된 모델의 오차에 대한 정상성에 대한 가정을 기반으로 개발되었습니다. 어차피 분석 결과를 평가하려면 사용 모델에 포함된 다양한 컴포넌트의 분포를 알아야 하기도 하죠. 위의 두 경우 모두 우리는 미지의 확률 밀도를 측정하는 일종의 '도구'를 만들게 됩니다. 가장 이상적인 경우 하나의 보편적인 도구가 생성되죠.

이번 글에서는 미지의 확률 밀도 함수를 추정하는 보편적 알고리즘을 만들어 볼 겁니다. 처음에는 MQL5만 사용하고 모든 외부 도구는 제외시키려고 했는데요. 아이디어를 조금 수정했습니다. 확률 밀도 함수의 시각적 추정은 두 개의 독립된 부분으로 구성됩니다.

확률 밀도 추정과 차트 또는 다이어그램을 생성하는 시각화의 두 부분으로 나뉩니다. 물론 연산은 MQL5로 수행되었습니다만 시각화는 반드시 HTML 페이지를 따로 생성해 구현해야 합니다. 궁극적으로는 벡터 그래픽을 얻는 것이 목표죠.

물론 다른 방법을 이용한 시각화도 가능합니다. 앞으로MetaTrader5에도 그래픽 라이브러리를 포함한 여러 라이브러리가 탑재될 겁니다. 차후에MetaTrader5에 차트와 다이어그램에 관련된 고급 기능이 추가되면 그걸 이용해도 좋고요.

시퀀스의 확률 밀도 함수 추정에 이용할 수 있는 보편적 알고리즘 생성은 사실 불가능한 일임을 미리 알립니다. 특별히 특수한 알고리즘을 만들 건 아니지만 완전히 보편적이라고 할 수는 없습니다. 밀도 추정 과정의 최적성 판단 기준이 분포 별로 다르게 나타나기 때문이죠.

따라서 분포에 대한 정보가 있는 경우 이를 이용해 케이스 별 가장 적절한 방법을 찾을 수 있습니다. 하지만 본문에서는 추정된 밀도의 특성에 대해 아무것도 알려지지 않았다고 가정하겠습니다. 물론 측정의 질적 측면은 영향을 받겠지만 다양한 밀도를 추정할 수 있게 되기를 바라 봅시다.

시장 데이터 분석 시에는 비정상성을 띠는 시퀀스를 다루는 경우가 많으므로 짧거나 중간 길이를 갖는 시퀀스의 밀도 추정에 중점을 두겠습니다. 사용될 추정 방법을 선택하는 데에 아주 중요한 조건이죠.

히스토그램과 P-스플라인은 백만 개 이상의 값을 포함하는 굉장히 긴 시퀀스에 적용됩니다. 10~20개의 값을 갖는 시퀀스에 대한 히스토그램을 작성하려고 하면 문제가 발생하죠. 따라서 시퀀스 값의 개수를 10~10000개로 설정하기로 했습니다.


1. 확률 밀도 함수 추정 방법

다양한 방법이 알려져 있는데요. '확률 밀도 추정', '확률 밀도', '밀도 추정' 등으로 검색하면 인터넷에서 금방 찾을 수 있죠. 하지만 어떤 방법이 가장 좋다고 딱 잘라 말할 수가 없네요. 각각의 장점과 단점이 있거든요.

전통적으로는 히스토그램이 이용됩니다[1]. 히스토그램을 이용하면 정확도가 높은 확률 밀도 추정이 가능하지만 시퀀스가 긴 경우에만 적용할 수 있습니다. 짧은 시퀀스를 다수 개의 그룹을 나누는 것은 불가능하며 2~3개의 바만을 포함하는 히스토그램은 해당 시퀀스의 확률 밀도 분포를 제대로 나타낼 수 없기 때문이죠. 다른 방법을 찾아볼까요?

잘 알려진 또 다른 방법은 커널 밀도 추정입니다[2]. 참고 자료 [3]에 커널 평활법이 잘 설명되어 있죠. 단점이 있기는 하지만 커널 밀도 추정을 이용하기로 했습니다. 커널 밀도 추정에 대해서는 아래에서 간략히 설명하겠습니다.

기댓값 최대화 알고리즘이라는 굉장히 흥미로운 개념도 있는데요[4]. 해당 알고리즘을 이용하면 하나의 시퀀스를 정규 분포를 갖는 별도의 컴포넌트로 나눌 수 있습니다. 각 컴포넌트 매개 변수를 설정하여 구해지는 분포 곡선을 모두 더해서 밀도 추정을 할 수 있죠. 참고 자료 [5]에 설명이 되어 있습니다. 본문에서는 해당 알고리즘을 구현하거나 테스트하지는 않을 겁니다. 대신 많은 참고 자료를 링크해 놓았습니다.

그럼 커널 밀도 측정법에 대해 알아볼까요?


2. 확률 밀도 함수의 커널 밀도 측정

확률 밀도 함수의 커널 밀도 측정은 커널 평활법을 기반으로 합니다. 커널 평활법에 대해서는 참고 자료 [6], [7]에서 찾아볼 수 있습니다.

기본 개념은 꽤 이해하기 쉽습니다.MetaTrader5 사용자라면 MA 인디케이터를 잘 아실 겁니다. 가중치가 평활화되면서 시퀀스를 따라 윈도우가 이동한다고 설명할 수 있겠네요. 윈도우는 사각형일 수도, 지수로 나타날 수도, 혹은 또 다른 형태를 띨 수도 있습니다. 커널 평활화에서도 동일한 윈도우가 관찰 되죠([3] 참조). 하지만 이번에는 대칭을 이룹니다.

커널 평활화에서 가장 자주 쓰이는 윈도우의 예시는 [8[에서 찾아볼 수 있습니다. 커널 평활화에 0차 상관 계수가 이용되는 경우, 시퀀스의 가중치는 MA에서처럼 평활화됩니다. 필터링에서도 동일한 유형의 윈도우 함수가 적용되는데요. 동일한 절차이지만 조금 다르게 나타납니다. 증폭-주파, 위상-주파 특성이 이용되며 커널 창은 임펄스 필터의 특징을 보입니다.

한 가지 과정이 여러 방법으로 표현될 수 있는 것이죠. 모두 수학적 장치 덕분입니다. 하지만 그렇다 보니 헷갈리게 되는 경우도 있습니다.

커널 밀도 추정이 커널 평활화와 같은 원리를 이용하긴 하지만, 알고리즘은 조금 다르거든요.

밀도 추정에 이용되는 수학식을 살펴 볼게요.

읽는 법

  • x - 길이가 n인 시퀀스
  • K - 대칭 커널
  • h - 범위(평활화 매개 변수)

지금부터는 밀도 추정에 가우시안 필터만을 적용하겠습니다.

위에서 볼 수 있듯, X 지점의 밀도는 X 지점 값과 시퀀스 값의 차이인 사분위수에 대한 커널 값을 모두 합해 구합니다. 밀도 산출에 이용되는 X 지점은 시퀀스 값과 상이할 수 있습니다.

다음은 커널 밀도 추정 알고리즘 구현 단계입니다.

  1. 평균 및 인풋 시퀀스 표준 편차를 평가합니다.
  2. 인풋 시퀀스를 정규화합니다. 각 값에서 평균을 제외한 후 표준 편차 값으로 나눕니다. 그 결과 초기 시퀀스는 평균값 0과 표준 편차값 1을 갖습니다. 밀도 추정에 해당 정규화 과정은 필요 없지만 덕분에 X 스케일을 갖는 모든 시퀀스 값이 표준 편차 유닛으로 표시되므로 결과 차트를 통합할 수 있게 됩니다.
  3. 정상화된 시퀀스의 최고값과 최저값을 구합니다.
  4. 결과 차트에 나타날 포인트 개수와 일치하는 값을 갖는 배열 두 개를 생성합니다. 예를 들어, 차트에 200 포인트가 이용될 경우 두 배열 모두 그 값이 200이 되어야 합니다.
  5. 둘 중 한 배열은 결과 저장에 쓰일 겁니다. 다른 배열은 밀도 추정이 실행되는 포인트 값 형성에 이용됩니다. 최고값과 최저값 사이에 동일한 간격으로 배치된 200개 값을 설정하고 배열에 저장합니다.
  6. 앞서 확인한 수식을 이용해 200개 테스트 포인트에 대한 밀도 추정을 실행합니다. 4번에서 생성한 배열에 결과를 저장합니다.

다음은 해당 알고리즘의 소프트웨어 구현입니다.

//+------------------------------------------------------------------+
//|                                                        CDens.mqh |
//|                                                    2012, victorg |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2012, victorg"
#property link      "https://www.mql5.com"

#include <Object.mqh>
//+------------------------------------------------------------------+
//| Class Kernel Density Estimation                                  |
//+------------------------------------------------------------------+
class CDens:public CObject
  {
public:
   double            X[];              // Data
   int               N;                // Input data length (N >= 8)
   double            T[];              // Test points for pdf estimating
   double            Y[];              // Estimated density (pdf)
   int               Np;               // Number of test points (Npoint>=10, default 200)
   double            Mean;             // Mean (average)
   double            Var;              // Variance
   double            StDev;            // Standard deviation
   double            H;                // Bandwidth
public:
   void              CDens(void);
   int               Density(double &x[],double hh);
   void              NTpoints(int n);
private:
   void              kdens(double h);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
void CDens::CDens(void)
  {
   NTpoints(200);            // Default number of test points
  }
//+------------------------------------------------------------------+
//| Setting number of test points                                    |
//+------------------------------------------------------------------+
void CDens::NTpoints(int n)
  {
   if(n<10)n=10;
   Np=n;                    // Number of test points
   ArrayResize(T,Np);        // Array for test points
   ArrayResize(Y,Np);        // Array for result (pdf)
  }
//+------------------------------------------------------------------+
//| Density                                                          |
//+------------------------------------------------------------------+
int CDens::Density(double &x[],double hh)
  {
   int i;
   double a,b,min,max,h;

   N=ArraySize(x);                           // Input data length
   if(N<8)                                  // If N is too small
     {
      Print(__FUNCTION__+": Error! Not enough data length!");
      return(-1);
     }
   ArrayResize(X,N);                         // Array for input data
   ArrayCopy(X,x);                           // Copy input data
   ArraySort(X);
   Mean=0;
   for(i=0;i<N;i++)Mean=Mean+(X[i]-Mean)/(i+1.0); // Mean (average)
   Var=0;
   for(i=0;i<N;i++)
     {
      a=X[i]-Mean;
      X[i]=a;
      Var+=a*a;
     }
   Var/=N;                                  // Variance
   if(Var<1.e-250)                           // Variance is too small
     {
      Print(__FUNCTION__+": Error! The variance is too small or zero!");
      return(-1);
     }
   StDev=MathSqrt(Var);                      // Standard deviation
   for(i=0;i<N;i++)X[i]=X[i]/StDev;          // Data normalization (mean=0,stdev=1)
   min=X[ArrayMinimum(X)];
   max=X[ArrayMaximum(X)];
   b=(max-min)/(Np-1.0);
   for(i=0;i<Np;i++)T[i]=min+b*(double)i;    // Create test points
//-------------------------------- Bandwidth selection
   h=hh;
   if(h<0.001)h=0.001;
   H=h;
//-------------------------------- Density estimation
   kdens(h);

   return(0);
  }
//+------------------------------------------------------------------+
//| Gaussian kernel density estimation                               |
//+------------------------------------------------------------------+
void CDens::kdens(double h)
  {
   int i,j;
   double a,b,c;

   c=MathSqrt(M_PI+M_PI)*N*h;
   for(i=0;i<Np;i++)
     {
      a=0;
      for(j=0;j<N;j++)
        {
         b=(T[i]-X[j])/h;
         a+=MathExp(-b*b*0.5);
        }
      Y[i]=a/c;                 // pdf
     }
  }
//--------------------------------------------------------------------

NTpoints() 메소드는 밀도 추정이 이루어지는 동일한 간격으로 배치된 포인트를 필요한 수만큼 설정해 줍니다. 해당 메소드는 반드시 Density() 메소드에 앞서 호출되야 합니다. 인풋 데이터와 범위 값(평활화 매개 변수)가 포함된 배열 링크는 Density() 메소드 호출 시 매개 변수로 전달됩니다.

성공적으로 완료된 경우 Density() 메소드는 0 값을 반환하며 테스트 포인트 값과 추정 결과는 각각 클래스 내 T[]와 Y[] 배열에 저장됩니다.

배열 크기는 NTpoints()에 액세스하여 설정되며 기본값은 200입니다.

다음의 예제 스크립트는 CDens 클래스 사용법을 설명합니다.

#include "CDens.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   int i;
   int ndata=1000;          // Input data length
   int npoint=300;          // Number of test points
   double X[];              // Array for data 
   double T[];              // Test points for pdf estimating
   double Y[];              // Array for result

   ArrayResize(X,ndata);
   ArrayResize(T,npoint);
   ArrayResize(Y,npoint);
   for(i=0;i<ndata;i++)X[i]=MathRand();// Create input data
   CDens *kd=new CDens;
   kd.NTpoints(npoint);    // Setting number of test points
   kd.Density(X,0.22);     // Density estimation with h=0.22
   ArrayCopy(T,kd.T);       // Copy test points
   ArrayCopy(Y,kd.Y);       // Copy result (pdf)
   delete(kd);

// Result: T[]-test points, Y[]-density estimation
  }
//--------------------------------------------------------------------

테스트 포인트와 추정 결과를 저장할 배열 또한 만들어 볼 겁니다. 그리고 나면 예로 들기 위해 X[] 배열을 임의의 값으로 채울 거예요. CDens 클래스 복사는 모든 데이터가 준비된 후에 이루어집니다.

NTpoints() 메소드를 호출해 필요한 테스트 포인트 개수가 설정되죠(우리의 경우 npoint=300). 포인트 기본값을 이용하는 경우 NTpoints() 메소드를 따로 호출하지 않아도 됩니다.

Density() 메소드 호출 후 미리 설정된 배열에 연산 결과 값이 복사됩니다. 이전에 생성된 CDens 클래스 사본은 삭제되죠. 해당 예제에서는 CDens 클래스와의 인터랙션만 다루었습니다. 획득 결과(T[] 및 Y[] 배열)에 대한 추가적인 분석은 실행되지 않았죠.

해당 결과로 차트를 생성하려면 T[] 배열 값을 X 스케일에, Y[] 배열 값을 Y 스케일에 위치시키면 됩니다.


3. 최적 범위 선택

그림 1은 정규 분포를 따르며 다양한 h 범위 값을 갖는 시퀀스에 대한 밀도 추정 차트를 나타냅니다.

위에서 설명한 CDens 클래스가 추정에 이용됐습니다. 차트는 HTML 형식을 갖습니다. 차트 작성법에 대해서는 본문 하단에서 다루게 될 겁니다. HTML로 차트 및 다이어그램을 작성하는 방법은 참고 자료 [9]를 참고하세요.

 그림 1. 다양한 h 범위 값에 대한 밀도 추정

그림 1. 다양한 h 범위 값에 대한 밀도 추정

그림 1은 정규 분포 밀도 곡선(가우스 분포)와 세 가지 밀도 추정을 보여줍니다. h=0.22인 경우에 가장 적절한 추정 결과가 획득됨을 알 수 있습니다. 나머지 두 경우는 '과대평활화'와 '과소평활화'에 해당하는데요.

커널 밀도 측정 시 적절한 h 범위 설정의 중요성을 알 수 있습니다. h 값이 적절하지 않은 경우 추정은 정규 분포 밀도에서 크게 벗어납니다.

최적 h 범위 선택에 대한 많은 연구가 진행되고 있는데요. 간단하고 효과적인 실버만(Silverman)의 방식이 주로 사용됩니다([10]).

이 경우 A는 시퀀스의 표준 편차 최저값이자 사분범위를 1.34로 나눈 값입니다.

인풋 시퀀스의 표준 편차 값이 1에 해당하므로 다음의 프래그먼트로 구현 가능합니다.

  ArraySort(X);
  i=(int)((N-1.0)/4.0+0.5);  
  a=(X[N-1-i]-X[i])/1.34;      // IQR/1.34
  a=MathMin(a,1.0);
  h=0.9*a/MathPow(N,0.2);       // Silverman's rule of thumb

해당 추정은 확률 밀도 함수가 정규 분포를 따르는 시퀀스에 적합하죠.

하지만 대부분의 경우 사분범위를 이용해 h 값을 낮출 수 있습니다. 따라서 해당 방법이 다양하게 사용되죠. 초기 기본 h 값 추정에 사용하면 됩니다. 연산도 전혀 복잡하지 않거든요.

점근적이고 경험적인 h 범위 값 추정 방법 이외에도 인풋 시퀀스 자체에 대한 분석을 기반으로 하는 방법도 있습니다. 이 경우 h의 최적 값은 미지의 밀도 추정을 통해 결정됩니다. 이 같은 원리를 갖는 여러 방식의 효율성은 참고 자료 [11], [12]를 참조하세요.

다양한 테스트 결과에 따르면 Sheather-Jones 플러그인(SJPI)가 가장 효과적인 범위 추정 방법이라고 합니다. 이 방법을 이용하도록 하죠. SJPI의 수학적 측면에 대해서는 간략하게 후술하겠습니다. 해당 플러그인의 수학적 근거는 참고 자료 [13]에서 찾아볼 수 있습니다.

무계 지지 집합을 갖는 커널인 가우시안 필터를 이용하겠습니다. 위의 수식에서 보셨듯이 길이가 N인 시퀀스의 M 포인트에 대한 밀도를 추정하려면 가우시안 필터와 직접 연산을 이용해 O(M*N)개의 오퍼레이션을 실행해야 합니다. 시퀀스의 모든 지점에 대한 추정을 해야 하는 경우 해당 값은 최대 O(N*N)개의 오퍼레이션이 되며 시퀀스 길이의 제곱값에 비례해 연산 시간이 증가합니다.

리소스가 많이 필요한 점이 커널 평활법의 최대 단점 중 하나인데요. SJPI도 크게 다르지 않습니다. SJPI 구현 시에는 인풋 시퀀스 전체를 따라 두 개의 밀도 편차의 합을 두 번 구해야 합니다.

획득 결과를 이용해 추정을 반복적으로 계산하죠. 추정 값은 0이 되어야 합니다. 인수는 뉴턴 방법(뉴턴-랜슨법, [14])을 이용해 찾을 수 있는데요. 이때 함수 값은 0입니다. 직접 연산을 하는 경우 SJPI를 이용한 최적 범위 산출은 밀도 추정 연산보다 훨씬 오래 걸릴 수 있습니다.

커널 평활화와 커널 밀도 추정 시 연산을 가속화하는 여러 방법이 있는데요. 우리가 이용하는 가우시안 필터의 경우 4보다 큰 인수의 값은 무시할 수 있다고 봅니다. 따라서 인수 값이 4보다 큰 경우 아예 커널 값을 계산할 필요가 없는 것이죠. 덕분에 요구되는 연산의 일부를 줄일 수 있습니다. 가우시안 필터가 적용된 추정과 적용되지 않은 추정의 차이는 차트로는 판단할 수 없습니다.

연산을 가속화하는 또 하나의 간단한 방법은 추정이 실행되는 지점의 개수를 줄이는 건데요. 위에서 말했다시피 M이 N보다 작은 경우 요구되는 오퍼레이션의 개수는 O(N*N)에서 O(M*N)으로 감소합니다. 예를 들어 N=100000인 긴 시퀀스가 있어도 추정 지점의 개수는 M=200이 되죠. 따라서 연산에 소요되는 시간을 크게 절약할 수 있습니다.

혹은 좀 더 복잡한 방법을 이용할 수도 있는데요. 푸리에 변환 또는 웨이블릿 변환을 통한 추정을 이용해도 연산 과정을 축소할 수 있죠. 굉장히 긴 시퀀스의 경우 인풋 시퀀스 크기 축소(예: 데이터 구간화 [15])를 기반으로 하는 방법을 적용할 수 있습니다.

가우시안 필터를 사용하는 경우 위의 방법과 더불어 고속 가우스 변환[16]을 이용해 연산을 가속화할 수 있습니다. SJPI 범위 측정에 가우스 변환[17]을 기반으로 한 알고리즘을 이용해 보겠습니다. 위의 링크는 알고리즘에 대한 설명과 알고리즘 구현 프로그램 코드를 포함하고 있습니다.


4. Sheather-Jones 플러그인(SJPI)

SJPI 알고리즘 구현의 수학적 근거는 참고 자료 [17]에서 찾아볼 수 있습니다. 필요한 경우 [17]에 포함된 자료들을 참조해 보세요.

[17]의 자료를 기반으로 CSJPlugin 클래스를 생성했습니다. 해당 클래스는 최적 h 범위 연산에 이용되며 단 하나의 전역 메소드, 더블형 CSJPlugin::SelectH(double &px[],double h,double stdev=1)만을 포함합니다.

위의 메소드를 호출하면 다음 인자들이 전달됩니다.

  • double &px[] – 초기 시퀀스 배열에 대한 링크
  • double h는 h 값 초기 범위이며 뉴턴 방법을 적용한 수식 연산 시 탐색이 적용됩니다. 해당 값은 탐색된 값과 최대한 가까운 것이 좋습니다. 시간을 절약하고 뉴턴 방법이 신뢰도를 잃는 경우의 수를 최소화할 수 있거든요. 실버만의 방식을 이용해 구한 값을 초기 h 값으로 사용해도 됩니다.
  • double stdev=1 – 초기 시퀀스 표준 편차 값 기본값은 1입니다. 이 경우 위에서 설명한 CDens 클래스와 함께 사용되도록 고안된 클래스이므로 기본값을 변경할 필요가 없습니다. 초기 시퀀스는 이미 정상화되었으며 표준 편차 값은 1이죠.

SelectH() 메소드는 호출이 완료되면 h 값 최적 범위를 반환합니다. 호출 실패 시 초기 h 값이 매개 변수로 반환됩니다. CSJPlugin 클래스 소스 코드는 CSJPlugin.mqh 파일에 포함되어 있습니다.

구현과 관련해 몇 가지 짚고 넘어갈 내용이 있습니다.

소스 시퀀스는 [0,1] 인터벌로 변환되며 초기 h 범위 값은 초기 시퀀스 변형 스케일에 비례해 정규화됩니다. 연산에서 산출된 최적 h 값에 역정규화가 적용되빈다.

eps=1e-4 – 밀도 추정 및 편차의 연산 정확도 P_UL=500 – CSJPlugin 클래스 컨스트럭터로 설정된 알고리즘 최대 내부 반복 가능 횟수 추가 정보는 참고 자료 [17]에 있습니다.

IMAX=20 – 뉴턴 방법 최대 반복 가능 회수 PREC=1e-4 – SelectH() 메소드로 설정된 뉴턴 방법이 적용된 수식 연산의 정확도

전통적인 뉴턴 방법은 각 반복이 실행될 때마다 동일한 포인트에서 타겟 함수와 그 편차를 계산합니다. 이 경우에는 편차 산출이 인수에 증분을 더해 계산된 추정으로 대체되었죠.

그림 2는 두 방법을 이용한 최적 범위 추정 예시입니다.

그림 2. h 범위 최적 값 추정 비교

그림 2. h 범위 최적 값 추정 비교

그림 2는 임의의 시퀀스에 대한 두 개의 추정을 나타내며 그 실제 밀도는 붉은 차트(Pattern)에 나타나 있습니다.

측정 대상 시퀀스는 총 10000개의 엘리먼트를 포함합니다. 실버만의 방식으로 산출한 h 범위 값은 0.14이며 SJPI로 산출한 값은 0.07입니다.

그림 2의 두 h 값에 대한 커널 밀도 추정 결과를 통해 SJPI를 적용했을 때 보다 정확한 h 값 측정이 가능함을 알 수 있습니다. h=0.07일 때 봉우리가 훨씬 날카로우며 경사면에서도 분산이 거의 증가하지 않죠.

대부분의 경우 SJPI를 이용하면 꽤 성공적인 h 범위 추정이 가능한 거네요. CSJPlugin 클래스 생성에 고속 알고리즘[17]이 사용되기는 했지만 긴 시퀀스에 대한 h 값 측정은 여전히 시간이 매우 오래 소요될 수 있습니다.

또 다른 단점 하나는 10~30개 값만을 포함하는 짧은 시퀀스에 대한 h 값이 과대평가되는 경향이 있다는 겁니다. SJPI를 이용한 측정은 실버만 방식이 적용된 측정을 초과하는 h 값을 가질 수 있습니다.

이런 단점을 보안하기 위해 추가 구현 단계에서 다음 법칙을 적용합니다.

  • 실버만의 방식이 h 값 추정에 기본으로 적용되며 별도의 명령을 통해 SJPI를 적용할 수 있습니다.
  • SJPI 적용 시 두 방법으로 산출한 값들 중 가장 낮은 값이 최종 h 값으로 설정됩니다.


5. 경계 효과

밀도 추정에 최적 범위를 이용하고 싶은 마음에 좀 복잡한 CSJPlugin 클래스를 생성하게 됐습니다. 범위 설정이나 커널 평활법 외에도 한 가지 살펴볼 것이 있는데요. 경계 효과라고 불리는 겁니다.

이는 간단하게 설명 가능합니다. [-1,1] 인터벌에 설정된 커널 함수를 이용해 나타낼 겁니다. 해당 커널은 유계 집합을 갖습니다. 특정 범위 밖에서는 그 값이 0이 되죠. 존재하지 않게 되는 겁니다.

그림 3. 범위 경계에서의 커널 함수 축소

그림 3. 범위 경계에서의 커널 함수 축소

그림 3에서 확인 가능하듯 첫 번째 곡선의 경우 코어가 중심을 기준으로 [-1,1] 인터벌에 위치한 데이터를 모두 커버합니다. 하지만 곡선이 오른쪽으로 이동하면서 커널 함수에 이용할 수 있는 데이터가 부족한 경우가 발생하죠.

첫 번째 경우에 비해서 훨씬 적은 데이터를 포함하는 건 당연하고요. 최악의 경우는 중심이 데이터 시퀀스 경계에 위치할 때 발생합니다. 이 경우 커널 함수가 커버하는 데이터는 50%로 줄어들죠. 밀도 추정에 이용되는 데이터 개수가 감소하면 추정 결과에 상당한 변화가 발생하며 경계에 가까운 지점의 분산이 증가합니다.

그림 3은 유계 집합을 갖는 커널 함수의 범위 경계 축소 예시입니다. 무계 지지 집합에 대한 가우시안 필터링이 커널 밀도 측정 구현에 이용되었습니다. 이론상 이런 커널은 항상 발생할 수밖에 없죠. 하지만 인수가 큰 경우 커널 값이 거의 0에 근접한다는 점을 고려하면 유계 집합을 갖는 커널이나 경계 효과나 비슷해 보입니다.

그림1과 2의 경우 해당 사항은 밀도 추정 결과에 영향을 주지 않습니다. 두 경우 모두 확률 밀도 함수가 경계에서 거의 0에 근접하는 분배에 대한 측정을 나타내기 떄문입니다.

양의 정수 X=1,2,3,4,5,6,…n으로 이루어진 시퀀스를 만들어 커널 함수의 범위 경계 축소를 살펴볼게요. 해당 시퀀스는 균등 분포를 따릅니다. 시퀀스의 밀도 추정이 0이 아닌 수준에서 수평선을 형성한다는 의미이죠.

그림. 4. 균등 분포를 따르는 시퀀스에 대한 밀도 추정

그림. 4. 균등 분포를 따르는 시퀀스에 대한 밀도 추정

예상한 바와 같이 범위 경계에서의 밀도 추정에 상당한 변화가 발생합니다. 이런 변화를 축소할 수 있는 몇 가지 방법이 있는데요. 크게 다음의 네 가지 그룹으로 나눌 수 있습니다.

  • 반영
  • 데이터 변형
  • 테스트 데이터(pseudodata)
  • 경계 커널

반영은 인풋 시퀀스에 데이터를 추가해 의도적으로 시퀀스를 증가시킵니다. 일종의 시퀀스 범위 경계가 거울에 비친 듯한 모습을 만드는 거죠. 밀도 추정은 초기 시퀀스와 동일한 지점에서 실행되지만 추가된 데이터 또한 추정에 이용됩니다.

데이터 변형은 범위 경계 근처의 시퀀스 변형에 중점을 둡니다. 로그 변환 등을 이용해 밀도 추정 중 범위 경계에 가까워지면 데이터 스케일을 왜곡시키는 것이죠.

테스트 데이터는 초기 시퀀스를 의도적으로 연장(확대)하는 데에 쓰입니다. 초기 시퀀스 값을 이용해 산출된 데이터를 이용하죠. 해당 데이터는 경계에서 일어나는 변화를 최적의 방법으로 보완하는 데에 사용됩니다.

경계 커널에 대한 자료는 쉽게 찾아볼 수 있습니다. 위의 방법을 이용하면 경계에 가까워지면 커널의 형태가 어느 정도 변하게 됩니다. 이는 경계에서 발생하는 측정의 변화를 상쇄하기 위함이죠.

범위 경계에서 발생하는 왜곡의 상쇄에 대한 정보는 참고 자료 [18]에서 찾아볼 수 있습니다.

약간의 실험 결과 반영을 이용하는 것이 좋겠다고 판단했습니다. 밀도 측정이 음의 값을 가지는 경우의 영향을 받지 않기 때문입니다. 복잡한 연산이 필요하지도 않고요. 다만 시퀀스 길이가 늘어났으므로 전체 연산 횟수는 증가합니다.

여기에는 두 가지 구현 방법이 있습니다. 첫 번째는 필요한 데이터를 이용해 초기 시퀀스를 보안하고 그 크기를 3배 증가시키는 겁니다. 이렇게 하면 CDens 클래스와 동일한 방법으로 추정을 할 수 있습니다. 두 번째 방법을 이용하면 인풋 데이터 배열을 확장하지 않아도 됩니다. 해당 배열에서 데이터를 중복 선택하는 방법이 있거든요. 두 번째 방법을 이용해 구현하겠습니다.

위의 CDens 클래스에서 밀도 추정은 void 타입 kdens(double h) 함수를 이용해 구현되었습니다. 여기에 경계 왜곡 보정을 추가하겠습니다.

그 결과 해당 함수는 다음과 같아 보일 겁니다.

//+------------------------------------------------------------------+
//| Kernel density estimation with reflection of data                |
//+------------------------------------------------------------------+
 void kdens(double h)
  {
  int i,j;
  double a,b,c,d,e,g,s,hh;
  
  hh=h/MathSqrt(0.5);
  s=sqrt(M_PI+M_PI)*N*h;
  c=(X[0]+X[0])/hh;
  d=(X[N-1]+X[N-1])/hh;
  for(i=0;i<Np;i++)
    {
    e=T[i]/hh; a=0;
    g=e-X[0]/hh;   if(g>-3&&g<3)a+=MathExp(-g*g);
    g=e-X[N-1]/hh; if(g>-3&&g<3)a+=MathExp(-g*g);
    for(j=1;j<N-1;j++)
      {
      b=X[j]/hh;
      g=e-b;   if(g>-3&&g<3)a+=MathExp(-g*g);
      g=d-e-b; if(g>-3&&g<3)a+=MathExp(-g*g);
      g=c-e-b; if(g>-3&&g<3)a+=MathExp(-g*g);
      }
    Y[i]=a/s;                                        // pdf
    }
  }

함수 호출 시 X[] 배열의 소스 데이터가 이미 정렬된 상태라고 가정하고 구현되는 함수입니다. 범위 극단 값에 해당하는 두 개의 시퀀스 엘리먼트를 제외시킬 수 있죠. 해당 함수 구현 시 가능한 모든 경우에 수학적 연산은 루프 외부에 위치하도록 했습니다. 그 결과 함수는 사용된 알고리즘의 영향을 덜 받게 됩니다.

큰 인수 값에 대한 커널 값을 계산하지 않으면 연산 횟수를 줄일 수 있다고 말씀드렸는데요. 위의 함수가 바로 그런 방식으로 구현된 경우입니다. 이 경우 밀도 차트 생성 후에 발생하는 변화는 인식 불가능합니다.

수정된 kdens() 함수를 이용하면 그림 4의 밀도 추정이 직선을 이루게 되며 경계의 빈 부분도 완전히 사라집니다. 하지만 위 같은 이상적인 조정은 경계 근처에 위치하는 분포가 제로 그라디언트를 따르는 경우, 즉 수평선으로 나타나는 경우에만 가능합니다.

분포에 대한 밀도 추정이 경계 근처에서 급증하거나 급감하는 경우 해당 방식은 경계 효과를 완전히 조정하지 못하죠. 다음 그림을 살펴보죠.

그림 5. 확률 밀도의 계단식 변화

그림 5. 확률 밀도 함수의 계단식 변화

그림 6. 확률 밀도의 직선적 증가

그림 6. 확률 밀도 함수의 직선적 증가

그림 5와 6은 기본 kdens() 함수(빨강)를 이용해 얻은 밀도 측정과 반영 시 발생하는 변화가 적용된 추정(파랑)을 나타냅니다. 그림 5에서 경계 효과가 완전히 조정된 반면 그림 6에서는 경계 근처의 변화가 완전히 제거되지 않았습니다. 경계 근처에서 측정된 밀도가 급증하거나 급감하는 경우 보다 부드럽게 나타나는 정도네요.

경계 효과 조정에 이용된 반영은 최적의 방법도, 최악의 방법도 아닙니다. 전체 경계 효과를 제거하는 건 아니지만 꽤 안정적이고 구현하기도 쉽죠. 논리적이고 예상 가능한 결과를 얻게 됩니다.


6. 최종 구현 CKDensity 클래스

앞서 생성한 CDens 클래스에 h 범위 자동 설정 기능 및 경계 효과를 추가하겠습니다.

다음은 수정된 클래스의 소스 코드입니다.

//+------------------------------------------------------------------+
//|                                                    CKDensity.mqh |
//|                                                    2012, victorg |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2012, victorg"
#property link      "https://www.mql5.com"

#include <Object.mqh>
#include "CSJPlugin.mqh"
//+------------------------------------------------------------------+
//| Class Kernel Density Estimation                                  |
//+------------------------------------------------------------------+
class CKDensity:public CObject
  {
public:
   double            X[];            // Data
   int               N;              // Input data length (N >= 8)
   double            T[];            // Test points for pdf estimating
   double            Y[];            // Estimated density (pdf)
   int               Np;             // Number of test points (Npoint>=10, default 200)
   double            Mean;           // Mean (average)
   double            Var;            // Variance
   double            StDev;          // Standard deviation
   double            H;              // Bandwidth
   int               Pflag;          // SJ plug-in bandwidth selection flag
public:
   void              CKDensity(void);
   int               Density(double &x[],double hh=-1);
   void              NTpoints(int n);
   void    PluginMode(int m) {if(m==1)Pflag=1; else Pflag=0;}
private:
   void              kdens(double h);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
void CKDensity::CKDensity(void)
  {
   NTpoints(200);                            // Default number of test points
   Pflag=0;                                  // Not SJ plug-in
  }
//+------------------------------------------------------------------+
//| Setting number of test points                                    |
//+------------------------------------------------------------------+
void CKDensity::NTpoints(int n)
  {
   if(n<10)n=10;
   Np=n;                                    // Number of test points
   ArrayResize(T,Np);                        // Array for test points
   ArrayResize(Y,Np);                        // Array for result (pdf)
  }
//+------------------------------------------------------------------+
//| Bandwidth selection and kernel density estimation                |
//+------------------------------------------------------------------+
int CKDensity::Density(double &x[],double hh=-1)
  {
   int i;
   double a,b,h;

   N=ArraySize(x);                           // Input data length
   if(N<8)                                   // If N is too small
     {
      Print(__FUNCTION__+": Error! Not enough data length!");
      return(-1);
     }
   ArrayResize(X,N);                         // Array for input data
   ArrayCopy(X,x);                           // Copy input data
   ArraySort(X);                             // Sort input data
   Mean=0;
   for(i=0;i<N;i++)Mean=Mean+(X[i]-Mean)/(i+1.0); // Mean (average)
   Var=0;
   for(i=0;i<N;i++)
     {
      a=X[i]-Mean;
      X[i]=a;
      Var+=a*a;
     }
   Var/=N;                                  // Variance
   if(Var<1.e-250)                           // Variance is too small
     {
      Print(__FUNCTION__+": Error! The variance is too small or zero!");
      return(-1);
     }
   StDev=MathSqrt(Var);                      // Standard deviation
   for(i=0;i<N;i++)X[i]=X[i]/StDev;          // Data normalization (mean=0,stdev=1)
   a=X[0];
   b=(X[N-1]-a)/(Np-1.0);
   for(i=0;i<Np;i++)T[i]=a+b*(double)i;      // Create test points

//-------------------------------- Bandwidth selection
   if(hh<0)
     {
      i=(int)((N-1.0)/4.0+0.5);
      a=(X[N-1-i]-X[i])/1.34;               // IQR/1.34
      a=MathMin(a,1.0);
      h=0.9*a/MathPow(N,0.2);                // Silverman's rule of thumb
      if(Pflag==1)
        {
         CSJPlugin *plug=new CSJPlugin();
         a=plug.SelectH(X,h);              // SJ Plug-in
         delete plug;
         h=MathMin(a,h);
        }
     }
   else {h=hh; if(h<0.005)h=0.005;}          // Manual select
   H=h;

//-------------------------------- Density estimation
   kdens(h);

   return(0);
  }
//+------------------------------------------------------------------+
//| Kernel density estimation with reflection of data                |
//+------------------------------------------------------------------+
void CKDensity::kdens(double h)
  {
   int i,j;
   double a,b,c,d,e,g,s,hh;

   hh=h/MathSqrt(0.5);
   s=sqrt(M_PI+M_PI)*N*h;
   c=(X[0]+X[0])/hh;
   d=(X[N-1]+X[N-1])/hh;
   for(i=0;i<Np;i++)
     {
      e=T[i]/hh; a=0;
      g=e-X[0]/hh;   if(g>-3&&g<3)a+=MathExp(-g*g);
      g=e-X[N-1]/hh; if(g>-3&&g<3)a+=MathExp(-g*g);
      for(j=1;j<N-1;j++)
        {
         b=X[j]/hh;
         g=e-b;   if(g>-3&&g<3)a+=MathExp(-g*g);
         g=d-e-b; if(g>-3&&g<3)a+=MathExp(-g*g);
         g=c-e-b; if(g>-3&&g<3)a+=MathExp(-g*g);
        }
      Y[i]=a/s;                               // pdf
     }
  }

Density(double &x[],double hh=-1) 메소드가 기본이 됩니다. 첫 번째 인수는 분석된 인풋 시퀀스를 포함하는 x[] 배열에 대한 링크가 됩니다. 인풋 시퀀스는 최소 8개 엘리먼트를 포함해야 합니다. 두 번째(선택적)) 인수는 h 범위 값의 강제 설정에 이용됩니다.

h 범위는 양의 값을 갖습니다. 설정된 h 값은 항상 아래에서 0.005로 제한됩니다. 해당 매개 변수가 음의 값을 갖는 경우 범위 값은 자동으로 선택됩니다. 성공적으로 완료된 경우 Density() 메소드는 0값을 반환하며 그렇지 않은 경우 -1이 반환됩니다.

Density() 메소드가 성공적으로 호출되면 T[] 배열에 밀도 추정이 실행된 테스트 포인트 값이 저장됩니다. Y[] 배열에는 해당 추정 값이 포함되죠. X[] 배열은 정규화되고 정렬된 인풋 시퀀스를 포함합니다. 해당 시퀀스의 평균값은 0이며 표준 편차값은 1입니다.

다음은 클래스에 속하는 변수 값입니다.

  • N – 인풋 시퀀스 길이
  • Np –밀도 추정이 실행되는 테스트 포인트 개수
  • Mean – 인풋 시퀀스 평균
  • Var – 인풋 시퀀스 편차
  • StDev – 인풋 시퀀스 표준 편차
  • H – 범위 값(평활화 변수)
  • Pflag – h 범위 최적 값 판단에 적용되는 SJPI 플래그

NTpoints(int n) 메소드는 밀도 추정이 적용되는 테스트 포인트 개수를 설정합니다. 테스트 포인트 개수 설정은 반드시 Density() 기본 메소드 호출 이전에 이루어져야 합니다. NTpoints(int n) 메소드가 사용되지 않는 경우 기본값은 200으로 설정됩니다.

PluginMode(int m) 메소드는 최적 범위 산출에 대한 SJPI 적용을 허용 또는 금지합니다. 매개 변수가 1인 경우 h 범위 특정에 SJPI가 사용됩니다. 매개 변수가 1이 아닌 경우 SJPI는 사용되지 않습니다. PluginMode(int m) 메소드가 호출되지 않으면 SJPI는 자동으로 사용되지 않습니다.


7. 차트 생성

참고 자료 [9]를 참고하세요. 하이차트[19] 자바스크립트 라이브러리가 포함된 HTML 페이지 및 전체 함수에 대한 호출 방법이 설명되어 있습니다. MQL 프로그램은 차트에 표시될 데이터가 포함된 텍스트 파일을 형성하며 해당 파일은 위의 HTML 페이지로 연결됩니다.

이 경우 차트 구조를 수정하려면 구현된 자바스크립트 코드를 수정해 HTML 페이지를 편집해야 합니다. 이를 피하기 위해 MQL 프로그램에서 하이차트 라이브러리의 자바크립트 메소드를 호출할 수 있는 인터페이스가 마련되었습니다.

인터페이스를 개발하면서 하이차트 라이브러리 전체에 대한 액세스를 목표로 하지는 않았습니다. 따라서 기존의 HTML 파일에 대한 수정이 필요 없는 하이차트 기능만이 구현되어 있습니다.

아래는 CLinDraw 클래스의 소스 코드입니다. 이 클래스는 하이차트 라이브러리 메소드와의 연결을 담당합니다. 완전히 구현된 소프트웨어로 생각해서는 안됩니다. 그래픽 자바스크립트 라이브러리가 있는 인터페이스 생성 예제일 뿐이죠.

//+------------------------------------------------------------------+
//|                                                     CLinDraw.mqh |
//|                                                    2012, victorg |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2012, victorg"
#property link      "https://www.mql5.com"

#include <Object.mqh>
#import "shell32.dll"
int ShellExecuteW(int hwnd,string lpOperation,string lpFile,string lpParameters,
                  string lpDirectory,int nShowCmd);
#import
#import "kernel32.dll"
int DeleteFileW(string lpFileName);
int MoveFileW(string lpExistingFileName,string lpNewFileName);
#import
//+------------------------------------------------------------------+
//| type = "line","spline","scatter"                                 |
//| col  = "r,g,b,y"                                                 |
//| Leg  = "true","false"                                            |
//| Reference: http://www.highcharts.com/                            |
//+------------------------------------------------------------------+
class CLinDraw:public CObject
  {
protected:
   int               Fhandle;           // File handle
   int               Num;               // Internal number of chart line
   string            Tit;               // Title chart
   string            SubTit;            // Subtitle chart
   string            Leg;               // Legend enable/disable
   string            Ytit;              // Title Y scale
   string            Xtit;              // Title X scale
   string            Fnam;              // File name

public:
   void              CLinDraw(void);
   void    Title(string s)      { Tit=s; }
   void    SubTitle(string s)   { SubTit=s; }
   void    Legend(string s)     { Leg=s; }
   void    YTitle(string s)     { Ytit=s; }
   void    XTitle(string s)     { Xtit=s; }
   int               AddGraph(double &y[],string type,string name,int w=0,string col="");
   int               AddGraph(double &x[],double &y[],string type,string name,int w=0,string col="");
   int               AddGraph(double &x[],double y,string type,string name,int w=0,string col="");
   int               LDraw(int ashow=1);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
void CLinDraw::CLinDraw(void)
  {
   Num=0;
   Tit="";
   SubTit="";
   Leg="true";
   Ytit="";
   Xtit="";
   Fnam="CLinDraw.txt";
   Fhandle=FileOpen(Fnam,FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(Fhandle<0)
     {
      Print(__FUNCTION__,": Error! FileOpen() error.");
      return;
     }
   FileSeek(Fhandle,0,SEEK_SET);               // if file exists
  }
//+------------------------------------------------------------------+
//| AddGraph                                                         |
//+------------------------------------------------------------------+
int CLinDraw::AddGraph(double &y[],string type,string name,int w=0,string col="")
  {
   int i,k,n;
   string str;

   if(Fhandle<0)return(-1);
   if(Num==0)
     {
      str="$(document).ready(function(){\n"
          "var lp=new Highcharts.Chart({\n"
          "chart:{renderTo:'lplot'},\n"
          "title:{text:'"+Tit+"'},\n"
          "subtitle:{text:'"+SubTit+"'},\n"
          "legend:{enabled:"+Leg+"},\n"
          "yAxis:{title:{text:'"+Ytit+"'}},\n"
          "xAxis:{title:{text:'"+Xtit+"'},showLastLabel:true},\n"
          "series:[\n";
      FileWriteString(Fhandle,str);
     }
   n=ArraySize(y);
   str="{type:'"+type+"',name:'"+name+"',";
   if(col!="")str+="color:'rgba("+col+")',";
   if(w!=0)str+="lineWidth:"+(string)w+",";
   str+="data:[";
   k=0;
   for(i=0;i<n-1;i++)
     {
      str+=StringFormat("%.5g,",y[i]);
      if(20<k++){k=0; str+="\n";}
     }
   str+=StringFormat("%.5g]},\n",y[n-1]);
   FileWriteString(Fhandle,str);
   Num++;
   return(0);
  }
//+------------------------------------------------------------------+
//| AddGraph                                                         |
//+------------------------------------------------------------------+
int CLinDraw::AddGraph(double &x[],double &y[],string type,string name,int w=0,string col="")
  {
   int i,k,n;
   string str;

   if(Fhandle<0)return(-1);
   if(Num==0)
     {
      str="$(document).ready(function(){\n"
          "var lp=new Highcharts.Chart({\n"
          "chart:{renderTo:'lplot'},\n"
          "title:{text:'"+Tit+"'},\n"
          "subtitle:{text:'"+SubTit+"'},\n"
          "legend:{enabled:"+Leg+"},\n"
          "yAxis:{title:{text:'"+Ytit+"'}},\n"
          "xAxis:{title:{text:'"+Xtit+"'},showLastLabel:true},\n"
          "series:[\n";
      FileWriteString(Fhandle,str);
     }
   n=ArraySize(x);
   str="{type:'"+type+"',name:'"+name+"',";
   if(col!="")str+="color:'rgba("+col+")',";
   if(w!=0)str+="lineWidth:"+(string)w+",";
   str+="data:[";
   k=0;
   for(i=0;i<n-1;i++)
     {
      str+=StringFormat("[%.5g,%.5g],",x[i],y[i]);
      if(20<k++){k=0; str+="\n";}
     }
   str+=StringFormat("[%.5g,%.5g]]},\n",x[n-1],y[n-1]);
   FileWriteString(Fhandle,str);
   Num++;
   return(0);
  }
//+------------------------------------------------------------------+
//| AddGraph                                                         |
//+------------------------------------------------------------------+
int CLinDraw::AddGraph(double &x[],double y,string type,string name,int w=0,string col="")
  {
   int i,k,n;
   string str;

   if(Fhandle<0)return(-1);
   if(Num==0)
     {
      str="$(document).ready(function(){\n"
          "var lp=new Highcharts.Chart({\n"
          "chart:{renderTo:'lplot'},\n"
          "title:{text:'"+Tit+"'},\n"
          "subtitle:{text:'"+SubTit+"'},\n"
          "legend:{enabled:"+Leg+"},\n"
          "yAxis:{title:{text:'"+Ytit+"'}},\n"
          "xAxis:{title:{text:'"+Xtit+"'},showLastLabel:true},\n"
          "series:[\n";
      FileWriteString(Fhandle,str);
     }
   n=ArraySize(x);
   str="{type:'"+type+"',name:'"+name+"',";
   if(col!="")str+="color:'rgba("+col+")',";
   if(w!=0)str+="lineWidth:"+(string)w+",";
   str+="data:[";
   k=0;
   for(i=0;i<n-1;i++)
     {
      str+=StringFormat("[%.5g,%.5g],",x[i],y);
      if(20<k++){k=0; str+="\n";}
     }
   str+=StringFormat("[%.5g,%.5g]]},\n",x[n-1],y);
   FileWriteString(Fhandle,str);
   Num++;
   return(0);
  }
//+------------------------------------------------------------------+
//| LDraw                                                            |
//+------------------------------------------------------------------+
int CLinDraw::LDraw(int ashow=1)
  {
   int i,k;
   string pfnam,to,p[];

   FileWriteString(Fhandle,"]});\n});");
   if(Fhandle<0)return(-1);
   FileClose(Fhandle);

   pfnam=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+Fnam;
   k=StringSplit(MQL5InfoString(MQL5_PROGRAM_PATH),StringGetCharacter("\\",0),p);
   to="";
   for(i=0;i<k-1;i++)to+=p[i]+"\\";
   to+="ChartTools\\";                          // Folder name
   DeleteFileW(to+Fnam);
   MoveFileW(pfnam,to+Fnam);
   if(ashow==1)ShellExecuteW(NULL,"open",to+"LinDraw.htm",NULL,NULL,1);
   return(0);
  }

해당 클래스가 제대로 작동하려면 자바스크립트 라이브러리가 구현된 HTML 페이지가 필요합니다. 차트에 필요한 필드 크기 및 위치 또한 특정되어야 합니다. 본문 하단의 첨부 파일에 해당 페이지 구현 예제가 포함되어 있습니다.

텍스트 파일에 AddGraph() 메소드가 호출되면 적절한 자바스크립트 코드가 생성됩니다. 해당 텍스트 파일은 이미 생성된 HTML 페이지에 병합됩니다. 이때 코드는 그래픽 라이브러리를 참조하며 AddGraph() 메소드에 전달된 데이터를 인수로 이용해 차트를 생성합니다. 해당 메소드를 재호출하면 한 개 이상의 차트를 아웃풋 필드에 추가할 수 있습니다.

세 가지 버전의 AddGraph() 오버로드 함수가 CLinDraw 클래스에 포함되어 있습니다. 그 중 하나는 표시된 데이터를 포함하는 단 하나의 배열만을 인수로 전달받습니다. 시퀀스 엘리먼트 인덱스가 X축에 나타나는 시퀀스 차트 생성을 위해 고안되었죠.

다른 하나는 두 개의 배열을 인수로 이용합니다. 이 두 배열은 각각 X축과 Y축에 나타나는 값을 포함하죠. 나머지 하나는 Y축의 상수 값을 설정합니다. 이를 이용해 수평선을 그릴 수도 있죠. 해당 함수의 나머지 인수는 꽤 유사합니다.

  • string type – 디스플레이 차트 유형 'line', 'spline' 또는 'scatter' 값을 가질 수 있습니다.
  • string name – 차트 이름
  • int w=0 – 선 너비 선택적 인수 값이 0인 경우 기본 선 너비 값은 2가 됩니다.
  • string col="" – 선 색깔 및 투명도 설정 선택적 인수

LDraw() 메소드는 가장 마지막에 호출되어야 합니다. 해당 스크립트는 자바스크립트 코드 아웃풋을 완성하며 텍스트 파일로 저장된 데이터는 자동으로 \MQL5\Files\에 저장됩니다. 그 후 해당 파일은 그래픽 라이브러리와 HTML 파일이 포함된 디렉토리로 이동됩니다.

LDraw() 메소드는 단일 선택적 인수를 가질 수 있습니다. 인수 값이 설정되지 않거나 1로 설정되는 경우 기본 설정된 인터넷 브라우저가 열리며 데이터 파일을 디렉토리로 옮기면 차트가 나타납니다. 인수가 1이 아닌 값을 갖는 경우 데이터 파일은 형성되지만 인터넷 브라우저는 자동으로 열리지 않습니다.

CLinDraw 클래스 메소드를 이용하면 차트 헤더와 축 라벨을 설정할 수 있습니다. 하이차트 라이브러리와 CLinDraw 클래스 관련 사항에 대해서는 따로 다루지 않겠습니다. 하이차트 그래픽 라이브러리에 대한 정보는 참고 자료 [19]에서, 차트 및 다이어그램 생성을 위한 라이브러리 적용 예제는 참고 자료 [9]와 [20]에서 찾아볼 수 있습니다.

CLinDraw 클래스의 정상적인 작동을 위해 터미널의 외부 라이브러리 사용이 반드시 허용되어야 합니다.


8. 밀도 추정 예제 및 첨부 파일 설명

CKDensity, CSJPlugin 그리고 CLinDraw의 세 가지 클래스를 만들었습니다.

아래는 클래스 사용 방법 예제의 소스 코드입니다.

//+------------------------------------------------------------------+
//|                                                  KDE_Example.mq5 |
//|                                                    2012, victorg |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2012, victorg"
#property link      "https://www.mql5.com"

#include "CKDensity.mqh"
#include "ChartTools\CLinDraw.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   int i,n;
   double a;
   int ndata=1000;                       // Input data length
   double X[];                           // Array for data
   double Z[];                           // Array for graph of a Laplace distribution
   double Sc[];                          // Array for scatter plot

//-------------------------- Preparation of the input sequence
   ArrayResize(X,ndata+1);
   i=CopyOpen(_Symbol,PERIOD_CURRENT,0,ndata+1,X);
   if(i!=ndata+1)
     {
      Print("Not enough historical data.");
      return;
     }
   for(i=0;i<ndata;i++)X[i]=MathLog(X[i+1]/X[i]); // Logarithmic returns
   ArrayResize(X,ndata);

//-------------------------- Kernel density estimation
   CKDensity *kd=new CKDensity;
   kd.PluginMode(1);                     // Enable Plug-in mode
   kd.Density(X);                        // Density estimation 

//-------------------------- Graph of a Laplace distribution
   n=kd.Np;                              // Number of test point
   ArrayResize(Z,n);
   for(i=0;i<n;i++)
     {
      a=MathAbs(kd.T[i]*M_SQRT2);
      Z[i]=0.5*MathExp(-a)*M_SQRT2;
     }
//-------------------------- Scatter plot
   n=kd.N;                               // Data length
   if(n<400)
     {
      ArrayResize(Sc,n);
      for(i=0;i<n;i++)Sc[i]=kd.X[i];
     }
   else                                  // Decimation of data
     {
      ArrayResize(Sc,400);
      for(i=0;i<400;i++)
        {
         a=i*(n-1.0)/399.0+0.5;
         Sc[i]=kd.X[(int)a];
        }
     }

//-------------------------- Visualization
   CLinDraw *ld=new CLinDraw;
   ld.Title("Kernel Density Estimation");
   ld.SubTitle(StringFormat("Data lenght=%i, h=%.3f",kd.N,kd.H));
   ld.YTitle("density");
   ld.XTitle("normalized X (mean=0, variance=1)");

   ld.AddGraph(kd.T,Z,"line","Laplace",1,"200,120,70,1");
   ld.AddGraph(kd.T,kd.Y,"line","Estimation");
   ld.AddGraph(Sc,-0.02,"scatter","Data",0,"0,4,16,0.4");

   ld.LDraw();                      // With WEB-browser autostart 
//   ld.LDraw(0);                        // Without autostart

   delete(ld);
   delete(kd);
  }

확률 밀도 추정 함수가 적용될 데이터는 KDE_Example.mq5 스크립트 실행 시 형성됩니다. 현재 통화쌍 값을 불러오기하면 피리어드가 X[] 배열로 복사됩니다. 이를 기반으로 로그 수익률이 산출됩니다.

다음은 CKDensity 클래스 사본을 만들 차례입니다. PluginMode() 메소드를 호출하면 SJPI를 h 범위 평가에 적용할 수 있습니다. 그러면 X[] 배열에 형성된 시퀀스에 대한 밀도 추정이 실행됩니다. 이렇게 평가 과정이 완료되었습니다. 나머지 단계는 획득된 밀도 추정 값의 시각화에 쓰입니다.

잘 알려진 분포 법칙을 이용해 추정 결과와 대조합니다. 이를 위해 Z[] 배열에 Laplace 분포 법칙을 따르는 값이 형성됩니다. 그 후 정규화되고 정렬된 인풋 데이터가 Sc[] 배열에 저장됩니다. 인풋 시퀀스 길이가 400 엘리먼트를 초과하는 경우 모든 값이 Sc[]에 포함되지는 않습니다. 부분적으로 생략될 겁니다. 다시 말해, Sc[] 배열은 최대 400개의 엘리먼트만 포함할 수 있습니다.

필요한 모든 데이터가 준비되면 CLinDraw 클래스 사본이 생성됩니다. AddGraph() 메소드를 이용해 헤드를 설정하고 필요한 차트를 추가합니다.

첫 번째 차트는 템플릿으로 활용될 Laplace 분포 차트입니다. 그 다음은 인풋 데이터로 산출한 밀도 추정 차트입니다. 소스 데이터 그룹을 나타내는 차트가 마지막으로 추가됩니다. 필요한 모든 엘리먼트가 설정되면 LDraw() 메소드를 호출합니다.

그림 7에 결과 차트가 나타나 있습니다.

그림 7. USDJPY 일간 로그 수익률에 대한 밀도 추정

그림 7. USDJPY 일간 로그 수익률에 대한 밀도 추정

그림 7의 추정은 1000개의 USDJPY 일간 로그 수익률 값을 대상으로 실행되었습니다. Laplace 분포와 매우 비슷한 곡선이 형성되었죠.

밀도 추정 및 시각화에 필요한 모든 파일은 본문 하단의 KDEFiles.zip 아카이브에서 찾아볼 수 있습니다. 해당 아카이브는 다음의 파일을 포함합니다.

  • DensityEstimation\ChartTools\CLinDraw.mqh – highcharts.js 인터랙션 클래스
  • DensityEstimation\ChartTools\highcharts.js - Highcharts JS v2.2.0 라이브러리(2012-02-16)
  • DensityEstimation\ChartTools\jquery.js – jQuery v1.7.1 라이브러리
  • DensityEstimation\ChartTools\LinDraw.htm – 디스플레이용 HTML 페이지
  • DensityEstimation\CKDensity.mqh – 밀도 추정 구현 클래스
  • DensityEstimation\CSJPlugin.mqh – 최적 범위 측정에 대한 SJPI 구현 클래스
  • DensityEstimation\KDE_Example.mq5 – 로그 수익률 밀도 추정 예제

아카이브 압축 해제 후 DensityEstimation\ 디렉토리는 터미널의 Scripts 또는 Indicators 티렉토리에 저장되어야 합니다. 그러면 컴파일을 거쳐 KDE_Example.mq5를 실행할 수 있게 됩니다. 필요한 경우 DensityEstimation 디렉토리의 이름은 변경해도 좋습니다. 프로그램 오퍼레이션에 아무런 영향을 끼치지 않으니까요.

CLinDraw 클래스가 정상적으로 작동하려면 터미널 내 외부 라이브러리 사용이 허용되야 합니다.

DensityEstimation\ 디렉토리에 ChartTools\ 서브디렉토리가 포함되며 추정 결과 시각화에 필요한 모든 컴포넌트가 여기에 위치합니다. DensityEstimation\ 디렉토리와 쉽게 구분이 가능하도록 시각화 파일은 별도의 서브디렉토리에 저장되었습니다. ChartTools\ 서브디렉토리 명칭은 소스 코드에 포함되어 있습니다. 따라서 이름을 변경하는 경우 소스 코드 자체를 수정해야 합니다.

시각화 과정에서는 텍스트 파일이 생성됩니다. 해당 파일에는 차트 작성에 필요한 데이터가 포함되어 있죠. 해당 파일은 먼저 터미널의 Files 디렉토리에 생성됩니다. 그 후 ChartTools 디렉토리로 이동되죠. 따라서 Files\ 또는 그 밖의 터미널 디렉토리에는 테스트 예제와 관련해 어떤 기록도 남지 않게 됩니다. 따라서 본문에 포함된 첨부 파일의 삭제를 원하는 경우 DensityEstimation 디렉토리만 삭제하면 됩니다.


결론

각 추정 방법 별로 핵심이 되는 수식만 설명했습니다. 이해하기 쉽도록 말이예요. 전체 공식에 대한 자료는 아주 쉽게 찾아볼 수 있습니다. 그 중 몇 가지는 하단에 링크해 놓았습니다.

본문에서 살펴본 소스 코드에 대한 테스트는 충분히 진행되지 않았습니다. 따라서 위의 소스 코드는 예제로만 간주되어야 하며 실제로 적용해서는 안됩니다.


참고 자료

  1. Histogram.
  2. Kernel density estimation.
  3. Kernel smoother.
  4. Expectation–maximization algorithm.
  5. А.V. Batov, V.Y. Korolyov, А.Y. Korchagin. EM-Algorithm Having a Large Number of Components as a Means of Constructing Non-Parametric Density Estimations (in Russian).
  6. Hardle W. Applied Nonparametric Regression.
  7. Kernel (statistics).
  8. Charts and diagrams in HTML.
  9. Simon J. Sheather. (2004). Density Estimation.
  10. Max Kohler, Anja Schindler, Stefan Sperlich. (2011). A Review and Comparison of Bandwidth Selection Methods for Kernel Regression.
  11. Clive R. Loader. (1999). Bandwidth Selection: Classical or Plug-In?.
  12. S. J. Sheather, M. C. Jones. (1991). A Reliable Data-Based Bandwidth Selection Method for Kernel Density Estimation.
  13. Newton's method.
  14. Data binning.
  15. Changjiang Yang, Ramani Duraiswami and Larry Davis. (2004). Efficient Kernel Machines Using the Improved Fast Gauss Transform.
  16. Fast optimal bandwidth estimation for univariate KDE.
  17. R.J. Karunamuni and T. Alberts. (2005). A Locally Adaptive Transformation Method of Boundary Correction in Kernel Density Estimation.
  18. Highcharts JS.
  19. 시간열 주요 특성 분석.


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

파일 첨부됨 |
kdefiles.zip (82.31 KB)
알고리즘 트레이딩 기사를 작성하고 200달러를 받으세요! 알고리즘 트레이딩 기사를 작성하고 200달러를 받으세요!
기사를 작성하고 알고리즘 트레이딩이 발전하도록 기여해 보세요. 여러분의 트레이딩 및 프로그래밍 경험을 공유해 주시면 $200를 지급해 드립니다. 또한 인기 있는 MQL5.com 웹사이트에 글을 게시하면 전문적인 커뮤니티에서 여러분의 브랜드를 홍보할 수 있는 좋은 기회가 됩니다. 수천 명의 트레이더들이 여러분의 작품을 읽어 볼 것입니다. 비슷한 생각을 가진 사람들과 아이디어를 논의하고 새로운 경험을 쌓고 여러분이 가진 지식을 수익화할 수 있습니다.
통계의 기초 통계의 기초
기본적 분석을 이용하는 투자자도 어느 정도의 통계적 연산은 필요로 합니다. 본문에서는 통계의 기초와 기본 구성 요소, 그리고 의사결정 과정에 있어 통계의 중요성을 알아보겠습니다.
일반화된 통계 분포의 구조 분석에 고유값 좌표계 적용하기 일반화된 통계 분포의 구조 분석에 고유값 좌표계 적용하기
응용통계학에 있어 가장 큰 문제는 가설 검증입니다. 아주 오랫동안 해결 불가능한 것으로 치부되어 왔죠. 하지만 고유값 좌표계가 나타나면서 상황이 바뀌었죠. 현대 응용통계학을 이용한 것보다 훨씬 효율적인 시그널 구조 연구가 가능해졌습니다. 본문은 고유값 좌표계의 실제 적용과 MQL5 구현을 다룹니다. Hilhorst와 Schehr가 소개한 분포를 예로 들어 함수 식별 문제에 대해서도 알아보겠습니다.
MQL5.community 회원 활동 기록 MQL5.community 회원 활동 기록
MQL5.com은 여러분 한 분 한 분을 기억하고 있답니다. 어떤 글을 썼는지, 게시글의 조회수는 얼마인지, 코드 베이스의 프로그램 다운로드 수는 몇 회인지까지도 모두 알고 있죠. 게다가 이건 일부일 뿐이랍니다. 개인 활동 기록은 프로필에서 확인 가능하지만 전체 회원의 활동 기록은 어떻게 확인할 수 있을까요? 이번에는 MQL5.community 회원 활동에 대해 알아보겠습니다.