English Русский Deutsch 日本語 Türkçe
preview
MetaTrader 5에서 L1 추세 필터링 적용하는 법

MetaTrader 5에서 L1 추세 필터링 적용하는 법

MetaTrader 5 — 예 |
67 9
MetaQuotes
MetaQuotes
L1 추세 필터링의 주요 목표는 다음과 같은 방식으로 시계열에서 기본 추세를 추출하는 것입니다:
  • 데이터의 장기적인 변화 추이가 보존됩니다;
  • 단기적인 변동과 잡음이 억제됩니다;
  • 추세 기울기의 변화인 구조적 변곡점이 자동으로 감지됩니다.
기존의 평활화 메서드와 달리 이 접근 방식은 추세의 평활도를 강제하지 않고 대신 구간별 선형 근사치를 생성하는데 이는 특히 금융 시계열 분석에서 특히 중요합니다.

L1 트렌드 필터링



콘텐츠


소개

금융 시계열 데이터는 높은 잡음 수준, 잦은 이상치, 그리고 변화하는 시장 환경이라는 특징을 가지고 있습니다. 실제 트레이딩 시스템에서 이는 간단하고 측정 가능한 방식으로 나타납니다: 고전적인 "평활" 필터(이동 평균, HP)는 추세 변화에 뒤처지고 기울기 변화의 순간을 모호하게 하며 종종 단기적인 조정을 추세 반전으로 잘못 해석합니다 — 결과적으로 잘못된 진입/청산 횟수가 증가하고 수익계수가 감소하며 드로다운이 증가합니다. 또한 정규화 매개변수 λ의 선택은 일반적으로 수동 조정으로 축소되며 상품, 차트 주기 및 히스토리 길이에 따라 제대로 전달되지 않습니다.

이 글은 L1 추세 필터링을 기반으로 이러한 문제에 대한 실제적인 해결책을 제시합니다: 2차 차분의 L1 정규화를 이용한 최적화는 명시적인 분기점을 갖는 구분적 선형 근사치를 자동으로 생성합니다. 주요 이점은 변곡점을 커다란 추세의 변화로 명확하게 해석할 수 있다는 점, λmax를 계산하고 상대적 매개변수 λ = coef · λmax로 이동하여 정규화 규모를 설정할 수 있다는 점, 그리고 MQL5 구현에 적합한 선형적인 계산 복잡성을 갖는다는 점입니다.

이 글에서는 이론뿐만 아니라 완벽한 실무 로드맵을 제시합니다: 여기에는 λmax 및 L1 추세 계산을 위한 메서드, 세 가지 지표(추세, 기울기, 기울기 부호), 7가지 L1 추세 변동성 지표, Expert Advisors와의 통합, 그리고 재현 가능한 테스트 프로토콜(네 가지 필터링 모드, 잔고/자산 내보내기 및 시각화).


1. 추세 필터링 문제의 공식화

우리는 두 구성 요소의 합으로 표현되는 스칼라 시계열을 고려합니다.

, 

는 추세 구성 요소입니다. 그리고 는 잡음 또는 불규칙적인 요소입니다.

목표는 추세를 추정하는 것입니다. ​  관찰된 데이터로부터  ​.

이 문제는 원본 데이터에 대한 충실도와 추정된 추세의 평활도 사이의 절충이라고 표현될 수 있습니다.


1.1. 호드릭-프레스콧 필터

호드릭-프레스콧 필터는 추세를 최소화 문제의 해로 정의합니다:

,

여기서 매개변수 λ는 평활화 정도를 조절합니다.

HP 필터의 주요 특징:

  • 데이터에 대한 선형성;
  • 계산 복잡도 O(n);
  • λ 값이 작을 경우 추세는 원래 데이터에 근접합니다;
  • λ 값이 클수록 추세는 최적의 선형 근사치에 수렴합니다;

하지만 HP 필터는 언제나 부드러운 추세를 나타내며 기울기의 급격한 변화를 제대로 감지하지 못합니다.


1.2. L1 트렌드 필터링 메서드

L1 추세 필터링의 핵심 아이디어는 원래 데이터와 유사하면서 기울기 변화가 최소화된 추세를 찾는 것입니다. 곡률 제곱을 최소화하는 고전적인 평활화 메서드와 달리 L1 방식은 2차 차분의 절댓값 합을 최소화합니다.

이는 근본적으로 다른 결과를 가져옵니다:

  • 대부분의 2차 차분은 0이 됩니다.
  • 추세선은 자동으로 선형 구간으로 나뉩니다.

따라서 L1 필터는 추세를 평활화 하려는 것이 아니라 관찰된 데이터를 설명하는 최소한의 구조적 변화를 찾는 데 중점을 둡니다. 이러한 특징 때문에 이 메서드는 특히 금융 시계열 분석에 적합합니다. 왜냐하면 금융 시계열의 동태는 종종 준선형적인 성장과 하락 단계의 연속으로 구성되기 때문입니다.

L1 추세 필터링에서는 2차 차분에 대한 2차 페널티가 L1 노름으로 대체되고 추세는 볼록 최적화 문제의 해로 정의됩니다:


행렬 형태로 나타내면:


설명:

  • y — 입력 시계열;
  • x — 추정 추세;
  • D — 2차 차분 행렬;
  • λ>=0 — 정규화 매개변수.

L1 노름을 사용하면 근본적으로 다른 결과가 나타납니다: 많은 2차 차분이 0이 되는데 이는 추세가 구간별 선형이 된다는 것입니다.

2차 차분은 다음과 같이 정의됩니다:

만약에면 점들은 일직선상에 놓여 있습니다.

따라서 제로 2차 차분은 추세의 선형 구간에 해당하고 제로가 아닌 2차 차분은 변곡점에 해당합니다. L1 노름은 벡터 Dx의 희소성을 촉진하는데 이는 대부분의 2차 차분이 0이 된다는 것을 의미합니다. 이는 해당 구간 동안 추세가 선형적임을 의미합니다. 2차 차분이 0이 아닌 지점은 추세 변곡점으로 해석됩니다.

따라서 L1 트렌드 필터링 메서드는 구조적 변화 지점에서 연결된 선형 세그먼트 집합으로 트렌드를 자동으로 구성합니다.

L1 트렌드 필터링의 주요 특징:

    • 추세는 선형 구간으로 구성됩니다;
    • 변곡점은 시계열의 구조적 변화로 해석됩니다;
    • λ = 0일 때 추세는 원래 데이터와 일치합니다;
    • λ 값이 충분히 크면 추세는 정확히 최적의 선형 근사치가 됩니다;
    • 계산 복잡도는 관찰 횟수에 따라 선형으로 유지됩니다.


    1.3. 정규화 매개변수 λ의 역할

    매개변수 λ는 근사 정확도와 추세 복잡성 간의 균형을 조절합니다:

    λ 값 솔루션의 본질
    λ=0
    x=y, 평활화 없음
    작은 λ
    약한 평활화, 많은 변곡점
    중간 λ
    구간별 선형 추세
    큰 λ
    거의 선형적인 추세
    λ≥λmax
    엄격하게 선형적인 추세

    표 1. L1 추세의 정규화 매개변수 λ에 대한 의존성

    따라서 λ는 추세 변곡점의 개수와 위치를 제어합니다.


    1.4. 문제의 기하학적 해석

    원하는 추세 x는 n차원 공간의 한 점으로 볼 수 있습니다. 목적 함수의 첫 번째 항은 근사 정확도를 좌우하며 관측점 y를 중심으로 하는 유클리드 구를 정의합니다: x가 y에 가까울수록 오차가 작아집니다.

    2차 차분의 L1 노름을 이용한 정규화 항은 볼록 다면체 집합(다면체)을 정의합니다. L2 정규화에서 나타나는 매끄러운 타원체와는 달리 이 다면체는 날카로운 꼭짓점을 가지고 있습니다. 이러한 꼭짓점은 추세의 일부 2차 차분이 0인 상황에 해당합니다.

    L1 노름에 날카로운 모서리가 존재한다는 점이 바로 희소한 해를 초래하는 원인입니다: 최적해는 일부 제약 조건만 적용되는 다면체의 꼭짓점에 위치하는 경향이 있습니다. 이는 대부분의 2차 차분이 0이 되고 추세가 자동으로 구간별 선형 형태를 취하게 된다는 것을 의미합니다.

    최적해는 유클리드 구와 L1 다면체가 처음 만나는 지점에 해당합니다. 이 지점에서 추세는 제한된 수의 변곡점에 연결된 선형 구간으로 구성됩니다.

    매개변수 λmax는 유클리드 구가 L1 다면체와 꼭짓점이 아닌 선형 함수의 부분 공간을 따라 접하는 상황에 해당합니다. 이 경우 모든 2차 차분은 0이며 추세는 엄격하게 선형입니다.

    λ ≥ λmax인 경우 L1 제약 조건 중 어느 것도 활성화되지 않으므로 정규화 값을 더 증가시켜도 해는 변하지 않으며 추세는 선형으로 유지됩니다.

    1.5. λmax 계산 알고리즘

    길이가 N인 입력 벡터 y에 대한 최대 정규화 매개변수 λmax의 계산을 고려해 보겠습니다.

    1. (N−2)×N 크기의 2차 차분 행렬 D를 구성합니다.


    2. 곡률 벡터 Dy를 계산합니다.

    3. 다음 연립선형방정식을 풉니다:

    4. 벡터 v에서 가장 큰(절댓값) 요소를 취합니다:

    금융 시계열 분석에서 매개변수 λmax는 중요한 실질적 의미를 갖습니다:

    • 이를 통해 정규화 매개변수를 정규화 할 수 있습니다;
    • 이를 통해 λ의 선택은 데이터 스케일과는 무관 해집니다;
    • 이는 서로 다른 시계열 간의 비교를 단순화합니다;
    • 이를 통해 λ를 최대 정규화의 일부로 해석할 수 있습니다.

    λ=coef_lambda_max⋅λmax⁡ 형태의 상대 매개변수를 사용하면 실제 적용이 크게 단순화됩니다. 여기서 coef_lambda_max ∈ (0,1)입니다.

    다음의 지표 및 EA 예시에서 λ는 λmax 단위로 사용되며 매개변수 설정에서는 승수 coef_lambda_max를 지정합니다.


    2. MQL5를 이용한 L1 추세 계산 방법

    L1 추세 필터링를 실제로 사용하기 위해 double형 및 float형 벡터에 대해 두 가지 메서드가 구현되었습니다.

    • L1TrendFilterLambdaMax는 최대 정규화 매개변수를 계산합니다;
    • L1TrendFilter는 주어진 정규화 매개변수 λ 값에 대한 L1 추세를 계산하며 λ 값은 λmax 단위로 지정할 수도 있습니다.

    2.1. L1TrendFilterLambdaMax

    데이터 벡터에 대해 최대 정규화 매개변수 λmax를 계산하는 메서드.

    vector<double>에 대한 계산:

    bool  vector::L1TrendFilterLambdaMax(
       double          &lambda_max       // the maximum value of the regularization parameter lambda
       )
    vector<float>에 대한 계산:
    bool  vectorf::L1TrendFilterLambdaMax(
       float           &lambda_max       // the maximum value of the regularization parameter lambda
       );

    매개변수

    lambda

    [out] 정규화 매개변수 λmax의 최댓값 또는 오류 발생 시 -1입니다.

    Return value

    성공하면 true를 반환합니다.

    메모

    메모리 사용량은 벡터 크기에 비례하여 선형적으로 증가합니다.


    2.2. L1TrendFilter

    데이터 벡터의 L1 추세를 계산하는 메서드.

    vector<double>에 대한 계산:

    bool  vector::L1TrendFilter(
       double          lambda,         // regularization parameter
       bool            relative,       // flag indicating lambda is in λmax units
       vector&         result          // output vector with L1 filtering result
       );

    vector<float>에 대한 계산:

    bool  vectorf::L1TrendFilter(
       float           lambda,         // regularization parameter
       bool            relative,       // flag indicating lambda is in λmax units
       vectorf&        result          // output vector with L1 filtering result
       );

    매개변수

    lambda

    [in] 정규화 매개변수 람다의 값(relative = true인 경우 람다는 λmax의 분수로서 [0, 1] 범위로 정의됩니다).

    relative

    [in] λ를 지정하는 방법을 나타내는 플래그입니다. 참일 경우 λ는 λmax 단위로 주어집니다; 그렇지 않으면 절댓값이 사용됩니다.

    result

    [out] L1 필터링 결과를 포함하는 벡터입니다.

    Return value

    성공하면 true를 반환합니다.

    메모

    메모리 사용량은 벡터 크기에 비례하여 선형적으로 증가합니다.


    λ에 대한 권장 범위(상대 모드).

    λ 승수 결과
    0.005 – 0.015 거의 L2, 잡음
    0.02 – 0.04 마이크로 세그먼트
    0.04 – 0.07 신호에 최적
    0.07 – 0.12 중기적 추세
    0.12 – 0.25 시장 체제
    > 0.3 몇몇 세그먼트

    표 2. λ의 작동 범위(단위: λmax⁡)


    실제 적용 시에는 0.04~0.25 범위의 승수를 사용하는 것이 좋습니다.



    3. 적용 사례

    이 섹션에서는 모의 브라운 운동 데이터와 S&P 500 가격 데이터에 대한 L1 추세 계산과 브라운 운동 및 외환 시장 데이터 모두에 대한 λmax의 스케일링 속성을 살펴봅니다.

    또한 특정 종목 및 차트 주기에 대해 최적의 L1 추세 분해를 얻기 위한 최적의 정규화 매개변수(λmax의 승수)를 결정하는 데 도움이 되는 세 가지 지표 변형을 제시합니다.

    또한 이동평균, MACD, ADX 및 EMA 전략에 대한 매매 신호 필터링(L1 추세와의 일치)의 결과를 제시합니다.


    3.1. 시뮬레이션 데이터(랜덤 워크)에 대한 L1 추세 계산

    예를 들어, 모의 브라운 운동 데이터에서 정규화 매개변수 λ의 값을 다르게 하여 L1 추세를 계산하는 경우를 생각해 보겠습니다.

    스크립트 코드:

    //+------------------------------------------------------------------+
    //|                                                  TestL1Trend.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property script_show_inputs
    #include <Graphics\Graphic.mqh>
    //+------------------------------------------------------------------+
    //| Generate Brown movement data                                     |
    //+------------------------------------------------------------------+
    void BMData(vector<double> &data,int &data_count)
      {
       data.Resize(data_count);
       data[0] = 0.0;
       for(int i=1; i<data_count; i++)
          data[i] = data[i-1] + (MathRand()/32767.0 - 0.5);
      }
    //+------------------------------------------------------------------+
    //| CopyValues                                                       |
    //+------------------------------------------------------------------+
    bool CopyValues(vector<double> &data_v,double &data[])
      {
       int data_count=(int)data.Size();
       if(data_count==0)
          return(false);
       ArrayResize(data,data.Size());
       for(int i=0; i<data_count; i++)
          data[i]=data_v[i];
       return(true);
      }
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
       MathSrand(1);
       int data_count=1000;
       vector<double> data_test;
       BMData(data_test,data_count);
    //--- prepare arrays for chart
       double x[],y[];
       ArrayResize(x,data_count);
       ArrayResize(y,data_count);
       for(int i=0; i<data_count; i++)
          x[i]=i;
    //---
       CGraphic graphic;
       long chart=0;
       string name="test";
       if(ObjectFind(chart,name)<0)
          graphic.Create(chart,name,0,0,0,1000,600);
       else
          graphic.Attach(chart,name);
       graphic.BackgroundMain("L1 Trend filtering (random walk) with different lambda");
       graphic.BackgroundMainSize(16);
       graphic.HistoryNameWidth(60);
       graphic.HistoryColor(ColorToARGB(clrGray,255));
       graphic.XAxis().AutoScale(false);
       graphic.XAxis().Min(0);
       graphic.XAxis().Max(data_count);
    //---
       CopyValues(data_test,y);
       graphic.CurveAdd(x,y,CURVE_LINES,"Data").LinesWidth(1);
    //--- L1TrendFilterLambdaMax
       double lambda_max=0.0;
       if(data_test.L1TrendFilterLambdaMax(lambda_max))
          PrintFormat("lambda_max=%f",lambda_max);
    //---
       vector<double> data_l1;
       const double lambda_factors[]= {1.0,0.9,0.8,0.5,0.25,0.1,0.01,0.05,0.001,0.0005};
       for(int i=0; i<ArraySize(lambda_factors); i++)
         {
          double lambda=lambda_max*lambda_factors[i];
          PrintFormat("%d. lambda=%f",i+1,lambda);
          bool res=data_test.L1TrendFilter(lambda_factors[i],true,data_l1);
          if(res)
            {
             CopyValues(data_l1,y);
             graphic.CurveAdd(x,y,CURVE_LINES,"lambda="+DoubleToString(lambda,0)).LinesWidth(3);
            }
         }
    //---
       graphic.CurvePlotAll();
       graphic.Update();
       DebugBreak();
      }
    //+------------------------------------------------------------------+
    
    출력:
    TestL1Trend (EURUSD,H1) lambda_max=51703.353749
    TestL1Trend (EURUSD,H1) 1. lambda=51703.353749
    TestL1Trend (EURUSD,H1) 2. lambda=46533.018374
    TestL1Trend (EURUSD,H1) 3. lambda=41362.682999
    TestL1Trend (EURUSD,H1) 4. lambda=25851.676874
    TestL1Trend (EURUSD,H1) 5. lambda=12925.838437
    TestL1Trend (EURUSD,H1) 6. lambda=5170.335375
    TestL1Trend (EURUSD,H1) 7. lambda=517.033537
    TestL1Trend (EURUSD,H1) 8. lambda=2585.167687
    TestL1Trend (EURUSD,H1) 9. lambda=51.703354
    TestL1Trend (EURUSD,H1) 10. lambda=25.851677
    

    이 예시에서는 정규화 매개변수 λ를 줄이면 추세 구간으로의 더욱 상세한 분해가 가능함을 알 수 있습니다(그림 1).

    만약 λ ≥ λmax이면 해는 선형 회귀(전역 추세)에 해당하는 직선이 됩니다.


    그림 1. 브라운 운동 데이터에 대해 λ 값이 다를 때 L1 필터 계산예시

    그림 1. 브라운 운동 데이터에 대해 λ 값이 다를 때 L1 필터 계산 예시


    L1 추세를 계산하는 함수는 double형 벡터와 float형 벡터 모두에 사용할 수 있습니다.

    계산 결과를 비교하기 위한 테스트 스크립트는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                                       TestL1TrendFloatDouble.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include <Graphics\Graphic.mqh>
    
    uint32_t ExtSeed=1;
    //+------------------------------------------------------------------+
    //| Generate Brown movement data                                     |
    //+------------------------------------------------------------------+
    template<typename T>
    void BMData(vector<T> &data,uint64_t data_count)
      {
       MathSrand(ExtSeed);
    
       data.Resize(data_count);
       data[0] = 0.0;
    
       for(uint64_t i=1; i<data_count; i++)
          data[i] = data[i-1] + T(MathRand()/32767.0 - 0.5);
      }
    //+------------------------------------------------------------------+
    //| CopyValues                                                       |
    //+------------------------------------------------------------------+
    template<typename T>
    bool CopyValues(double &data[],const vector<T> &data_v)
      {
       if(ArrayResize(data,data.Size())!=data.Size())
          return(false);
       for(uint64_t i=0; i<data.Size(); i++)
          data[i]=data_v[i];
    
       return(true);
      }
    //+------------------------------------------------------------------+
    //| L1TrendCalculate                                                 |
    //+------------------------------------------------------------------+
    template<typename T>
    bool L1TrendCalculate(double &result[],uint64_t data_count,double lambda,bool lambda_is_relative)
      {
       vector<T> data_test;
       BMData(data_test,data_count);
    
       vector<T> vres;
       if(!data_test.L1TrendFilter((T)lambda,lambda_is_relative,vres))
          return(false);
       if(ArrayResize(result,(uint32_t)vres.Size())!=vres.Size())
          return(false);
       for(uint64_t n=0; n<result.Size(); n++)
          result[n]=vres[n];
    
       return(true);
      }
    //+------------------------------------------------------------------+
    //| TestRun                                                          |
    //+------------------------------------------------------------------+
    bool TestRun(uint32_t data_count,uint32_t mode)
      {
    //--- create graph
       CGraphic graphic;
       long     chart=0;
       string   name="L1TrendTest";
    
       if(ObjectFind(chart,name)<0)
          graphic.Create(chart,name,0,0,0,1280,600);
       else
          graphic.Attach(chart,name);
    
       string mode_name="(";
       if((mode&1)==1)
          mode_name+="DOUBLE";
       if((mode&3)==3)
          mode_name+=" & ";
       if((mode&2)==2)
          mode_name+="FLOAT";
       mode_name+=")";
    
       graphic.BackgroundMain("L1Trend filtering (random walk) with different lambda "+mode_name);
       graphic.BackgroundMainSize(16);
       graphic.HistoryNameWidth(60);
       graphic.HistoryColor(ColorToARGB(clrGray,255));
       graphic.XAxis().AutoScale(false);
       graphic.XAxis().Min(0);
       graphic.XAxis().Max(data_count);
    //--- prepare arrays
       double x[];
       double y[];
    
       if(ArrayResize(x,data_count)!=data_count)
          return(false);
      
       for(uint32_t i=0; i<data_count; i++)
          x[i]=i;
    
       vector<double> v;
       BMData(v,data_count);
       v.Swap(y);
       graphic.CurveAdd(x,y,CURVE_LINES,"Data").LinesWidth(1);
    //--- calculate
       const double lambda_factors[]= {1.0,0.9,0.8,0.5,0.25,0.1,0.01,0.05,0.001,0.0005};
    //--- double
       if((mode&1)==1)
         {
          for(uint64_t i=0; i<lambda_factors.Size(); i++)
            {
             if(L1TrendCalculate<double>(y,data_count,lambda_factors[i],true))
                graphic.CurveAdd(x,y,CURVE_LINES,"DBL="+DoubleToString(lambda_factors[i],4)).LinesWidth(4);
            }
         }
    //--- float
       if((mode&2)==2)
         {
          for(uint64_t i=0; i<lambda_factors.Size(); i++)
            {
             if(L1TrendCalculate<float>(y,data_count,(float)lambda_factors[i],true))
                graphic.CurveAdd(x,y,CURVE_LINES,"FLT="+DoubleToString(lambda_factors[i],4)).LinesWidth(2);
            }
         }
    //--- update
       graphic.CurvePlotAll();
       graphic.Update();
       return(true);
      }
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
       for(uint32_t n=0; !IsStopped(); n++,Sleep(1000))
         {
          TestRun(1000,1+n%3);
    
          if((n%3)==2)
             ExtSeed++;
         }
      }
    //+------------------------------------------------------------------+

    출력:


    3.2. S&P 500 가격 시리즈에 대한 L1 추세 계산

    오리지날 논문 , SJ에서 log(S&P 500) 계산을 살펴보십시오. Kim, K. Koh, S. Boyd 및 D. Gorinevsky, SIAM Review, 문제 및 기술 섹션, 51(2):339–360, 2009년 5월. 

    스크립트를 실행하려면 "snp500.txt" 파일의 데이터가 사용됩니다. 해당 파일은 terminal_data_folder\MQL5\Files 폴더에 넣어주셔야 합니다.

    //+------------------------------------------------------------------+
    //|                                       TestL1TrendFilterSP500.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property script_show_inputs
    #include <Graphics\Graphic.mqh>
    //+------------------------------------------------------------------+
    //| LoadData                                                         |
    //+------------------------------------------------------------------+
    void LoadData(string filename,vector<double> &data,int &data_count)
      {
       data_count=0;
       ResetLastError();
       int file_handle=FileOpen(filename,FILE_READ|FILE_TXT|FILE_ANSI);
       if(file_handle!=INVALID_HANDLE)
         {
          while(!FileIsEnding(file_handle))
            {
             string str=FileReadString(file_handle);
             if(data.Size()<=(ulong)data_count)
                data.Resize(data_count+1);
             data[data_count]=StringToDouble(str);
             data_count++;
            }
          FileClose(file_handle);
         }
       else
          PrintFormat("Failed to open %s file, Error code = %d",filename,GetLastError());
    //---
       data.Resize(data_count);
      }
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
       long chart=0;
       string name="log SP500";
       int data_count=0;
       vector<double> data_sp500;
       LoadData("snp500.txt",data_sp500,data_count);
       vector<double> data_l1_sp500;
       data_l1_sp500.Resize(data_count);
    //--- L1TrendFilterLambdaMax
       double lambda_max=0.0;
       if(data_sp500.L1TrendFilterLambdaMax(lambda_max))
          PrintFormat("Lambda_max=%f",lambda_max);
       double lambda=50;
    //--- L1TrendFilter
       if(data_sp500.L1TrendFilter(lambda,false,data_l1_sp500))
         {
          //--- prepare arrays for chart
          double x[],y[],y2[];
          ArrayResize(x,data_count);
          ArrayResize(y,data_count);
          ArrayResize(y2,data_count);
          for(int i=0; i<data_count; i++)
            {
             x[i]=i;
             y[i]=data_sp500[i];
             y2[i]=data_l1_sp500[i];
            }
          //---
          CGraphic graphic;
          if(ObjectFind(chart,name)<0)
             graphic.Create(chart,name,0,0,0,1000,600);
          else
             graphic.Attach(chart,name);
          graphic.BackgroundMain("log SP500 L1 trend filtering");
          graphic.BackgroundMainSize(16);
          graphic.HistoryNameWidth(60);
          graphic.HistoryColor(ColorToARGB(clrGray,255));
          graphic.XAxis().AutoScale(false);
          graphic.XAxis().Min(0);
          graphic.XAxis().Max(data_count);
          graphic.XAxis().DefaultStep(100);
          graphic.CurveAdd(x,y,CURVE_LINES,"SP500").LinesWidth(1);
          graphic.CurveAdd(x,y2,CURVE_LINES,"L1 trend").LinesWidth(3);
          graphic.CurvePlotAll();
          graphic.Update();
          DebugBreak();
         }
      }
    //+------------------------------------------------------------------+
    

    스크립트 실행 결과는 그림 2에 나와 있습니다.

    그림 2. S&P 500 지수의 로그 가격 시계열에 대한 L1 추세 추정 예시

    그림 2. S&P 500 지수의 로그 가격 시계열에 대한 L1 추세 추정 예시


    Expert 탭에서 해당 시계열에 대한 λmax 값이 표시됩니다:

    TestL1TrendFilterSP500 (EURUSD,H1)      Lambda_max=37394.835512
    

    이 스크립트는 해당 메서드의 저자들이 발표한 원 논문에서와 같이 고정된 값 λ = 50을 사용하여 L1TrendFilterLambdaMax 및 L1TrendFilter 메서드를 사용하는 방법을 보여줍니다.

    다음 예시에서는 정규화 매개변수 λ의 절대값 대신 상대값(λmax 단위)을 `relative = true` 플래그와 함께 사용합니다.


    3.3. λmax⁡의 스케일링 속성

    매개변수 λmax는 L1 필터링에서 핵심적인 역할을 하는데 이는 해법이 전역적인 선형 근사치로 퇴화하는 정규화의 상한을 정의하기 때문입니다. 이 양의 흥미로운 특성 중 하나는 시계열의 길이에 따라 스케일링이 달라진다는 점입니다.

    수치 실험 결과, λmax는 관측 횟수에 대해 멱법칙에 따라 증가하는 것으로 나타났습니다:

    설명: T는 시계열의 길이이고 α는 스케일링 지수입니다.

    랜덤 워크(브라운 운동)의 경우 지수는 α ≈ 2.5에 가까워야 함을 보여줄 수 있습니다. 브라운 운동의 진폭은 다음과 같이 증가합니다. 반면, 2차 차분 연산자는 다음과 같이 스케일링 됩니다..  λmax를 계산할 때 실제로는 급수의 곡률 적분과 관련된 양의 최댓값을 평가하는 것이 효과적입니다.


    결과적으로 결합된 스케일링은 다음과 같은 관계를 도출합니다:

    이는 지수 α ≈ 2.5에 해당합니다.

    따라서 시계열의 길이가 증가함에 따라 λmax 값은 선형적으로 증가하는 것보다 훨씬 빠르게 증가합니다.


    3.3.1. 브라운 운동에 대한 수치 실험

    스케일링 법칙을 검증하기 위해 수치 실험이 수행되었습니다.

    서로 다른 시계열 길이 T에 대해 λmax의 평균값이 계산된 후 브라운 운동의 실현값이 생성되었습니다.

    로그 근사법이 사용되었습니다:

    이를 통해 선형 회귀를 이용하여 지수 α를 추정할 수 있습니다.

    실험을 위한 코드는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                            TestScalingLambdaMaxBrownMovement.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #include <Graphics\Graphic.mqh>
    //+------------------------------------------------------------------+
    //| Generate Brownian motion                                         |
    //+------------------------------------------------------------------+
    void GenerateBrownian(int N,vector<double> &data)
      {
       data.Resize(N);
       data[0] = 0.0;
       for(int i=1; i<N; i++)
          data[i] = data[i-1] + (MathRand()/32767.0 - 0.5);
      }
    //+------------------------------------------------------------------+
    //| LinearRegression                                                 |
    //+------------------------------------------------------------------+
    void LinearRegression(const double &x[], const double &y[], int n, double &a, double &b)
      {
       double sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0;
       for(int i = 0; i < n; i++)
         {
          sx  += x[i];
          sy  += y[i];
          sxx += x[i] * x[i];
          sxy += x[i] * y[i];
         }
       double denom = n * sxx - sx * sx;
       a = (n * sxy - sx * sy) / denom;
       b = (sy - a * sx) / n;
      }
    //+------------------------------------------------------------------+
    //| TestScaling with statistics                                      |
    //+------------------------------------------------------------------+
    void TestScalingStatistics()
      {
       MathSrand(42);
       int RUNS = 10;     //
       int MC   = 10;   // Monte Carlo
       double alpha_values[];
       ArrayResize(alpha_values, RUNS);
    // --- geometric grid of T
       int nT = 8;
       int Tvals[];
       ArrayResize(Tvals, nT);
       int T0 = 64;
       for(int i = 0; i < nT; i++)
          Tvals[i] = T0 << i;
       Print("Scaling test with statistics");
    //---
       double logT[];
       double logLambda[];
       vector<double> bm;
       vector<double> l1_trend;
       for(int run = 0; run < RUNS; run++)
         {
          ArrayResize(logT, nT);
          ArrayResize(logLambda, nT);
          //---
          for(int i = 0; i < nT; i++)
            {
             int T = Tvals[i];
             double lambda_sum = 0.0;
             l1_trend.Resize(T);
             for(int k = 0; k < MC; k++)
               {
                GenerateBrownian(T, bm);
                double lambda_max=0.0;
                if (bm.L1TrendFilterLambdaMax(lambda_max))
                   lambda_sum += lambda_max;
                bm.L1TrendFilter(0.2,true,l1_trend);
               }
             double lambda_avg = lambda_sum / MC;
             logT[i]      = MathLog((double)T);
             logLambda[i] = MathLog(lambda_avg);
            }
          // --- regression
          double alpha, c;
          LinearRegression(logT, logLambda, nT, alpha, c);
          alpha_values[run] = alpha;
          PrintFormat("run %d -> alpha = %.6f", run+1, alpha);
         }
    //--- statistics
       double mean = 0.0;
       for(int i=0;i<RUNS;i++)
          mean += alpha_values[i];
       mean /= RUNS;
    // --- standard deviation
       double var = 0.0;
       for(int i=0;i<RUNS;i++)
          var += (alpha_values[i]-mean)*(alpha_values[i]-mean);
       var /= (RUNS - 1);
       double stddev = MathSqrt(var);
    // --- standard error of mean
       double sem = stddev / MathSqrt((double)RUNS);
    // --- theoretical comparison
       double alpha_theory=2.5;
       double percent_error=MathAbs(mean-alpha_theory)/alpha_theory*100.0;
    //--- results
       PrintFormat("mean alpha = %.6f", mean);
       PrintFormat("std deviation = %.6f", stddev);
       PrintFormat("standard error = %.6f", sem);
       PrintFormat("theory = %.4f", alpha_theory);
       PrintFormat("percent error from theory = %.4f %%", percent_error);
      }
    //+------------------------------------------------------------------+
    //| TestScaling                                                      |
    //+------------------------------------------------------------------+
    void TestScaling()
      {
       MathSrand(1);
    // --- geometric grid of T
       int nT = 8;
       int Tvals[];
       ArrayResize(Tvals,nT);
    //---
       int T0 = 64;
       for(int i=0; i<nT; i++)
          Tvals[i]=T0<<i;   // 64 * 2^i
    //---
       double logT[], logLambda[];
       ArrayResize(logT,nT);
       ArrayResize(logLambda,nT);
    //---
       Print("scaling test for lambda_max");
       for(int i=0; i<nT; i++)
         {
          int T = Tvals[i];
          //--- Monte-Carlo simulations
          int MC=1000;
          double lambda_sum = 0.0;
          for(int k=0; k<MC; k++)
            {
             vector<double> bm;
             GenerateBrownian(T, bm);
             double lambda_max=0.0;
             if(bm.L1TrendFilterLambdaMax(lambda_max))
                lambda_sum += lambda_max;
            }
          double lambda_avg=lambda_sum/MC;
          logT[i]= MathLog((double)T);
          logLambda[i]=MathLog(lambda_avg);
          PrintFormat("T=%5d   <lambda_max>=%.6f",T,lambda_avg);
         }
    // --- linear regression in log-log
       double alpha, c;
       LinearRegression(logT,logLambda,nT,alpha,c);
    //---
       PrintFormat("estimated scaling exponent alpha = %.4f",alpha);
       double alpha_theory=2.5;
       PrintFormat("theoretical value = %.4f",alpha_theory);
    //--- plot scaling law
       CGraphic g;
       g.Create(0, "ScalingLaw",0,0,0,1000,600);
       g.BackgroundMain("Scaling law of lambda_max (Brownian motion)");
       g.BackgroundMainSize(16);
       g.CurveAdd(logT, logLambda, CURVE_POINTS, "Simulation");
    //---
       double xfit[2], yfit[2];
       xfit[0] = logT[0];
       xfit[1] = logT[nT-1];
    //---
       yfit[0] = alpha*xfit[0] + c;
       yfit[1] = alpha*xfit[1] + c;
    //---least squares fit
       g.CurveAdd(xfit, yfit, CURVE_LINES, "LS fit");
       g.CurvePlotAll();
       g.Update();
       DebugBreak();
      }
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //--- calculate scaling with statistics
       TestScalingStatistics();
    //--- show sample results
       TestScaling();
      }
    //+------------------------------------------------------------------+

    출력:

    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   Scaling test with statistics    
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 1 -> alpha = 2.480774       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 2 -> alpha = 2.530977       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 3 -> alpha = 2.435511       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 4 -> alpha = 2.461984       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 5 -> alpha = 2.467093       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 6 -> alpha = 2.487965       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 7 -> alpha = 2.532371       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 8 -> alpha = 2.455831       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 9 -> alpha = 2.483485       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   run 10 -> alpha = 2.420283      
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   mean alpha = 2.475627   
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   std deviation = 0.036281        
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   standard error = 0.011473       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   theory = 2.5000 
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   percent error from theory = 0.9749 %    
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   scaling test for lambda_max     
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T=   64   <lambda_max>=97.302362        
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T=  128   <lambda_max>=566.626861       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T=  256   <lambda_max>=3162.076116      
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T=  512   <lambda_max>=18271.204936     
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T= 1024   <lambda_max>=100057.796790    
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T= 2048   <lambda_max>=578620.887399    
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T= 4096   <lambda_max>=3192555.936035   
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   T= 8192   <lambda_max>=17895314.647170  
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   estimated scaling exponent alpha = 2.4967       
    TestScalingLambdaMaxBrownMovement (EURUSD,H1)   theoretical value = 2.5000      
    

    이중 로그 그래프(이중 로그 스케일)는 브라운 운동에 대한 데이터 포인트 수에 대해 함수 λmax가 멱법칙 의존성을 나타낸다는 것을 보여줍니다.
    .

    그림 3, 브라운 운동에 대한 LambdaMax의 멱법칙 의존성

    그림 3. 브라운 운동에 대한 LambdaMax의 멱법칙 의존성


    시뮬레이션 결과는 다음과 같습니다:

    mean alpha = 2.4756
    std deviation = 0.036
    theory = 2.5
    percent error ≈ 1%

    따라서 이 실험은 이론적 관계를 확인시켜 줍니다:



    이중 로그 척도의 이중 로그 플롯은 log(λmax)와 log(T) 사이의 선형 관계를 보여줍니다.


    3.3.2. 금융 시계열 데이터의 스케일링

    외환 시장 가격 시계열에 대해서도 유사한 실험이 진행되었습니다. 다양한 통화쌍과 기간에 대해 지수 α를 추정했습니다.

    결과에 따르면 실제 금융 데이터의 경우 α 값은 α ≈ 2.45–2.60 범위에 있으며 이는 브라운 운동의 이론적 값과 매우 가깝습니다. 이는 λmax의 스케일링 동작이 거의 보편적이며 다양한 시장과 기간에 걸쳐 유효하다는 것을 의미합니다.

    TestScalingLambdaMaxSymbol.mq5 스크립트는 표준 시간 프레임 M1~H1에 걸쳐 주어진 심볼에 대한 λmax의 지수를 계산합니다.

    //+------------------------------------------------------------------+
    //|                                   TestScalingLambdaMaxSymbol.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property script_show_inputs
    //--- input parameters
    input string WorkSymbol   = "EURUSD";     // Symbol
    input int    YearStart    = 2024;
    input int    YearEnd      = 2025;
    #include <Graphics\Graphic.mqh>
    //+------------------------------------------------------------------+
    //| GetHistoricalData                                                |
    //+------------------------------------------------------------------+
    bool GetHistoricalData(double &data[], string symbol, ENUM_TIMEFRAMES tf, int year_start, int year_end)
      {
       datetime from = StringToTime(IntegerToString(year_start) + ".01.01 00:00");
       datetime to   = StringToTime(IntegerToString(year_end)   + ".12.31 23:59");
       int copied = CopyClose(symbol, tf, from, to, data);
       if(copied <= 0)
         {
          Print("Error in CopyClose: ", GetLastError());
          ArrayResize(data, 0);
          return false;
         }
    //PrintFormat("Loaded bars: %d (%s %s)", ArraySize(data), symbol, EnumToString(tf));
       return true;
      }
    //+------------------------------------------------------------------+
    //| LinearRegression                                                 |
    //+------------------------------------------------------------------+
    void LinearRegression(const double &x[], const double &y[], int n, double &a, double &b)
      {
       double sx = 0, sy = 0, sxx = 0, sxy = 0;
       for(int i = 0; i < n; i++)
         {
          sx  += x[i];
          sy  += y[i];
          sxx += x[i] * x[i];
          sxy += x[i] * y[i];
         }
       double denom = n*sxx - sx*sx;
       if(denom!=0)
         {
          a = (n*sxy-sx*sy)/denom;
          b = (sy-a*sx)/n;
         }
      }
    //+------------------------------------------------------------------+
    //| Scaling test for one timeframe                                   |
    //+------------------------------------------------------------------+
    bool TestScalingLambaMaxTF(string symbol, ENUM_TIMEFRAMES tf, double &logT_out[], double &logLambda_out[], double &alpha_out)
      {
       MathSrand(42);
       double prices[];
       if(!GetHistoricalData(prices, symbol, tf, YearStart, YearEnd))
          return false;
       int Tvals[];
       int nT=8;
       int T0=64;
       ArrayResize(Tvals, nT);
       for(int i = 0; i < nT; i++)
          Tvals[i] = T0 << i;
       ArrayResize(logT_out, nT);
       ArrayResize(logLambda_out, nT);
       int data_size = ArraySize(prices);
       vector<double> data_prices;
       for(int i = 0; i < nT; i++)
         {
          int T = Tvals[i];
          int MC = 1000;
          double lambda_sum = 0.0;
          for(int k = 0; k < MC; k++)
            {
             if(data_size < T)
                break;
             int start = MathRand() % (data_size - T);
             data_prices.Resize(T);
             for(int j=0; j<T;  j++)
                data_prices[j]=prices[start+j];
             double lambda_max=0.0;
             if(data_prices.L1TrendFilterLambdaMax(lambda_max))
                lambda_sum += lambda_max;
            }
          double lambda_avg = lambda_sum / MC;
          logT_out[i]=MathLog((double)T);
          logLambda_out[i]=MathLog(lambda_avg);
          //PrintFormat("TF=%s T=%5d   <lambda_max>=%.6f", EnumToString(tf), T, lambda_avg);
         }
       double c;
       LinearRegression(logT_out, logLambda_out, nT, alpha_out, c);
       PrintFormat("%s (%s) estimated scaling exponent = %.4f", symbol,EnumToString(tf), alpha_out);
       return true;
      }
    //+------------------------------------------------------------------+
    //| TestScalingLambdaMaxSymbol                                       |
    //+------------------------------------------------------------------+
    void TestScalingLambdaMaxSymbol(string symbol)
      {
       ENUM_TIMEFRAMES timeframes[] = {PERIOD_M1, PERIOD_M2, PERIOD_M3, PERIOD_M4, PERIOD_M5, PERIOD_M6,
                                       PERIOD_M10, PERIOD_M12, PERIOD_M15, PERIOD_M20, PERIOD_M30, PERIOD_H1
                                      };
       uint colors[] = {clrRed,clrBlue,clrGreen,clrOrange,clrPurple,clrDarkGreen,clrCyan,
                        clrNavy,clrOrangeRed,clrDodgerBlue,clrCrimson,clrDarkRed
                       };
    //---
       CGraphic g;
       g.Create(0,"ScalingLawTest",0,0,0,1000,600);
       g.BackgroundMain("Scaling law of lambda_max ("+symbol+")");
       g.BackgroundMainSize(16);
       PrintFormat("%s scaling test for standard timeframes",symbol);
       for(int i = 0; i < ArraySize(timeframes); i++)
         {
          double logT[], logLambda[], alpha;
          // Print("processing timeframe: ", EnumToString(timeframes[i]), " -----");
          if(TestScalingLambaMaxTF(symbol,timeframes[i],logT,logLambda,alpha))
            {
             g.CurveAdd(logT,logLambda,ColorToARGB(colors[i % ArraySize(colors)],255),CURVE_POINTS_AND_LINES,EnumToString(timeframes[i]));
            }
         }
       g.CurvePlotAll();
       g.Update();
    //---
       DebugBreak();
      }
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //--- estimate lambda_max scale exponent for price data
       TestScalingLambdaMaxSymbol(WorkSymbol);
      }
    //+------------------------------------------------------------------+
    

    EURUSD예 대한 결과:

    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD scaling test for standard timeframes     
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M1) estimated scaling exponent = 2.5038  
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M2) estimated scaling exponent = 2.5350  
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M3) estimated scaling exponent = 2.5034  
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M4) estimated scaling exponent = 2.5422  
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M5) estimated scaling exponent = 2.5341  
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M6) estimated scaling exponent = 2.5132  
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M10) estimated scaling exponent = 2.5188 
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M12) estimated scaling exponent = 2.5126 
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M15) estimated scaling exponent = 2.5208 
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M20) estimated scaling exponent = 2.4887 
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_M30) estimated scaling exponent = 2.5695 
    TestScalingLambdMaxSymbol (EURUSD,H1)   EURUSD (PERIOD_H1) estimated scaling exponent = 2.6118  

    EURUSD(표준 시간 프레임 M1~H1)에 대한 결과는 그림 4와 같습니다.

    그림 4. 다양한 EURUSD 시간 프레임에 대한 λmax의 멱법칙 의존성.

    그림 4. EURUSD의 다양한 시간 프레임에 대한 λmax의 멱법칙 의존성

    마찬가지로 다른 통화쌍도 분석할 수 있습니다.

    USDJPY의 경우:

    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY scaling test for standard timeframes     
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M1) estimated scaling exponent = 2.5851  
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M2) estimated scaling exponent = 2.5825  
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M3) estimated scaling exponent = 2.4889  
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M4) estimated scaling exponent = 2.5099  
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M5) estimated scaling exponent = 2.5059  
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M6) estimated scaling exponent = 2.4939  
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M10) estimated scaling exponent = 2.5548 
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M12) estimated scaling exponent = 2.5641 
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M15) estimated scaling exponent = 2.5525 
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M20) estimated scaling exponent = 2.5390 
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_M30) estimated scaling exponent = 2.5805 
    TestScalingLambdMaxSymbol (EURUSD,H1)   USDJPY (PERIOD_H1) estimated scaling exponent = 2.4645  
    

    USDJPY에 대한 결과 또한 멱법칙 관계로 잘 근사화됩니다.

    그림 5. 다양한 USDJPY 기간에 대한 λmax의 멱법칙 의존성

    그림 5. USDJPY의 다양한 시간 프레임에 대한 λmax의 멱법칙 의존성

    GBPUSD의 경우:

    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD scaling test for standard timeframes     
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M1) estimated scaling exponent = 2.5235  
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M2) estimated scaling exponent = 2.5449  
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M3) estimated scaling exponent = 2.5439  
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M4) estimated scaling exponent = 2.5427  
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M5) estimated scaling exponent = 2.5248  
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M6) estimated scaling exponent = 2.5308  
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M10) estimated scaling exponent = 2.5293 
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M12) estimated scaling exponent = 2.5235 
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M15) estimated scaling exponent = 2.5069 
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M20) estimated scaling exponent = 2.4977 
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_M30) estimated scaling exponent = 2.5659 
    TestScalingLambdMaxSymbol (EURUSD,H1)   GBPUSD (PERIOD_H1) estimated scaling exponent = 2.5524  

    GBPUSD 가격 시리즈에서도 유사한 상황이 관찰됩니다(그림 6).

    그림 6. 다양한 GBPUSD 시간 프레임에 대한 λmax의 멱법칙 의존성

    그림 6. GBPUSD의 다양한 시간 프레임에 대한 λmax의 멱법칙 의존성


    검토 대상인 EURUSD, USDJPY, GBPUSD 시리즈의 경우 추정된 지수 값 또한 2.5에 가깝습니다.

    다양한 차트 주기와 통화쌍에 걸쳐 함수 λmax의 로그-로그 스케일에서의 선형 관계는 λmax가 관측치 수에 대해 멱법칙 의존성을 나타낸다는 것을 의미합니다.


    3.3.3. 스케일링의 실질적인 의미

    λmax에 대한 멱법칙 의존성의 존재는 중요한 실제적 의미를 지닙니다.

    λmax ∝ T^2.5이므로 λ의 절댓값은 다음과 같은 요소에 크게 의존합니다:

    1. 데이터 윈도우의 길이,
    2. 기간,
    3. 시계열의 규모.

    따라서 λ의 절댓값을 사용하는 것은 실제 사용시 불편합니다.

    훨씬 더 견고한 접근 방식은 상대적 매개변수 λ=c⋅λmax를 사용하는 것이며 여기서 0<c<1입니다.

    그러한 접근 방식:

    • 정규화 매개변수를 스케일 불변으로 만듭니다.
    • 서로 다른 상품 간의 매개변수 전송을 간소화합니다.
    • 서로 다른 기간에 걸쳐 동일한 설정을 사용하도록 합니다.

    이러한 이유로 이후의 모든 예시에서는 매개변수 λ를 λmax 단위로 지정합니다.


    3.4. L1 추세 지표

    이 섹션에서는 세 가지 유형의 지표를 살펴봅니다:

    • 종가를 기준으로 한 L1 추세 계산;
    • L1 추세의 선형 성장 계수(기울기) 계산;
    • L1 추세 기울기의 부호 계산;
    이러한 지표는 추세 분해의 시각적 분석에 사용될 수 있으며 매매 전략에 사용할 정규화 매개변수 λ의 적절한 값을 식별하는 데 도움이 될 수 있습니다.

    3.4.1. L1TrendFilter.mq5 - L1 추세 지표

    이 예시에서는 지정된 개수의 바(예시에서는 BarsToShow = 1000)에 대한 종가를 사용하여 L1 필터를 계산하고 람다 계수를 λmax 단위로 지정합니다.

    이 계산은 L1TrendFilter(relative = true) 메서드 호출을 사용하며 여기서 매개변수 λ는 λmax 단위로 정의됩니다. 지표 값은 차트 창에 직접 표시됩니다.

    L1TrendFilter.mq5 지표의 코드는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                                                L1TrendFilter.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_chart_window
    #property indicator_buffers 1
    #property indicator_plots   1
    //---
    #property indicator_label1  "L1TrendFilter"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrDodgerBlue
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    //---
    double Trend[];
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0,Trend,INDICATOR_DATA);
       ArrayInitialize(Trend,EMPTY_VALUE);
    //---
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
    //--- check bars
       static bool warned=false;
       if(rates_total < BarsToShow)
         {
          if(!warned)
            {
             Print("Waiting for enough bars: ",BarsToShow);
             warned=true;
            }
          ArrayInitialize(Trend,EMPTY_VALUE);
          return(0);
         }
    //--- check new bar
       static datetime last_bar_time=0;
       bool new_bar=(time[0]!=last_bar_time);
       bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
       if(!need_recalc)
          return(prev_calculated);
       last_bar_time=time[0];
    //--- range
       int start=rates_total-BarsToShow;
    //--- hide old bars
       for(int i=0; i<start; i++)
          Trend[i]=EMPTY_VALUE;
    //---
       int data_count=BarsToShow;
    //--- copy Close
       vector<double> DataClose;
       DataClose.Resize(data_count);
       for(int i=0; i<data_count; i++)
          DataClose[i]=close[start+i];
    //--- lambda max
       double lambda_max=0.0;
       bool res=DataClose.L1TrendFilterLambdaMax(lambda_max);
       if(res)
         {
          PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f",
                      lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda);
         }
    //--- L1 trend filtering
       vector<double> filtered_data;
       filtered_data.Resize(data_count);
       if(DataClose.L1TrendFilter(CoefLambda,true,filtered_data))
         {
          for(int i=0; i<data_count; i++)
             Trend[start+i]=filtered_data[i];
         }
    //---
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    그림 7은 CoefLambda = 0.015로 L1TrendFilter.mq5 지표를 계산하는 예시를 보여줍니다.


    그림 7. CoefLambda = 0.015일 때 L1TrendFilter.mq5 지표 계산 예시

    그림 7. CoefLambda = 0.015일 때 L1TrendFilter.mq5 지표 계산 예시


    비교를 위해 서로 다른 정규화 매개변수를 사용하여 여러 변형을 계산할 수 있습니다.

    그림 8은 CoefLambda = 0.015, CoefLambda = 0.025 및 CoefLambda = 0.055 매개변수를 사용한 계산 결과를 보여줍니다.


    그림 8. 서로 다른CoefLambda 값을 사용한 L1TrendFilter.mq5 지표 계산 예시

    그림 8. CoefLambda 값을 다르게 하여 L1TrendFilter.mq5 지표를 계산하는 예시.



    3.4.2. L1TrendFilterSlope.mq5 - L1 추세 역학 지표

    추세 기울기를 표시하려면 L1TrendFilter 지표 값의 증분값을 사용할 수 있습니다.

    예를 들어 별도의 창에 값을 표시하는 L1TrendFilterSlope 지표를 생각해 보세요.

    지표의 코드는 아래에 있습니다.

    //+------------------------------------------------------------------+
    //|                                           L1TrendFilterSlope.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    //---
    #property indicator_label1  "L1TrendFilterSlope"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrDodgerBlue
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    //---
    double Trend[];
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0,Trend,INDICATOR_DATA);
       ArrayInitialize(Trend,EMPTY_VALUE);
    //---
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
    //--- check bars
       static bool warned=false;
       if(rates_total < BarsToShow)
         {
          if(!warned)
            {
             Print("Waiting for enough bars: ",BarsToShow);
             warned=true;
            }
          ArrayInitialize(Trend,EMPTY_VALUE);
          return(0);
         }
    //--- check  new bar
       static datetime last_bar_time=0;
       bool new_bar=(time[0]!=last_bar_time);
       bool need_recalc= (prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
       if(!need_recalc)
          return(prev_calculated);
       last_bar_time=time[0];
    //---
       int start=rates_total-BarsToShow;
       int data_count=BarsToShow;
    //--- hide old bars
       for(int i=0;i<start;i++)
          Trend[i]=EMPTY_VALUE;
    //--- copy Close
       vector<double> DataClose;
       DataClose.Resize(data_count);
       for(int i=0;i<data_count;i++)
          DataClose[i]=close[start+i];
    //--- lambda max
       double lambda_max=0.0;
       if(DataClose.L1TrendFilterLambdaMax(lambda_max))
         {
          PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f",
                      lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda);
         }
    //--- L1 filtering
       vector<double> filtered_data;
       filtered_data.Resize(data_count);
       bool res=DataClose.L1TrendFilter(CoefLambda,true,filtered_data);
       if(res)
         {
          //--- slope (first difference)
          for(int i=1; i<data_count; i++)
            {
             double delta=filtered_data[i]-filtered_data[i-1];
             Trend[start+i]=delta;
            }
          //--- copy first element
          Trend[start]=Trend[start+1];
         }
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    L1TrendFilter.mq5 및 L1TrendFilterSlope.mq5 지표를 결합한 결과는 그림 9에 나와 있습니다.

    그림 9. CoefLambda = 0.015일 때 L1TrendFilter.mq5 및 L1TrendFilterSlope.mq5 지표 계산 예시

    그림 9. CoefLambda = 0.015일 때 L1TrendFilter.mq5 및 L1TrendFilterSlope.mq5 지표 계산 예시


    3.4.3. L1TrendFilterSlopeSign.mq5 - L1 추세 방향 지표

    마찬가지로 L1TrendFilterSlope.mq5 지표의 증가량의 부호를 표시하는 지표를 계산할 수 있습니다.

    L1TrendFilterSlopeSign.mq5 지표의 코드:

    //+------------------------------------------------------------------+
    //|                                       L1TrendFilterSlopeSign.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    //---
    #property indicator_label1  "L1TrendFilterSlope"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrDodgerBlue
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    //---
    double Trend[];
    //+------------------------------------------------------------------+
    //| Signum                                                           |
    //+------------------------------------------------------------------+
    double Signum(const double value)
      {
       return((value>0)-(value<0));
      }
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0,Trend,INDICATOR_DATA);
       ArrayInitialize(Trend,EMPTY_VALUE);
    //---
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
    //--- check bars
       static bool warned=false;
       if(rates_total < BarsToShow)
         {
          if(!warned)
            {
             Print("Waiting for enough bars: ",BarsToShow);
             warned=true;
            }
          ArrayInitialize(Trend,EMPTY_VALUE);
          return(0);
         }
    //--- check new bar
       static datetime last_bar_time=0;
       bool new_bar=(time[0]!=last_bar_time);
       bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
       if(!need_recalc)
          return(prev_calculated);
       last_bar_time=time[0];
    //---
       int start=rates_total-BarsToShow;
       int data_count=BarsToShow;
    //--- hide old bars
       for(int i=0; i<start; i++)
          Trend[i]=EMPTY_VALUE;
    //--- copy Close
       vector<double> DataClose;
       DataClose.Resize(data_count);
       for(int i=0; i<data_count; i++)
          DataClose[i]=close[start+i];
    //--- lambda max
       double lambda_max=0.0;
       bool res=DataClose.L1TrendFilterLambdaMax(lambda_max);
       if(res)
         {
          PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f",
                      lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda);
         }
    //--- L1 filtering
       vector<double> filtered_data;
       filtered_data.Resize(data_count);
       res=DataClose.L1TrendFilter(CoefLambda,true,filtered_data);
       if(res)
         {
          Trend[start]=0;
          for(int i=1; i<data_count; i++)
            {
             double delta=filtered_data[i]-filtered_data[i-1];
             Trend[start+i]=Signum(delta);
            }
         }
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    세 가지 지표를 모두 결합한 예는 그림 10에 나와 있습니다(동일한 계수 값 CoefLambda = 0.015가 사용되었습니다).

    그림 10. CoefLambda = 0.015일 때 L1TrendFilter.mq5, L1TrendFilterSlope.mq5 및 L1TrendFilterSlopeSign.mq5 지표 계산 예시

    그림 10. CoefLambda = 0.015일 때 L1TrendFilter.mq5, L1TrendFilterSlope.mq5 및 L1TrendFilterSlopeSign.mq5 지표 계산 예시




    3.4.4. L1 추세를 기반으로 한 변동성 지표

    이 섹션에서는 L1 추세를 기반으로 한 금융 상품의 변동성을 평가하도록 설계된 지표를 제시합니다.

    이러한 도구들을 통해 시장의 불안정성과 안정기를 파악하고 현재 시장 동향을 분석하며 한층 더 정보에 기반한 매매 결정을 내릴 수 있습니다.

    이 섹션에서 살펴보는 지표는 다음과 같습니다:

    • L1Volatility.mq5 — L1 추세 대비 잔여 변동성;
    • L1VolatilitySmoothed.mq5 — 평활화된 잔여 변동성;
    • L1VolatilityAbsolute.mq5 — 절대 변동성;
    • L1VolatilityNormalized.mq5 — 정규화된 변동성;
    • L1VolatilityNormalizedSmoothed.mq5 — 평활화된 정규화 변동성;
    • L1VolatilityRegime.mq5 — 변동성을 기반으로 한 시장 체제 감지.

    모든 지표는 통합된 L1 추세 프레임워크를 기반으로 구축됩니다. 이를 통해 분석의 일관성을 보장하고 얻어진 결과의 해석을 단순화할 수 있습니다.

    이러한 지표를 사용하면 변동성이 높거나 낮은 기간을 시각적으로 파악할 수 있을 뿐만 아니라 현재 시장 상황(횡보, 추세, 확장 또는 공황)을 판단할 수 있습니다.

    결과적으로 트레이더는 시장 상황에 맞춰 매매 전략을 조정할 수 있습니다. 예를 들어 변동성이 낮은 시기에는 보다 보수적인 접근 방식을 적용하고 시장 움직임이 강한 시기에는 보다 적극적인 전략을 사용할 수 있습니다.


    3.4.4.1. L1Volatility.mq5 — L1 변동성 지표

    이 지표는 종가와 L1 추세의 해당 값 간의 차이를 계산하여 잔여 변동성을 산출합니다.

    이러한 접근 방식을 통해 불안정한 시장 기간을 파악하고 정확한 진입 또는 청산 시점을 파악할 수 있습니다.

    시각적으로 해당 지표는 별도의 차트 창에 주황색 선으로 표시됩니다.

    지표는 다음과 같은 데 도움이 됩니다:

    • L1 추세에서 가격 편차를 평가하고 시장 움직임의 강도를 측정합니다.
    • 보다 정확한 위험 관리를 위해 지역적 변동성의 급등을 감지합니다.
    • 동일한 시간 범위 내에서 서로 다른 상품의 변화 양상을 비교하십시오.

    이 지표는 전반적인 추세 맥락을 놓치지 않으면서 단기적인 변동성의 변화를 모니터링해야 하는 시스템에서 특히 유용합니다.

    L1Volatility.mq5 지표의 코드는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                                                 L1Volatility.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    //---
    #property indicator_label1  "L1Volatility"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrOrangeRed
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    //---
    double Volatility[];
    //---
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0,Volatility,INDICATOR_DATA);
       ArrayInitialize(Volatility,EMPTY_VALUE);
    //---
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
    //--- check bars
       static bool warned=false;
       if(rates_total<BarsToShow)
         {
          if(!warned)
            {
             Print("Waiting for enough bars: ",BarsToShow);
             warned=true;
            }
          ArrayInitialize(Volatility,EMPTY_VALUE);
          return(0);
         }
    //--- check new bar
       static datetime last_bar_time=0;
       bool new_bar=(time[0]!=last_bar_time);
       bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
       if(!need_recalc)
          return(prev_calculated);
       last_bar_time=time[0];
    //---
       int start=rates_total-BarsToShow;
       int data_count=BarsToShow;
    //--- hide old bars
       for(int i=0;i<start;i++)
          Volatility[i]=EMPTY_VALUE;
    //--- copy Close
       vector<double> DataClose;
       DataClose.Resize(data_count);
       for(int i=0; i<data_count; i++)
          DataClose[i]=close[start+i];
    //--- lambda max
       double lambda_max=0.0;
       bool res=DataClose.L1TrendFilterLambdaMax(lambda_max);
       if(res)
         {
          PrintFormat("lambda_max=%f (%s,%s) Coef=%f lambda=%f",
                      lambda_max,Symbol(),EnumToString(Period()),CoefLambda,lambda_max*CoefLambda);
         }
    //--- L1 filter
       vector<double> filtered_data;
       filtered_data.Resize(data_count);
       res=DataClose.L1TrendFilter(CoefLambda,true,filtered_data);
       if(res)
         {
          for(int i=0; i<data_count; i++)
            {
             double residual=close[start+i]-filtered_data[i];
             Volatility[start+i]=residual;
            }
         }
    //---
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    계산 결과는 그림 11에 나타나 있습니다.


    그림 11. L1Volatility.mq5 지표

    그림 11. L1Volatility.mq5 지표



    3.4.4.2. L1VolatilitySmoothed.mq5 — 평활화된 잔여 변동성 지표

    이 지표는 단순 이동 평균(SMA)을 적용하여 평활화한 L1Volatility의 버전을 나타냅니다.

    평활화를 통해 다음과 같은 효과를 얻을 수 있습니다:

    • 단기적인 잡음 및 이상치 감소;
    • 더욱 명확하고 이해하기 쉬운 시각화;
    • 변동성의 지속적인 변화에 집중.

    이 지표는 적응형 거래 시스템이나 추세 및 횡보장세에서 잘못된 신호를 걸러낼 때와 같이 장기적인 변동성 추세를 평가해야 하는 전략에 유용합니다.

    L1VolatilitySmoothed.mq5 지표의 코드는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                                         L1VolatilitySmoothed.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    #property indicator_label1  "L1VolatilitySmoothed"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrMediumVioletRed
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    input int    SmoothPeriod = 10;  // Smooth period
    //---
    double VolSmoothed[];
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0, VolSmoothed, INDICATOR_DATA);
       ArrayInitialize(VolSmoothed, EMPTY_VALUE);
       PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
       if(rates_total<BarsToShow)
         {
          ArrayInitialize(VolSmoothed,EMPTY_VALUE);
          return(0);
         }
    //--- recalc only on new bar
       static datetime last_bar_time = 0;
       if(time[0] == last_bar_time && prev_calculated > 0)
          return(prev_calculated);
       last_bar_time=time[0];
    //---
       int start=rates_total-BarsToShow;
       for(int i=0; i<start; i++)
          VolSmoothed[i]=EMPTY_VALUE;
    //--- copy close prices
       vector<double> price(BarsToShow);
       for(int i=0; i<BarsToShow; i++)
          price[i] = close[start+i];
       vector<double> l1(BarsToShow);
       bool res=price.L1TrendFilter(CoefLambda,true,l1);
       if(res)
         {
          //--- calculate raw volatility
          vector<double> rawVol(BarsToShow);
          for(int i=0; i<BarsToShow; i++)
             rawVol[i]=close[start+i]-l1[i];
          //--- apply simple moving average smoothing
          for(int i=0; i<BarsToShow; i++)
            {
             double sum = 0.0;
             int count = 0;
             for(int j=MathMax(0,i-SmoothPeriod+1); j<=i; j++)
               {
                sum+=rawVol[j];
                count++;
               }
             VolSmoothed[start+i]=sum/count;
            }
         }
    //---
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    그림 12는 L1Volatility.mq5 및 L1VolatilitySmoothed.mq5 지표를 모두 보여줍니다.

    그림 12. L1Volatility.mq5 및 L1VolatilitySmoothed.mq5 지표

    그림 12. L1Volatility.mq5 및 L1VolatilitySmoothed.mq5 지표



    3.4.4.3. L1VolatilityAbsolute.mq5 — 절대 변동성 지표

    이 지표는 종가와 L1 추세선 간의 차이의 절댓값을 계산합니다.

    특징 및 활용 분야:

    • 움직임 방향을 무시하고 변동 폭만 평가합니다;
    • 추세 방향과 관계없이 가격 변동 폭을 분석하는 데 편리합니다;
    • 극값 통계 및 위험 분석 기반 시스템에 유용합니다.

    절대 변동성은 가격 편차의 실제 크기를 반영하므로 트레이더는 시장 방향에 현혹되지 않고 시장 움직임의 강도를 관찰할 수 있습니다.

    L1VolatilityAbsolute.mq5 지표의 코드는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                                         L1VolatilityAbsolute.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    //---
    #property indicator_label1  "L1VolatilityAbsolute"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrOrange
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    //---
    double Vol[];
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0,Vol,INDICATOR_DATA);
       ArrayInitialize(Vol,EMPTY_VALUE);
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
    //---
       static bool warned=false;
       if(rates_total < BarsToShow)
         {
          if(!warned)
            {
             Print("Waiting bars ",BarsToShow);
             warned=true;
            }
          ArrayInitialize(Vol,EMPTY_VALUE);
          return(0);
         }
       static datetime last_bar=0;
       bool new_bar=(time[0]!=last_bar);
    //---
       if(!(prev_calculated==0 || new_bar || rates_total!=prev_calculated))
          return(prev_calculated);
    //---
       last_bar=time[0];
       int start=rates_total-BarsToShow;
       int N=BarsToShow;
       for(int i=0; i<start; i++)
          Vol[i]=EMPTY_VALUE;
    //---
       vector<double> price;
       price.Resize(N);
       for(int i=0; i<N; i++)
          price[i]=close[start+i];
       vector<double> l1;
       l1.Resize(N);
       bool res=price.L1TrendFilter(CoefLambda,true,l1);
       if(res)
         {
          for(int i=0; i<N; i++)
             Vol[start+i]=MathAbs(close[start+i]-l1[i]);
         }
    //---
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    지표 계산의 예는 그림 13에 나와 있습니다.


    그림 13. L1VolatilityAbsolute.mq5 지표

    그림 13. L1VolatilityAbsolute.mq5 지표


    3.4.4.4. L1VolatilityNormalized.mq5 — 정규화된 변동성 지표

    이 지표는 ATR(평균 실제 범위)과 L1 추세를 함께 사용하여 변동성을 정규화 합니다.

    이는 ATR 기간 동안의 평균 가격 변동폭 대비 추세으로부터 절대 가격 편차의 비율을 계산합니다. 정규화를 통해 가격-규모 의존성을 제거하여 서로 다른 금융 상품 및 차트 주기 간의 비교를 가능하게 합니다.

    다음과 같이 적용 가능합니다:

    • 상대적으로 강한 시장 움직임과 약한 시장 움직임을 파악;
    • 서로 다른 자산 간의 변동성 비교;
    • 가격 수준과 무관하게 시장 상황 평가;

    L1VolatilityNormalized.mq5 지표의 코드는 아래와 같습니다.

    //+------------------------------------------------------------------+
    //|                                       L1VolatilityNormalized.mq5 |
    //|                             Copyright 2000-2026, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    #property indicator_separate_window
    #property indicator_buffers 1
    #property indicator_plots   1
    #property indicator_label1  "L1VolatilityNormalized"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrDodgerBlue
    #property indicator_width1  2
    //---
    input int    BarsToShow = 1000;  // Number of bars to calculate L1
    input double CoefLambda = 0.015; // Lambda in lambda_max units
    //---
    double VolNormalized[];
    //---
    //+------------------------------------------------------------------+
    //| Indicator initialization function                                |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- prepare
       SetIndexBuffer(0, VolNormalized,INDICATOR_DATA);
       ArrayInitialize(VolNormalized,EMPTY_VALUE);
       PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
       IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
    //---
       return(INIT_SUCCEEDED);
      }
    //+------------------------------------------------------------------+
    //| 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[])
      {
    //--- check bars
       static bool warned=false;
       if(rates_total<BarsToShow)
         {
          if(!warned)
            {
             Print("Waiting for enough bars: ",BarsToShow);
             warned=true;
            }
          ArrayInitialize(VolNormalized,EMPTY_VALUE);
          return(0);
         }
    //--- check new bar
       static datetime last_bar_time=0;
       bool new_bar=(time[0]!=last_bar_time);
       bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
       if(!need_recalc)
          return(prev_calculated);
       last_bar_time=time[0];
       int start=rates_total-BarsToShow;
    //---
       for(int i=0; i<start; i++)
          VolNormalized[i]=EMPTY_VALUE;
    //--- copy close prices
       vector<double> price(BarsToShow);
       for(int i=0; i<BarsToShow; i++)
          price[i]=close[start+i];
    //---
       vector<double> l1(BarsToShow);
       bool res=price.L1TrendFilter(CoefLambda,true,l1);
       if(res)
         {
          //--- compute normalized volatility
          double mean=0.0;
          double stddev=0.0;
          for(int i=0; i<BarsToShow; i++)
             mean+=close[start+i]-l1[i];
          mean/=BarsToShow;
          //---
          for(int i=0; i<BarsToShow; i++)
             stddev+=MathPow(close[start+i]-l1[i]-mean,2);
          stddev=MathSqrt(stddev/BarsToShow);
          //---
          for(int i=0; i<BarsToShow; i++)
             VolNormalized[start+i]=stddev>0?(close[start+i]-l1[i])/stddev:0;
         }
    //---
       return(rates_total);
      }
    //+------------------------------------------------------------------+
    

    계산 결과는 그림 14에 나타나 있습니다.

    그림 14. L1VolatilityNormalized.mq5 지표

    그림 14. L1VolatilityNormalized.mq5 지표


      3.4.4.5. L1VolatilityNormalizedSmoothed.mq5 — 평활화된 정규화 변동성 지표

      이 지표는 지수 이동 평균(EMA) 평활화를 추가하여 정규화 방식을 확장합니다.

      장점:

      • 단기적인 잡음과 급격한 변동의 영향을 줄여줍니다;
      • 보다 명확하고 해석하기 쉬운 변동성 프로필을 생성합니다.
      • 지속적인 변동성과 현재 시장 상황을 평가하는 데 도움이 됩니다.

      이 지표는 특히 안정적인 변동성 추정이 필요한 적응형 전략, 예를 들어 트레이딩 모드를 자동으로 선택할 때 유용합니다.

      L1VolatilityNormalizedSmoothed.mq5 지표의 코드는 아래와 같습니다.

      //+------------------------------------------------------------------+
      //|                               L1VolatilityNormalizedSmoothed.mq5 |
      //|                             Copyright 2000-2026, MetaQuotes Ltd. |
      //|                                             https://www.mql5.com |
      //+------------------------------------------------------------------+
      #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
      #property link      "https://www.mql5.com"
      #property version   "1.00"
      #property indicator_separate_window
      #property indicator_buffers 1
      #property indicator_plots   1
      
      #property indicator_label1  "L1VolatilityNormalizedSmoothed"
      #property indicator_type1   DRAW_LINE
      #property indicator_color1  clrDeepSkyBlue
      #property indicator_width1  2
      //---
      input int    BarsToShow   = 1000;  // Number of bars to calculate L1
      input double CoefLambda   = 0.015; // Lambda in lambda_max units
      input int    SmoothPeriod = 10;    // EMA smoothing period (1=no smoothing)
      //---
      double NormVolSmooth[];
      //+------------------------------------------------------------------+
      //| Indicator initialization function                                |
      //+------------------------------------------------------------------+
      int OnInit()
        {
      //--- prepare
         SetIndexBuffer(0,NormVolSmooth,INDICATOR_DATA);
         ArrayInitialize(NormVolSmooth,EMPTY_VALUE);
         PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
         IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
      //---
         return(INIT_SUCCEEDED);
        }
      //+------------------------------------------------------------------+
      //| 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[])
        {
      //--- check bars
         static bool warned=false;
         if(rates_total < BarsToShow)
           {
            if(!warned)
              {
               Print("Waiting for enough bars: ",BarsToShow);
               warned=true;
              }
            ArrayInitialize(NormVolSmooth,EMPTY_VALUE);
            return(0);
           }
      //--- check  new bar
         static datetime last_bar_time=0;
         bool new_bar=(time[0]!=last_bar_time);
         bool need_recalc= (prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
         if(!need_recalc)
            return(prev_calculated);
         last_bar_time=time[0];
         int start=rates_total-BarsToShow;
      //---
         for(int i=0; i<start; i++)
            NormVolSmooth[i]=EMPTY_VALUE;
      //--- copy close prices
         vector<double> price(BarsToShow);
         for(int i=0; i<BarsToShow; i++)
            price[i]=close[start+i];
      //---
         vector<double> l1(BarsToShow);
         bool res=price.L1TrendFilter(CoefLambda,true,l1);
         if(res)
           {
            //--- compute normalized volatility
            vector<double> VolNormalized(BarsToShow);
            double mean = 0, stddev = 0;
            for(int i=0; i<BarsToShow; i++)
               mean += close[start+i]-l1[i];
            mean /= BarsToShow;
            //---
            for(int i=0; i<BarsToShow; i++)
               stddev += MathPow(close[start+i]-l1[i]-mean,2);
            stddev = MathSqrt(stddev/BarsToShow);
            //---
            for(int i=0; i<BarsToShow; i++)
               VolNormalized[i]=stddev>0 ? (close[start+i]-l1[i])/stddev: 0;
            //--- EMA smoothing
            vector<double> Smooth(BarsToShow);
            double alpha=(SmoothPeriod<=1) ? 1.0: 2.0/(SmoothPeriod+1.0);
            //---
            Smooth[0] = VolNormalized[0];
            for(int i=1; i<BarsToShow; i++)
               Smooth[i]=alpha*VolNormalized[i]+(1.0-alpha)*Smooth[i-1];
            //--- copy to indicator buffer
            for(int i=0; i<BarsToShow; i++)
               NormVolSmooth[start+i]=Smooth[i];
           }
      //---
         return(rates_total);
        }
      //+------------------------------------------------------------------+
      

      계산 결과는 그림 15에 나타나 있습니다.

      그림 15. L1VolatilityNormalized.mq5 및 L1VolatilityNormalizedSmoothed.mq5지표

      그림 15. L1VolatilityNormalized.mq5 및 L1VolatilityNormalizedSmoothed.mq5 지표



      3.4.4.6. L1VolatilityRegime.mq5 — 시장 체제 감지 지표

      이 지표는 정규화 및 평활화된 변동성을 기반으로 현재 시장 상황을 분류하여 네 가지 종류의 시장 상태를 식별합니다.

      지표 특징:

      • 완전히 자율적으로 작동하며 외부 데이터가 필요하지 않습니다;
      • 적응형 전략을 위한 시장 역학에 대한 명확한 시각화를 제공합니다;
      • LowVolThresh 및 HighVolThresh 임계값 매개변수는 다양한 상품 및 기간에 맞게 조정될 수 있습니다.
      값 체제 설명
      0 Range
      변동성이 낮고 횡보하는 시장
      1 Trend
      적당한 변동성, 추세의 존재
      2 Expansion
      강한 움직임, 시장 확장
      3 Panic 극심한 변동성, 급격한 움직임

      표 3. L1VolatilityRegime.mq5 지표의 체제

      • 현재 시장 상황을 신속하게 파악합니다.
      • 현재 상황에 맞춰 거래 전략을 조정합니다.
      • 극단적인 변동 시 위험을 줄이고 거래 효율성을 높입니다.

      L1VolatilityRegime.mq5 지표의 코드는 아래와 같습니다.

      //+------------------------------------------------------------------+
      //|                                           L1VolatilityRegime.mq5 |
      //|                             Copyright 2000-2026, MetaQuotes Ltd. |
      //|                                             https://www.mql5.com |
      //+------------------------------------------------------------------+
      #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
      #property link      "https://www.mql5.com"
      #property version   "1.00"
      #property indicator_separate_window
      #property indicator_buffers 1
      #property indicator_plots   1
      //---
      #property indicator_label1  "L1 Volatility Regime"
      #property indicator_type1   DRAW_LINE
      #property indicator_color1  clrRoyalBlue
      #property indicator_width1  2
      //--- input parameters
      input int    BarsToShow    = 1000;  // Number of bars to calculate L1
      input double CoefLambda    = 0.015; // Lambda in lambda_max units
      input int    ATRPeriod     = 14;    // ATR period
      input int    SmoothPeriod  = 10;    // Smooth period
      input double L1MoveThresh  = 0.0;   // Move volatility
      input double LowVolThresh  = 0.5;   // Low volatility
      input double HighVolThresh = 1.5;   // High volatility
      input double PanicMult     = 2.0;   // Panic volatility
      //---
      double Regime[];
      //+------------------------------------------------------------------+
      //| Indicator initialization function                                |
      //+------------------------------------------------------------------+
      int OnInit()
        {
         SetIndexBuffer(0, Regime, INDICATOR_DATA);
         PlotIndexSetDouble(0,PLOT_EMPTY_VALUE, EMPTY_VALUE);
         IndicatorSetInteger(INDICATOR_DIGITS, 0);
      //---
         return(INIT_SUCCEEDED);
        }
      //+------------------------------------------------------------------+
      //| 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[])
        {
      //--- check bars
         if(rates_total<BarsToShow+ATRPeriod)
           {
            ArrayInitialize(Regime,EMPTY_VALUE);
            return(0);
           }
      //--- check new bar
         static datetime last_bar_time=0;
         bool new_bar=(time[0]!=last_bar_time);
         bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
         if(!need_recalc)
            return(prev_calculated);
         last_bar_time=time[0];
      //---
         int start=rates_total-BarsToShow;
         int count=BarsToShow;
      //---
         for(int i=0; i<start; i++)
            Regime[i]=EMPTY_VALUE;
      //---
         vector<double> DataClose(count),DataHigh(count),DataLow(count);
         for(int i=0; i<count; i++)
           {
            DataClose[i]=close[start+i];
            DataHigh[i]=high[start+i];
            DataLow[i]=low[start+i];
           }
      //---
         vector<double> L1(count);
         bool res=DataClose.L1TrendFilter(CoefLambda,true,L1);
         if(!res)
            return(prev_calculated);
      //---
         vector<double> TR(count),ATR(count);
         for(int i=0; i<count; i++)
           {
            if(i==0)
               TR[i]=DataHigh[i]-DataLow[i];
            else
              {
               double h_l=DataHigh[i]-DataLow[i];
               double h_pc=MathAbs(DataHigh[i]-DataClose[i-1]);
               double l_pc=MathAbs(DataLow[i]-DataClose[i-1]);
               TR[i]=MathMax(h_l,MathMax(h_pc,l_pc));
              }
            int from=MathMax(0,i-ATRPeriod+1);
            double sumTR=0.0;
            int n = i-from+1;
            for(int j=from; j<=i;j++)
               sumTR += TR[j];
            ATR[i]=sumTR/n;
           }
      //---
         vector<double> NormVol(count), SmoothVol(count);
         for(int i=0; i<count; i++)
            NormVol[i]=(ATR[i]>0) ? MathAbs(DataClose[i]-L1[i])/ATR[i] : 0;
         double alpha=2.0/(SmoothPeriod+1.0);
         SmoothVol[0]=NormVol[0];
         for(int i=1; i<count; i++)
            SmoothVol[i]=alpha*NormVol[i]+(1.0-alpha)*SmoothVol[i-1];
      //---
         for(int i=0; i<count; i++)
           {
            double vol=SmoothVol[i];
            double deltaL1=(i>0) ? (L1[i]-L1[i-1]): 0.0;
            if(vol<LowVolThresh)
               Regime[start+i]=0; // Range
            else
               if(vol>=LowVolThresh && vol<HighVolThresh)
                  Regime[start+i]=(MathAbs(deltaL1)>L1MoveThresh) ? 1:0; // Trend/Range
               else
                  if(vol>=HighVolThresh && vol<HighVolThresh*PanicMult)
                     Regime[start+i]=2; // Expansion
                  else
                     Regime[start+i]=3; // Panic
           }
      //---
         return(rates_total);
        }
      //+------------------------------------------------------------------+
      

      계산 결과는 그림 16에 나타나 있습니다.

      그림 16. L1VolatilityRegime.mq5 지표

      그림 16. L1VolatilityRegime.mq5 지표

        편의를 위해 색상으로 구분된 체제 시각화 버전을 사용할 수도 있습니다.

        L1VolatilityRegimeColor.mq5 지표의 코드는 아래와 같습니다.

        //+------------------------------------------------------------------+
        //|                                      L1VolatilityRegimeColor.mq5 |
        //|                             Copyright 2000-2026, MetaQuotes Ltd. |
        //|                                             https://www.mql5.com |
        //+------------------------------------------------------------------+
        #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
        #property link      "https://www.mql5.com"
        #property version   "1.00"
        #property indicator_separate_window
        #property indicator_buffers 4
        #property indicator_plots   4
        //---
        #property indicator_label1  "Range"
        #property indicator_type1   DRAW_LINE
        #property indicator_color1  clrDodgerBlue
        #property indicator_width1  2
        //---
        #property indicator_label2  "Trend"
        #property indicator_type2   DRAW_LINE
        #property indicator_color2  clrLime
        #property indicator_width2  2
        //---
        #property indicator_label3  "Expansion"
        #property indicator_type3   DRAW_LINE
        #property indicator_color3  clrOrange
        #property indicator_width3  2
        //---
        #property indicator_label4  "Panic"
        #property indicator_type4   DRAW_LINE
        #property indicator_color4  clrRed
        #property indicator_width4  2
        //--- input parameters
        input int    BarsToShow    = 1000;  // Number of bars to calculate L1
        input double CoefLambda    = 0.015; // Lambda in lambda_max units
        input int    ATRPeriod     = 14;    // ATR period
        input int    SmoothPeriod  = 10;    // Smooth period
        input double L1MoveThresh  = 0.0;   // Move volatility
        input double LowVolThresh  = 0.5;   // Low volatility
        input double HighVolThresh = 1.5;   // High volatility
        input double PanicMult     = 2.0;   // Panic volatility
        //--- buffers
        double Regime[];
        double BufRange[], BufTrend[], BufExpansion[], BufPanic[];
        //+------------------------------------------------------------------+
        //| Indicator initialization function                                |
        //+------------------------------------------------------------------+
        int OnInit()
          {
           SetIndexBuffer(0,BufRange,INDICATOR_DATA);
           SetIndexBuffer(1,BufTrend,INDICATOR_DATA);
           SetIndexBuffer(2,BufExpansion,INDICATOR_DATA);
           SetIndexBuffer(3,BufPanic,INDICATOR_DATA);
        //---
           PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
           PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,EMPTY_VALUE);
           PlotIndexSetDouble(2,PLOT_EMPTY_VALUE,EMPTY_VALUE);
           PlotIndexSetDouble(3,PLOT_EMPTY_VALUE,EMPTY_VALUE);
        //---
           IndicatorSetInteger(INDICATOR_DIGITS,0);
           return(INIT_SUCCEEDED);
          }
        //+------------------------------------------------------------------+
        //| 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[])
          {
           if(rates_total<BarsToShow+ATRPeriod)
             {
              ArrayInitialize(Regime,EMPTY_VALUE);
              ArrayInitialize(BufRange,EMPTY_VALUE);
              ArrayInitialize(BufTrend,EMPTY_VALUE);
              ArrayInitialize(BufExpansion,EMPTY_VALUE);
              ArrayInitialize(BufPanic,EMPTY_VALUE);
              return(0);
             }
        //--- new bars
           static datetime last_bar_time=0;
           bool new_bar=(time[0]!=last_bar_time);
           bool need_recalc=(prev_calculated==0) || new_bar || (rates_total!=prev_calculated);
           if(!need_recalc)
              return(prev_calculated);
           last_bar_time=time[0];
        //---
           int start=rates_total-BarsToShow;
           int count=BarsToShow;
        //---
           ArrayResize(Regime,rates_total);
           for(int i=0;i<start;i++)
              Regime[i]=EMPTY_VALUE;
        //---
           vector<double> DataClose(count),DataHigh(count),DataLow(count);
           for(int i=0; i<count; i++)
             {
              DataClose[i]=close[start+i];
              DataHigh[i]=high[start+i];
              DataLow[i]=low[start+i];
             }
        //---
           vector<double> L1(count);
           bool res=DataClose.L1TrendFilter(CoefLambda,true,L1);
           if(!res)
              return(prev_calculated);
        //---
           vector<double> TR(count),ATR(count);
           for(int i=0; i<count; i++)
             {
              if(i==0)
                 TR[i]=DataHigh[i]-DataLow[i];
              else
                {
                 double h_l = DataHigh[i]-DataLow[i];
                 double h_pc = MathAbs(DataHigh[i]-DataClose[i-1]);
                 double l_pc = MathAbs(DataLow[i]-DataClose[i-1]);
                 TR[i] = MathMax(h_l, MathMax(h_pc, l_pc));
                }
              int from=MathMax(0,i-ATRPeriod+1);
              double sumTR=0;
              int n=i-from+1;
              for(int j=from; j<=i; j++)
                 sumTR+=TR[j];
              ATR[i]=sumTR/n;
             }
        //---
           vector<double> NormVol(count), SmoothVol(count);
           for(int i=0;i<count;i++)
              NormVol[i]=(ATR[i]>0) ? MathAbs(DataClose[i]-L1[i])/ATR[i] : 0;
        //---
           double alpha=2.0/(SmoothPeriod+1.0);
           SmoothVol[0]=NormVol[0];
           for(int i=1; i<count; i++)
              SmoothVol[i]=alpha*NormVol[i]+(1.0-alpha)*SmoothVol[i-1];
        //--- calc Regime[]
           for(int i=0; i<count; i++)
             {
              double vol=SmoothVol[i];
              double deltaL1=(i>0) ? (L1[i]-L1[i-1]):0.0;
              if(vol<LowVolThresh)
                 Regime[start+i]=0;
              else
                 if(vol<HighVolThresh)
                    Regime[start+i]=(MathAbs(deltaL1)>L1MoveThresh) ? 1:0;
                 else
                    if(vol<HighVolThresh*PanicMult)
                       Regime[start+i]=2;
                    else
                       Regime[start+i]=3;
             }
        //--- buffers
           for(int i=0; i<rates_total; i++)
             {
              BufRange[i] = (Regime[i]==0) ? Regime[i]: EMPTY_VALUE;
              BufTrend[i] = (Regime[i]==1) ? Regime[i]: EMPTY_VALUE;
              BufExpansion[i] = (Regime[i]==2) ? Regime[i]: EMPTY_VALUE;
              BufPanic[i] = (Regime[i]==3) ? Regime[i]: EMPTY_VALUE;
             }
        //---
           return(rates_total);
          }
        //+------------------------------------------------------------------+
        

        종합한 계산 결과는 그림 17에 나타나 있습니다.

        그림 17. L1VolatilityRegime.mq5 및 L1VolatilityRegimeColor.mq5 지표

        그림 17. L1VolatilityRegime.mq5 및 L1VolatilityRegimeColor.mq5 지표


        그림 18~20은 EURGBP, AUDCAD 및 CHFJPY에 대한 모든 변동성 지표를 종합적으로 나타낸 예시입니다.

        그림 18. EURGBP의 변동성 지표

        그림 18. EURGBP 변동성 지표


        그림 19. AUDCAD의 변동성 지표

        그림 19. AUDCAD의 변동성 지표


        그림 20. CHFJPY의 변동성 지표

        그림 20. CHFJPY 변동성 지표


        3.5. 트레이딩 전략에서 L1 추세 활용

        이 섹션에서는 이동평균, MACD, ADX 및 EMA를 활용한 매매 전략과 매매 신호 필터 적용에 대한 다양한 옵션을 살펴봅니다.

        매매 신호에 필터를 추가하면 트레이딩 시스템의 성능을 향상시킬 수 있습니다. 제시된 Expert Advisor에서 필터 사용의 효과를 분석하기 위해 잔고 및 평가 금액 데이터(각 새로운 캔들마다)를 별도의 파일에 저장하고 파이썬 스크립트를 사용하여 다양한 모드에서 트레이딩 시스템의 결과를 시각화 할 것입니다.

        제시된 모든 Expert Advisor는 동일한 아키텍처를 가지고 있습니다.

        L1 추세와 매매 신호를 일치시키는 일반 원칙

        • 오픈 필터(L1FilterOpen = true)를 사용하면 지배적인 추세 방향으로만 거래를 시작할 수 있습니다.
        • 클로즈 필터(L1FilterClose = true)는 강한 추세 동안 포지션을 유지하는 데 도움이 되며 단기적인 조정 시 조기 청산을 줄여줍니다.

        입력 매개변수(모든 Expert Advisor에 공통):

        //--- L1 filter parameters
        input int    L1TotalBars    = 1000;   // Total bars for L1 filter
        input bool   L1FilterOpen   = false;  // Use filter for Open
        input bool   L1FilterClose  = false;  // Use filter for Close
        input double L1CoefLambda   = 0.2;    // Lambda in lambda_max units
        //--- save statistics
        input bool   SaveStatistics = false;  // Save statistics to file

        잔고 및 자산 데이터

        입력 매개변수 SaveStatistics를 사용하면 시간, 종가, 잔고, 평가 금액 등의 현재 값을 새 캔들이 생성될 때마다 terminal_data_folder\Tester\Agent-127.0.0.1-3000\MQL5\Files 파일에 저장할 수 있습니다.

        데이터 저장 함수는 OnTick() 함수 내부에서 호출되며 입력 매개변수 bool SaveStatistics의 값에 따라 달라집니다.

        //+------------------------------------------------------------------+
        //| Expert OnTick function                                           |
        //+------------------------------------------------------------------+
        void OnTick()
          {
        //--- trade only at new bar
           if(!IsNewBar())
              return;
        //--- check trade conditions
           if(SelectPosition())
              CheckForClose();
           else
              CheckForOpen();
        //--- save account statistics
           if(SaveStatistics)
              SaveAccountStatistics();
          }

        저장된 파일 이름의 접두사는 L1FilterOpen과 L1FilterClose의 조합에 따라 결정됩니다.

        파일 이름은 전략과 심볼에 따라 달라지며 Expert Advisor의 초기화 함수에서 생성됩니다.

        //+------------------------------------------------------------------+
        //| PrepareStrategyFileName                                          |
        //+------------------------------------------------------------------+
        string PrepareStrategyFileName(string strategy_name)
          {
           int v=0;
           if(L1FilterOpen)
              v=v | 1;
        //---
           if(L1FilterClose)
              v=v | 2;
        //---
           string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt";
           return filename;
          }
        //+------------------------------------------------------------------+
        //| Expert initialization                                            |
        //+------------------------------------------------------------------+
        int OnInit()
          {
        //--- prepare filename
           ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName);
        //--- delete old file if exists
           if(FileIsExist(ExtStrategyFileName))
              FileDelete(ExtStrategyFileName);
        //---
           return INIT_SUCCEEDED;
          }

        다양한 모드에서 매매 신호 필터 적용:

        //+------------------------------------------------------------------+
        //| Save account statistics to file                                  |
        //+------------------------------------------------------------------+
        void SaveAccountStatistics()
          {
        //--- check file name
           if(ExtStrategyFileName=="")
              return;
        //---
           int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI);
           if(file==INVALID_HANDLE)
             {
              Print("File open error: ",GetLastError());
              return;
             }
        //--- append
           FileSeek(file,0,SEEK_END);
        //--- account data
           double balance     = AccountInfoDouble(ACCOUNT_BALANCE);
           double equity      = AccountInfoDouble(ACCOUNT_EQUITY);
           double margin      = AccountInfoDouble(ACCOUNT_MARGIN);
           double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
           double margin_lvl  = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
        //--- volume
           double volume=0.0;
           if(PositionSelect(_Symbol))
              volume=PositionGetDouble(POSITION_VOLUME);
        //--- time
           datetime t[1];
           if(CopyTime(_Symbol,_Period,0,1,t)<=0)
             {
              FileClose(file);
              return;
             }
           double current_close[1];
           if(CopyClose(_Symbol,_Period,0,1,current_close)<=0)
             {
              FileClose(file);
              return;
             }
           string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS),
                                    balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]);
        //---
           FileWrite(file,line);
        //---
           FileClose(file);
          }

        다양한 모드에서 거래 신호 필터 적용

        전략 테스터에서 4가지 조합 모두를 사용하여 Expert Advisor를 순차적으로 실행한 결과:

        1. L1FilterOpen = false, L1FilterClose = false (필터 없이 거래);
        2. L1FilterOpen = true, L1FilterClose = false (입력 필터);
        3. L1FilterOpen = false, L1FilterClose = true (필터 종료);
        4. L1FilterOpen = true, L1FilterClose = true (진입 및 종료 필터).

        다음과 같은 파일을 생성합니다: 0_MA_EURUSD.txt, 1_MA_EURUSD.txt, 2_MA_EURUSD.txt, 3_MA_EURUSD.txt.

        이 파일에는 잔고/평가금액 데이터가 포함되어 있으며 L1 추세 정렬을 사용하여 매매 신호 필터링의 효율성을 비교할 수 있습니다.

        데이터 시각화

        여러 차트를 하나로 합치려면 4개의 파일을 별도의 폴더(예: "C:\data\")에 복사한 다음 파이썬 스크립트를 실행하세요.

        import pandas as pd
        import matplotlib.pyplot as plt
        import os
        
        # --- folder for charts
        output_dir = "C:\\data\\charts\\"
        os.makedirs(output_dir, exist_ok=True)
        
        symbol = "EURUSD" 
        name_strategy = "MA"
        file_strategy = name_strategy+"_"+symbol 
        title_strategy = " ("+symbol+" "+name_strategy+" strategy+filters)"
        file_prefix = symbol+"_"+name_strategy+"_"
        
        # --- files
        files = [
            "C:\\data\\0_"+file_strategy+".txt",
            "C:\\data\\1_"+file_strategy+".txt",
            "C:\\data\\2_"+file_strategy+".txt",
            "C:\\data\\3_"+file_strategy+".txt"
        ]
        
        # --- labels
        labels = [
            "No filters",
            "Open L1 filter",
            "Close L1 filter",
            "Open+Close L1 filter"
        ]
        
        # --- load data
        def load_file(filename):
            df = pd.read_csv(
                filename,
                sep=";",
                header=None,
                names=[
                    "time",
                    "balance",
                    "equity",
                    "margin",
                    "free_margin",
                    "margin_level",
                    "volume",
                    "close"
                ]
            )
            df["time"] = pd.to_datetime(df["time"])
            return df
        
        # --- close price chart
        plt.figure(figsize=(10,6), dpi=100)
        for file, label in zip(files, labels):
            df = load_file(file)
        plt.plot(df["time"], df["close"], color='gray')
        plt.title(symbol+" Close Price")
        plt.xlabel("Time")
        plt.ylabel("closing price")
        plt.legend()
        plt.grid()
        plt.tight_layout()
        plt.savefig(output_dir + file_prefix+"close_price.png", dpi=100)
        plt.show()
            
        # --- balance chart
        plt.figure(figsize=(10,6), dpi=100)
        for file, label in zip(files, labels):
            df = load_file(file)
            plt.plot(df["time"], df["balance"], label=label)
        plt.title("Balance" + title_strategy)
        plt.xlabel("Time")
        plt.ylabel("Balance")
        plt.legend()
        plt.grid()
        plt.tight_layout()
        plt.savefig(output_dir + file_prefix+"balance.png", dpi=100)
        plt.show()
        plt.close()
        
        # --- equity chart
        plt.figure(figsize=(10,6), dpi=100)
        for file, label in zip(files, labels):
            df = load_file(file)
            plt.plot(df["time"], df["equity"], label=label)
        plt.title("Equity" + title_strategy)
        plt.xlabel("Time")
        plt.ylabel("Equity")
        plt.legend()
        plt.grid()
        plt.tight_layout()
        plt.savefig(output_dir + file_prefix+"equity.png", dpi=100)
        plt.show()
        plt.close()
        
        #--- balance + equity chart
        plt.figure(figsize=(10,6), dpi=100)
        for i, (file, label) in enumerate(zip(files, labels)):
        
            df = load_file(file)
            # --- get matplotlib color
            color = plt.rcParams["axes.prop_cycle"].by_key()["color"][i % 10]
            #--- equity — solid line
            plt.plot(
                df["time"],
                df["equity"],
                color=color,
                linestyle="-",
                label=f"{label} equity"
            )
            #--- balance — dashed line
            plt.plot(
                df["time"],
                df["balance"],
                color=color,
                linestyle="--",
                label=f"{label} balance"
            )
        plt.title("Balance + Equity" + title_strategy)
        plt.xlabel("Time")
        plt.ylabel("Value")
        plt.legend()
        plt.grid()
        plt.tight_layout()
        plt.savefig(output_dir+file_prefix+"balance_equity.png", dpi=100)
        plt.show()
        plt.close()


        매매 신호 필터 구현

        매개변수 L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda는 L1 추세 계산 설정과 매매 신호 필터링에서의 사용 방식 정의.

        L1 추세 계산 및 매매 신호와의 연동

        모든 Expert Advisor에서 L1 추세 분석을 사용하여 추가 필터링이 적용됩니다:

          1. 평활화된 가격 시계열은 마지막 L1TotalBars를 사용하여 생성됩니다;
          2. 추세 성장 계수 차이는 최근 두 필터링 된 값의 차이로 계산됩니다;
            델타 값이 0보다 크면 상승 추세, 0보다 작으면 하락 추세입니다.
            //+------------------------------------------------------------------+
            //| CheckTrendL1                                                     |
            //+------------------------------------------------------------------+
            double CheckTrendL1()
              {
               int max_bars=L1TotalBars;
               MqlRates rates_data[];
               ArrayResize(rates_data,max_bars);
               ArraySetAsSeries(rates_data,false);
               if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars)
                 {
                  Print("CopyRates failed for L1Trend");
                  return(0);
                 }
            //--- prepare data (close prices vector)
               int data_count=max_bars;
               vector<double> data_close;
               data_close.Resize(data_count);
               for(int i=0; i<data_count; i++)
                  data_close[i] = rates_data[i].close;
            //--- calculate L1 filter
               vector<double> data_filtered;
               data_filtered.Resize(data_count);
               double dp=0.0;
               bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered);
               if(res)
                  dp = data_filtered[data_count-1]-data_filtered[data_count-2];
            //---
               return(dp);
              }
            

            매개변수 L1CoefLambda는 λmax 단위로 지정되므로 필터링이 변동성과 캔들 개수에 대해 견고 해집니다.

            매매 진입 필터

            L1FilterOpen이 true인 경우:

            • L1 추세가 음일 경우 매수 신호는 무시됩니다;
            • L1 추세가 양일 경우 매도 신호는 무시됩니다.

            매매는 지배적인 추세 방향으로만 개시됩니다.

            //+------------------------------------------------------------------+
            //| CheckForOpen                                                     |
            //+------------------------------------------------------------------+
            void CheckForOpen()
              {
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
               if(signal == WRONG_VALUE)
                  return;
            //--- L1 filter
               if(L1FilterOpen)
                 {
                  double delta = CheckTrendL1();
                  if(signal == ORDER_TYPE_BUY && delta < 0)
                    {
                     signal = WRONG_VALUE;
                     PrintFormat("Open BUY signal cancelled by L1 trend delta=%.5f", delta);
                    }
                  if(signal == ORDER_TYPE_SELL && delta > 0)
                    {
                     signal = WRONG_VALUE;
                     PrintFormat("Open SELL signal cancelled by L1 trend delta=%.5f", delta);
                    }
                 }
            //---
               if(signal == WRONG_VALUE)
                  return;
            //---
               if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars)
                  return;
            //---
               double price = (signal==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol,SYMBOL_ASK): SymbolInfoDouble(_Symbol,SYMBOL_BID);
            //---
               ExtTrade.PositionOpen(_Symbol, signal, TradeLot, price, 0, 0);
              }


            매매 청산 필터

            L1FilterClose가 true인 경우:

            • L1 추세가 상승세를 유지하는 동안에는 반전 신호가 발생하더라도 매수 포지션은 청산되지 않습니다:
            • L1 추세가 하락세로 유지되는 동안에는 매도 포지션이 청산되지 않습니다.

            이는 강한 추세에서 포지션을 유지하는 데 도움이 되며 국지적 조정 기간 동안 조기 청산을 줄이는 데에도 효과적입니다.

            //+------------------------------------------------------------------+
            //| CheckForClose                                                    |
            //+------------------------------------------------------------------+
            void CheckForClose()
              {
            //--- check position
               if(!PositionSelect(_Symbol))
                  return;
            //--- check position magic
               if(PositionGetInteger(POSITION_MAGIC)!=MA_MAGIC)
                  return;
            //--- check trade signal
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
            //---
               long type = PositionGetInteger(POSITION_TYPE);
               bool close_signal = false;
            //---
               if(type == POSITION_TYPE_BUY && signal == ORDER_TYPE_SELL)
                  close_signal = true;
               if(type == POSITION_TYPE_SELL && signal == ORDER_TYPE_BUY)
                  close_signal = true;
            //--- check L1 filter
               if(L1FilterClose)
                 {
                  double delta = CheckTrendL1();
                  if(type == POSITION_TYPE_BUY && delta > 0)
                    {
                     close_signal = false;
                     PrintFormat("Close BUY signal cancelled by L1 trend delta=%.5f", delta);
                    }
                  if(type == POSITION_TYPE_SELL && delta < 0)
                    {
                     close_signal = false;
                     PrintFormat("Close SELL signal cancelled by L1 trend delta=%.5f", delta);
                    }
                 }
            //---
               if(close_signal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>=L1TotalBars)
                  ExtTrade.PositionClose(_Symbol,3);
              }



            3.5.1. 이동평균 거래 전략

            첫 번째 예로 고전적인 이동 평균 추세 추종 전략을 기반으로 거래하는 Expert Advisor를 생각해 보겠습니다.

            매매 신호는 종가와 이동평균선의 교차점을 기준으로 생성됩니다.

            • 매수 — 종가가 이동평균선을 아래에서 위로 돌파할 때;
            • 매도 — 종가가 이동평균선을 위에서 아래로 하향 돌파할 때.

            신호는 최근 마감된 두 개의 캔들을 사용하여 평가되므로 현재 미완료 캔들의 영향을 제거하고 허위 신호를 줄입니다.

            또한 진입 및 청산 필터는 L1 추세 설정(L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda)에 따라 적용됩니다.

            MovingAverageFilteredL1.mq5 Expert Advisor 코드:

            //+------------------------------------------------------------------+
            //|                                      MovingAverageFilteredL1.mq5 |
            //|                             Copyright 2000-2026, MetaQuotes Ltd. |
            //|                                             https://www.mql5.com |
            //+------------------------------------------------------------------+
            #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
            #property link      "https://www.mql5.com"
            #property version   "1.00"
            //--- best MovingAverage parameters for EURUSD,H1,2025
            input int    MovingPeriod   = 61;     // MA period
            input int    MovingShift    = 0;      // MA shift
            //--- trade volume
            input double TradeLot       = 0.1;    // Lot size
            //--- L1 filter parameters
            input int    L1TotalBars    = 1000;   // Total bars for L1 filter
            input bool   L1FilterOpen   = false;  // Use filter for Open
            input bool   L1FilterClose  = false;  // Use filter for Close
            input double L1CoefLambda   = 0.2;    // Lambda in lambda_max units
            //--- save statistics
            input bool   SaveStatistics = false;  // Save statistics to file
            //---
            #define MA_MAGIC 1234501
            #include <Trade\Trade.mqh>
            CTrade ExtTrade;
            int    ExtHandle = INVALID_HANDLE;
            bool   ExtHedging = false;
            string ExtStrategyName="MA";
            string ExtStrategyFileName="";
            //+------------------------------------------------------------------+
            //| Check new bar                                                    |
            //+------------------------------------------------------------------+
            bool IsNewBar()
              {
               static datetime last_time=0;
               datetime t[1];
            //---
               if(CopyTime(_Symbol,_Period,0,1,t)>0)
                 {
                  if(t[0]!=last_time)
                    {
                     last_time=t[0];
                     return(true);
                    }
                 }
               return(false);
              }
            //+------------------------------------------------------------------+
            //| CheckTrendL1                                                     |
            //+------------------------------------------------------------------+
            double CheckTrendL1()
              {
               int max_bars=L1TotalBars;
               MqlRates rates_data[];
               ArrayResize(rates_data,max_bars);
               ArraySetAsSeries(rates_data,false);
               if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars)
                 {
                  Print("CopyRates failed for L1Trend");
                  return(0);
                 }
            //--- prepare data (close prices vector)
               int data_count=max_bars;
               vector<double> data_close;
               data_close.Resize(data_count);
               for(int i=0; i<data_count; i++)
                  data_close[i] = rates_data[i].close;
            //--- calculate L1 filter
               vector<double> data_filtered;
               data_filtered.Resize(data_count);
               double dp=0.0;
               bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered);
               if(res)
                  dp = data_filtered[data_count-1]-data_filtered[data_count-2];
            //---
               return(dp);
              }
            //+------------------------------------------------------------------+
            //| GetTradeSignal                                                   |
            //+------------------------------------------------------------------+
            bool GetTradeSignal(ENUM_ORDER_TYPE &signal)
              {
               signal = WRONG_VALUE;
               MqlRates bars[];
               double ma[];
               ArraySetAsSeries(bars,true);
               ArraySetAsSeries(ma,true);
               ArrayResize(bars,2);
               ArrayResize(ma,2);
            //-- two last closed bars
               if(CopyRates(_Symbol,_Period,2,2,bars) != 2)
                 {
                  Print("CopyRates failed");
                  return(false);
                 }
               if(CopyBuffer(ExtHandle,0,2,2,ma) != 2)
                 {
                  Print("CopyBuffer failed");
                  return(false);
                 }
               double close_prev = bars[1].close;
               double close_last = bars[0].close;
               double ma_prev = ma[1];
               double ma_last = ma[0];
            //--- check MA crossover
               if(close_prev < ma_prev && close_last > ma_last)
                  signal = ORDER_TYPE_BUY;
               else
                  if(close_prev > ma_prev && close_last < ma_last)
                     signal = ORDER_TYPE_SELL;
            //--- log
            //   PrintFormat("PrevBar: time=%s close=%.5f ma=%.5f | LastBar: time=%s close=%.5f ma=%.5f | Signal=%s",
            //               TimeToString(bars[0].time,TIME_DATE|TIME_MINUTES), close_prev, ma_prev,
            //               TimeToString(bars[1].time,TIME_DATE|TIME_MINUTES), close_last, ma_last,
            //               (signal==ORDER_TYPE_BUY?"BUY":signal==ORDER_TYPE_SELL?"SELL":"NONE"));
            //---
               return(true);
              }
            //+------------------------------------------------------------------+
            //| CheckForOpen                                                     |
            //+------------------------------------------------------------------+
            void CheckForOpen()
              {
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
               if(signal == WRONG_VALUE)
                  return;
            //--- L1 filter
               if(L1FilterOpen)
                 {
                  double delta = CheckTrendL1();
                  if(signal == ORDER_TYPE_BUY && delta < 0)
                    {
                     signal = WRONG_VALUE;
                     PrintFormat("Open BUY signal cancelled by L1 trend delta=%.5f", delta);
                    }
                  if(signal == ORDER_TYPE_SELL && delta > 0)
                    {
                     signal = WRONG_VALUE;
                     PrintFormat("Open SELL signal cancelled by L1 trend delta=%.5f", delta);
                    }
                 }
            //---
               if(signal == WRONG_VALUE)
                  return;
            //---
               if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars)
                  return;
            //---
               double price = (signal==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol,SYMBOL_ASK): SymbolInfoDouble(_Symbol,SYMBOL_BID);
            //---
               ExtTrade.PositionOpen(_Symbol, signal, TradeLot, price, 0, 0);
              }
            //+------------------------------------------------------------------+
            //| CheckForClose                                                    |
            //+------------------------------------------------------------------+
            void CheckForClose()
              {
            //--- check position
               if(!PositionSelect(_Symbol))
                  return;
            //--- check position magic
               if(PositionGetInteger(POSITION_MAGIC)!=MA_MAGIC)
                  return;
            //--- check trade signal
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
            //---
               long type = PositionGetInteger(POSITION_TYPE);
               bool close_signal = false;
            //---
               if(type == POSITION_TYPE_BUY && signal == ORDER_TYPE_SELL)
                  close_signal = true;
               if(type == POSITION_TYPE_SELL && signal == ORDER_TYPE_BUY)
                  close_signal = true;
            //--- check L1 filter
               if(L1FilterClose)
                 {
                  double delta = CheckTrendL1();
                  if(type == POSITION_TYPE_BUY && delta > 0)
                    {
                     close_signal = false;
                     PrintFormat("Close BUY signal cancelled by L1 trend delta=%.5f", delta);
                    }
                  if(type == POSITION_TYPE_SELL && delta < 0)
                    {
                     close_signal = false;
                     PrintFormat("Close SELL signal cancelled by L1 trend delta=%.5f", delta);
                    }
                 }
            //---
               if(close_signal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>=L1TotalBars)
                  ExtTrade.PositionClose(_Symbol,3);
              }
            //+------------------------------------------------------------------+
            //| SelectPosition                                                   |
            //+------------------------------------------------------------------+
            bool SelectPosition()
              {
               bool res = false;
               if(ExtHedging)
                 {
                  uint total = PositionsTotal();
                  for(uint i=0; i<total; i++)
                    {
                     string sym = PositionGetSymbol(i);
                     if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==MA_MAGIC)
                       {
                        res = true;
                        break;
                       }
                    }
                 }
               else
                 {
                  if(PositionSelect(_Symbol))
                     res = (PositionGetInteger(POSITION_MAGIC)==MA_MAGIC);
                 }
               return(res);
              }
            //+------------------------------------------------------------------+
            //| Expert initialization function                                   |
            //+------------------------------------------------------------------+
            int OnInit()
              {
            //--- check parameters
               if(MovingPeriod<=0)
                 {
                  Print("Error: MovingPeriod parameter must be positive");
                  return(INIT_PARAMETERS_INCORRECT);
                 }
               ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
               ExtTrade.SetExpertMagicNumber(MA_MAGIC);
               ExtTrade.SetMarginMode();
               ExtTrade.SetTypeFillingBySymbol(_Symbol);
            //--- prepare indicator
               ExtHandle = iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
               if(ExtHandle==INVALID_HANDLE)
                 {
                  Print("Failed to create MA handle");
                  return(INIT_FAILED);
                 }
            //--- prepare filename
               ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName);
            //--- delete old file if exists
               if(FileIsExist(ExtStrategyFileName))
                  FileDelete(ExtStrategyFileName);
            //---
               return(INIT_SUCCEEDED);
              }
            //+------------------------------------------------------------------+
            //| PrepareStrategyFileName                                          |
            //+------------------------------------------------------------------+
            string PrepareStrategyFileName(string strategy_name)
              {
               int v=0;
               if(L1FilterOpen)
                  v=v | 1;
            //---
               if(L1FilterClose)
                  v=v | 2;
            //---
               string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt";
               return(filename);
              }
            //+------------------------------------------------------------------+
            //| Save account statistics to file                                  |
            //+------------------------------------------------------------------+
            void SaveAccountStatistics()
              {
            //--- check file name
               if(ExtStrategyFileName=="")
                  return;
            //---
               int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI);
               if(file==INVALID_HANDLE)
                 {
                  Print("File open error: ",GetLastError());
                  return;
                 }
            //--- append
               FileSeek(file,0,SEEK_END);
            //--- account data
               double balance     = AccountInfoDouble(ACCOUNT_BALANCE);
               double equity      = AccountInfoDouble(ACCOUNT_EQUITY);
               double margin      = AccountInfoDouble(ACCOUNT_MARGIN);
               double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
               double margin_lvl  = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
            //--- volume
               double volume=0.0;
               if(PositionSelect(_Symbol))
                  volume=PositionGetDouble(POSITION_VOLUME);
            //--- time
               datetime t[1];
               if(CopyTime(_Symbol,_Period,0,1,t)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               double current_close[1];
               if(CopyClose(_Symbol,_Period,0,1,current_close)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS),
                                        balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]);
            //---
               FileWrite(file,line);
            //---
               FileClose(file);
              }
            //+------------------------------------------------------------------+
            //| Expert OnTick function                                           |
            //+------------------------------------------------------------------+
            void OnTick()
              {
            //--- trade only at new bar
               if(!IsNewBar())
                  return;
            //--- check trade conditions
               if(SelectPosition())
                  CheckForClose();
               else
                  CheckForOpen();
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
              }
            //+------------------------------------------------------------------+
            //| Expert deinitialization function                                 |
            //+------------------------------------------------------------------+
            void OnDeinit(const int reason)
              {
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
            //---
               IndicatorRelease(ExtHandle);
              }
            //+------------------------------------------------------------------+
            

            3.5.1.1. L1 추세 필터링 효율성 평가를 위한 일반적인 방법론

            L1 필터의 효과를 평가하기 위해서는 다음 사항들이 필요합니다:

            1. 최대 수익을 창출하는 매매 전략의 최적 매개변수 조합을 찾습니다.
              가장 실적이 좋은 전략의 매매 신호를 개선하는 것이 좋습니다.

            2. L1FilterOpen/L1FilterClose 매개변수를 설정하여 필터 사용 방식을 네 가지로 변형했을 때의 테스트 결과를 살펴보십시오:
              • 필터 없이 거래하기;
              • 진입 필터를 이용한 거래;
              • 청산 필터를 이용한 거래;
              • 진입 및 청산 필터를 모두 적용한 거래.
              결과를 파일에 저장합니다(입력 매개변수 SaveStatistics=true). 생성된 파일은 별도의 폴더(예시에서는 C:\Data\)에 복사해야 합니다.

            3. 파이썬 스크립트를 실행하여 결합된 그래프를 생성합니다.

            EURUSD 통화쌍, H1 시간 프레임, 2025년 테스트 기간을 사용하는 MovingAverageFilteredL1.mq5 EA를 예로 들어 다음 단계를 살펴보십시오.

            필터 작업에서는 추세 계산에 고정된 개수의 캔들을 사용하겠습니다: L1TotalBars = 1000. 정규화 매개변수 λ는 λmax 단위로 지정되며 고정값 L1CoefLambda = 0.2를 사용합니다.

            입력 매개변수:

            //--- best MovingAverage parameters for EURUSD,H1,2025
            input int    MovingPeriod   = 64;     // MA period
            input int    MovingShift    = 0;      // MA shift
            //--- trade volume
            input double TradeLot       = 0.1;    // Lot size
            //--- L1 filter parameters
            input int    L1TotalBars    = 1000;   // Total bars for L1 filter
            input bool   L1FilterOpen   = false;  // Use filter for Open
            input bool   L1FilterClose  = false;  // Use filter for Close
            input double L1CoefLambda   = 0.2;    // Lambda in lambda_max units
            //--- save statistics
            input bool   SaveStatistics = false;  // Save statistics to file

            최적화 설정에서 심볼은 EURUSD, 시간 프레임은 H1, 테스트 기간은 2025년 1월 1일부터 2025년 12월 31일까지로 지정합니다.

            Fig.21. Optimization settings of the Expert Advisor MovingAverageFilteredL1.mq5

            그림 21. Expert Advisor MovingAverageFilteredL1.mq5의 최적화 설정


            빠른 최적화를 위해 "1분 OHLC" 모드를 사용하겠습니다(이 전략은 새로운 캔들에만 적용되므로 이 근사치는 허용 가능합니다). 최적화 알고리즘으로는 "최대 균형" 설정으로 "빠른 유전 기반 알고리즘"을 선택합니다.

              그림 22. Expert Advisor MovingAverageFilteredL1.mq5의 최적화 매개 변수

            그림 22. Expert Advisor MovingAverageFilteredL1.mq5의 최적화 매개변수


            이 단계에서 우리의 목표는 매매 전략의 최적 매개변수를 찾는 것이므로 최적화 매개변수에서 모든 필터와 파일 저장 기능을 비활성화합니다.

            간단하게 여기서는 "MA 주기"라는 하나의 매개변수만 1부터 800까지의 범위에서 1씩 증가시키면서 최적화하겠습니다.

            그림 23. Expert Advisor MovingAverageFilteredL1.mq5의 최적화 결과

            그림 23. Expert Advisor MovingAverageFilteredL1.mq5의 최적화 결과


            "MA 주기" 매개변수의 최적화는 1분도 채 걸리지 않았습니다; 결과 및 최적값 목록은 그림 23에 나와 있습니다.

            보다 정확한 테스트를 위해 테스트 설정에서 "실제 틱을 기반으로 한 모든 틱"을 선택할 수 있습니다:


            그림 24. Expert Advisor MovingAverageFilteredL1.mq5의 테스트 설정

            그림 24. Expert Advisor MovingAverageFilteredL1.mq5의 테스트 설정


            최적값인 "MA Period" = 64로 단일 테스트를 실행합니다:

            그림 25. Expert Advisor MovingAverageFilteredL1.mq5의 최적 최적화 매개변수

            그림 25. Expert Advisor MovingAverageFilteredL1.mq5의 최적 최적화 매개변수


            그림 26. Expert Advisor MovingAverageFilteredL1.mq5에 대한 최적 매개변수를 사용한 테스트 결과

            그림 26. Expert Advisor MovingAverageFilteredL1.mq5에 대한 최적 매개변수를 사용한 테스트 결과


            이제 데이터를 파일에 저장하는 테스트를 실행해야 합니다.

            이를 위해 SaveStatistics = true로 설정하고 매매 신호 필터의 4가지 조합으로 테스트를 실행하십시오. 

            그림 27. Expert Advisor MovingAverageFilteredL1.mq5의 결과를 파일에 저장하기 위한 테스트 매개변수

            그림 27. Expert Advisor MovingAverageFilteredL1.mq5의 결과를 파일로 저장하기 위한 매개변수 테스트


            네 가지 조합 모두로 테스트를 실행한 후 테스터 디렉터리에는 다음 파일들이 포함됩니다: 0_MA_EURUSD.txt, 1_MA_EURUSD.txt, 2_MA_EURUSD.txt 및 3_MA_EURUSD.txt.

            여기에는 테스트 기간 동안 각 캔들의 시간, 종가, 잔고 및 평가금액이 포함됩니다.

            별도의 디렉터리를 만들고 그곳에 파일을 복사하세요(이 예에서는 C:\Data).


            그림 28. Expert Advisor MovingAverageFilteredL1.mq5의 4가지 필터 모드에 대한 테스트 결과 파일.

            그림 28. 4가지 필터 모드 모두에 대한 테스트 결과가 포함된 파일입니다.


            데이터 분석은 파이썬 스크립트를 사용해서 수행됩니다.

            import pandas as pd
            import matplotlib.pyplot as plt
            import os
            
            # --- folder for charts
            output_dir = "C:\\data\\charts\\"
            os.makedirs(output_dir, exist_ok=True)
            
            
            symbol = "EURUSD" 
            name_strategy = "MA"
            file_strategy = name_strategy+"_"+symbol 
            title_strategy = " ("+symbol+" "+name_strategy+" strategy+filters)"
            file_prefix = symbol+"_"+name_strategy+"_"
            
            # --- files
            files = [
                "C:\\data\\0_"+file_strategy+".txt",
                "C:\\data\\1_"+file_strategy+".txt",
                "C:\\data\\2_"+file_strategy+".txt",
                "C:\\data\\3_"+file_strategy+".txt"
            ]
            
            # --- labels
            labels = [
                "No filters",
                "Open L1 filter",
                "Close L1 filter",
                "Open+Close L1 filter"
            ]
            
            # --- load data
            def load_file(filename):
                df = pd.read_csv(
                    filename,
                    sep=";",
                    header=None,
                    names=[
                        "time",
                        "balance",
                        "equity",
                        "margin",
                        "free_margin",
                        "margin_level",
                        "volume",
                        "close"
                    ]
                )
                df["time"] = pd.to_datetime(df["time"])
                return df
            
            # --- close price chart
            plt.figure(figsize=(10,6), dpi=100)
            for file, label in zip(files, labels):
                df = load_file(file)
            plt.plot(df["time"], df["close"], color='gray')
            plt.title(symbol+" Close Price")
            plt.xlabel("Time")
            plt.ylabel("closing price")
            plt.legend()
            plt.grid()
            plt.tight_layout()
            plt.savefig(output_dir + file_prefix+"close_price.png", dpi=100)
            plt.show()
                
            # --- balance chart
            plt.figure(figsize=(10,6), dpi=100)
            for file, label in zip(files, labels):
                df = load_file(file)
                plt.plot(df["time"], df["balance"], label=label)
            plt.title("Balance" + title_strategy)
            plt.xlabel("Time")
            plt.ylabel("Balance")
            plt.legend()
            plt.grid()
            plt.tight_layout()
            plt.savefig(output_dir + file_prefix+"balance.png", dpi=100)
            plt.show()
            plt.close()
            
            # --- equity chart
            plt.figure(figsize=(10,6), dpi=100)
            for file, label in zip(files, labels):
                df = load_file(file)
                plt.plot(df["time"], df["equity"], label=label)
            plt.title("Equity" + title_strategy)
            plt.xlabel("Time")
            plt.ylabel("Equity")
            plt.legend()
            plt.grid()
            plt.tight_layout()
            plt.savefig(output_dir + file_prefix+"equity.png", dpi=100)
            plt.show()
            plt.close()
            
            
            #--- balance + equity chart
            plt.figure(figsize=(10,6), dpi=100)
            for i, (file, label) in enumerate(zip(files, labels)):
            
                df = load_file(file)
                # --- get matplotlib color
                color = plt.rcParams["axes.prop_cycle"].by_key()["color"][i % 10]
                #--- equity — solid line
                plt.plot(
                    df["time"],
                    df["equity"],
                    color=color,
                    linestyle="-",
                    label=f"{label} equity"
                )
                #--- balance — dashed line
                plt.plot(
                    df["time"],
                    df["balance"],
                    color=color,
                    linestyle="--",
                    label=f"{label} balance"
                )
            plt.title("Balance + Equity" + title_strategy)
            plt.xlabel("Time")
            plt.ylabel("Value")
            plt.legend()
            plt.grid()
            plt.tight_layout()
            plt.savefig(output_dir+file_prefix+"balance_equity.png", dpi=100)
            plt.show()
            plt.close()
            
            

            MetaEditor에서 Python 스크립트를 실행하려면 "컴파일"을 누르십시오(그림 29).


            그림 29. MetaEditor의 PlotData.py 스크립트

            그림 29. MetaEditor의 PlotData.py 스크립트


            PlotData.py를 실행하면 다음과 같은 차트가 표시됩니다:

            1. EURUSD 가격 시계열 (각 캔들의 종가);
            2. 모든 필터 모드에 대한 밸런스 차트;
            3. 모든 필터 모드에 대한 평가금액 차트;
            4. 잔고 + 평가금액 통합 차트(드로다운 감소 평가용).

            차트는 C:\Data\Charts\ 폴더에 PNG 형식으로 저장됩니다.


            그림 30. 테스트 기간 동안의 EURUSD 가격차트

            그림 30. 테스트 기간 동안의 EURUSD 가격 차트


            그림 31. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 밸런스 차트

            그림 31. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 잔고 차트


            그림 32. Expert Advisor MovingAverageFilteredL1.mq5의 평가금액 차트

            그림 32. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 평가금액 차트


            그림 33. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 통합 잔고 및 평가금액 차트

            그림 33. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 통합 잔고 및 평가금액 차트


            MetaEditor에서 Python 스크립트를 실행하려면 Python(예시에서는 버전 3.14)을 설치하고 설정에서 해당 경로를 지정하십시오.


            그림 34. MetaEditor의 Python 설정

            그림 34. MetaEditor의 Python 설정


            이 스크립트는 pandas 및 matplotlib 라이브러리를 사용합니다. 설치되어 있지 않은 경우:

            pip install pandas
            pip install matplotlib


            3.5.1.2. L1 필터(이동평균 전략) 적용 결과

            결과(잔고, 평가금액 및 통합 차트)는 그림 35~37에 나와 있습니다.

            색상:

            1. 파란색 (필터 없는 전략);
            2. 녹색 (포지션 청산 시 L1 필터);
            3. 빨간색 (진입과 청산 시 모두 L1 필터 적용);
            4. 주황색 (진입시 L1 필터 장착);

            그림 35. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 밸런스 차트

            그림 35. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 잔고 차트



            그림 36. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 평가금액 차트

            그림 36. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 평가금액 차트


            그림 37. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 통합 잔고 및 평가금액 차트

            그림 37. Expert Advisor MovingAverageFilteredL1.mq5의 다양한 필터 모드에 대한 통합 잔고 및 평가금액 차트



            3.5.2. MACD 매매 전략

            또 다른 예로 MACD (Moving Average Convergence/Divergence) indicator.(이동평균 수렴/발산) 지표를 기반으로 구축된 매매 전략의 신호에 따라 매매하는 EA를 생각해 보겠습니다.

            트레이딩 시그널

            MACD 메인 라인이 시그널 라인을 교차할 때 신호가 발생합니다.

            1. 매수 — MACD 메인 라인이 시그널 라인을 아래에서 위로 교차합니다.
            2. 매도 — MACD 메인 라인이 시그널 라인을 위에서 아래로 하향 교차합니다.

            신호는 캔들 마감 시점에만 분석되므로 캔들 내 오탐지 횟수가 줄어듭니다.

            또한 진입 및 청산 필터는 L1 추세 필터 설정(L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda)에 따라 사용됩니다.

            Expert Advisor MACDFilteredL1.mq5의 코드:

            //+------------------------------------------------------------------+
            //|                                               MACDFilteredL1.mq5 |
            //|                             Copyright 2000-2026, MetaQuotes Ltd. |
            //|                                             https://www.mql5.com |
            //+------------------------------------------------------------------+
            #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
            #property link      "https://www.mql5.com"
            #property version   "1.00"
            //--- best MACD parameters for USDCHF,H1,2025
            input int    FastEMA        = 43;     // Fast EMA
            input int    SlowEMA        = 59;     // Slow EMA
            input int    SignalEMA      = 37;     // SignalEMA
            //--- trade volume
            input double TradeLot       = 0.1;    // Lot size
            //--- L1 filter parameters
            input int    L1TotalBars    = 1000;   // Total bars for L1 filter
            input bool   L1FilterOpen   = false;  // Use filter for Open
            input bool   L1FilterClose  = false;  // Use filter for Close
            input double L1CoefLambda   = 0.2;    // Lambda in lambda_max units
            //--- save statistics
            input bool   SaveStatistics = false;  // Save statistics to file
            //---
            #define MACD_MAGIC 1234502
            #include <Trade\Trade.mqh>
            int    ExtHandle = INVALID_HANDLE;
            bool   ExtHedging = false;
            CTrade ExtTrade;
            string ExtStrategyName="MACD";
            string ExtStrategyFileName="";
            //+------------------------------------------------------------------+
            //| Check new bar                                                    |
            //+------------------------------------------------------------------+
            bool IsNewBar()
              {
               static datetime last_time=0;
               datetime t[1];
            //---
               if(CopyTime(_Symbol,_Period,0,1,t)>0)
                 {
                  if(t[0]!=last_time)
                    {
                     last_time=t[0];
                     return(true);
                    }
                 }
               return(false);
              }
            //+------------------------------------------------------------------+
            //| CheckTrendL1                                                     |
            //+------------------------------------------------------------------+
            double CheckTrendL1()
              {
               int max_bars=L1TotalBars;
               MqlRates rates_data[];
               ArrayResize(rates_data,max_bars);
               ArraySetAsSeries(rates_data,false);
               if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars)
                 {
                  Print("CopyRates failed for L1Trend");
                  return(0);
                 }
            //--- prepare data (close prices vector)
               int data_count=max_bars;
               vector<double> data_close;
               data_close.Resize(data_count);
               for(int i=0; i<data_count; i++)
                  data_close[i] = rates_data[i].close;
            //--- calculate L1 filter
               vector<double> data_filtered;
               data_filtered.Resize(data_count);
               double dp=0.0;
               bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered);
               if(res)
                  dp = data_filtered[data_count-1]-data_filtered[data_count-2];
            //---
               return(dp);
              }
            //+------------------------------------------------------------------+
            //| GetTradeSignal(MACD)                                             |
            //+------------------------------------------------------------------+
            bool GetTradeSignal(ENUM_ORDER_TYPE &signal)
              {
               signal = WRONG_VALUE;
               double macd_main[];
               double macd_signal[];
            //---
               ArrayResize(macd_main,2);
               ArrayResize(macd_signal,2);
            //---
               ArraySetAsSeries(macd_main, true);
               ArraySetAsSeries(macd_signal, true);
            //--- buffer 0 = MACD main, buffer 1 = signal line
               if(CopyBuffer(ExtHandle,0,1,2,macd_main)!=2)
                  return(false);
               if(CopyBuffer(ExtHandle,1,1,2,macd_signal)!=2)
                  return(false);
            //---
               double main_prev   = macd_main[1];
               double main_last   = macd_main[0];
               double signal_prev = macd_signal[1];
               double signal_last = macd_signal[0];
            //--- MACD crossover
               if(main_prev < signal_prev && main_last > signal_last)
                  signal = ORDER_TYPE_BUY;
               else
                  if(main_prev > signal_prev && main_last < signal_last)
                     signal = ORDER_TYPE_SELL;
            //---
               return(true);
              }
            //+------------------------------------------------------------------+
            //| CheckForOpen                                                     |
            //+------------------------------------------------------------------+
            void CheckForOpen()
              {
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal) || signal == WRONG_VALUE)
                  return;
            //---
               if(L1FilterOpen)
                 {
                  double dp = CheckTrendL1();
                  if(signal == ORDER_TYPE_BUY && dp < 0)
                     return;
                  if(signal == ORDER_TYPE_SELL && dp > 0)
                     return;
                 }
            //---
               if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol, _Period) < L1TotalBars)
                  return;
            //---
               double price = (signal == ORDER_TYPE_BUY)
                              ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                              : SymbolInfoDouble(_Symbol, SYMBOL_BID);
            //---
               ExtTrade.PositionOpen(_Symbol, signal, TradeLot, price, 0, 0);
              }
            //+------------------------------------------------------------------+
            //| CheckForClose                                                    |
            //+------------------------------------------------------------------+
            void CheckForClose()
              {
            //--- check position
               if(!PositionSelect(_Symbol))
                  return;
            //--- check position magic
               if(PositionGetInteger(POSITION_MAGIC)!=MACD_MAGIC)
                  return;
            //--- check trade signal
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
            //---
               long type = PositionGetInteger(POSITION_TYPE);
               bool close_signal = false;
            //---
               if(type == POSITION_TYPE_BUY  && signal == ORDER_TYPE_SELL)
                  close_signal = true;
               if(type == POSITION_TYPE_SELL && signal == ORDER_TYPE_BUY)
                  close_signal = true;
            //--- check L1 filter
               if(L1FilterClose)
                 {
                  double dp = CheckTrendL1();
                  if(type == POSITION_TYPE_BUY && dp > 0)
                    {
                     close_signal = false;
                     PrintFormat("Close BUY signal cancelled by L1 trend dp=%.5f", dp);
                    }
                  if(type == POSITION_TYPE_SELL && dp < 0)
                    {
                     close_signal = false;
                     PrintFormat("Close SELL signal cancelled by L1 trend dp=%.5f", dp);
                    }
                 }
            //---
               if(close_signal)
                  ExtTrade.PositionClose(_Symbol, 3);
              }
            //+------------------------------------------------------------------+
            //| SelectPosition                                                   |
            //+------------------------------------------------------------------+
            bool SelectPosition()
              {
               bool res = false;
               if(ExtHedging)
                 {
                  uint total = PositionsTotal();
                  for(uint i=0; i<total; i++)
                    {
                     string sym = PositionGetSymbol(i);
                     if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==MACD_MAGIC)
                       {
                        res = true;
                        break;
                       }
                    }
                 }
               else
                 {
                  if(PositionSelect(_Symbol))
                     res = (PositionGetInteger(POSITION_MAGIC)==MACD_MAGIC);
                 }
               return(res);
              }
            //+------------------------------------------------------------------+
            //| Expert initialization                                            |
            //+------------------------------------------------------------------+
            int OnInit()
              {
            //--- check parameters
               if(FastEMA <= 0 || SlowEMA <= 0 || SignalEMA <= 0)
                 {
                  Print("Error: MACD parameters must be positive");
                  return(INIT_PARAMETERS_INCORRECT);
                 }
               if(FastEMA >= SlowEMA)
                 {
                  Print("FastEMA must be less than SlowEMA");
                  return(INIT_PARAMETERS_INCORRECT);
                 }
            //---
               ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
               ExtTrade.SetExpertMagicNumber(MACD_MAGIC);
               ExtTrade.SetMarginMode();
               ExtTrade.SetTypeFillingBySymbol(_Symbol);
            //--- prepare indicator
               ExtHandle=iMACD(_Symbol,_Period,FastEMA,SlowEMA,SignalEMA,PRICE_CLOSE);
               if(ExtHandle==INVALID_HANDLE)
                 {
                  Print("Failed to create MACD handle");
                  return(INIT_FAILED);
                 }
            //--- prepare filename
               ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName);
            //--- delete old file if exists
               if(FileIsExist(ExtStrategyFileName))
                  FileDelete(ExtStrategyFileName);
            //---
               return(INIT_SUCCEEDED);
              }
            //+------------------------------------------------------------------+
            //| PrepareStrategyFileName                                          |
            //+------------------------------------------------------------------+
            string PrepareStrategyFileName(string strategy_name)
              {
               int v=0;
               if(L1FilterOpen)
                  v=v | 1;
            //---
               if(L1FilterClose)
                  v=v | 2;
            //---
               string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt";
               return(filename);
              }
            //+------------------------------------------------------------------+
            //| Save account statistics to file                                  |
            //+------------------------------------------------------------------+
            void SaveAccountStatistics()
              {
            //--- check file name
               if(ExtStrategyFileName=="")
                  return;
            //---
               int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI);
               if(file==INVALID_HANDLE)
                 {
                  Print("File open error: ",GetLastError());
                  return;
                 }
            //--- append
               FileSeek(file,0,SEEK_END);
            //--- account data
               double balance     = AccountInfoDouble(ACCOUNT_BALANCE);
               double equity      = AccountInfoDouble(ACCOUNT_EQUITY);
               double margin      = AccountInfoDouble(ACCOUNT_MARGIN);
               double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
               double margin_lvl  = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
            //--- volume
               double volume=0.0;
               if(PositionSelect(_Symbol))
                  volume=PositionGetDouble(POSITION_VOLUME);
            //--- time
               datetime t[1];
               if(CopyTime(_Symbol,_Period,0,1,t)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               double current_close[1];
               if(CopyClose(_Symbol,_Period,0,1,current_close)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS),
                                        balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]);
            //---
               FileWrite(file,line);
            //---
               FileClose(file);
              }
            //+------------------------------------------------------------------+
            //| Expert OnTick function                                           |
            //+------------------------------------------------------------------+
            void OnTick()
              {
            //--- trade only at new bar
               if(!IsNewBar())
                  return;
            //--- check trade conditions
               if(SelectPosition())
                  CheckForClose();
               else
                  CheckForOpen();
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
              }
            //+------------------------------------------------------------------+
            //| Expert deinitialization                                          |
            //+------------------------------------------------------------------+
            void OnDeinit(const int reason)
              {
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
            //---
               if(ExtHandle != INVALID_HANDLE)
                  IndicatorRelease(ExtHandle);
              }
            //+------------------------------------------------------------------+
            

            테스트 설정은 그림 38에 나와 있습니다.

            그림 38. Expert Advisor MACDFilteredL1.mq5의 전략 테스터 설정

            그림 38. Expert Advisor MACDFilteredL1.mq5의 전략 테스터 설정


            그림 39. Expert Advisor MACDFilteredL1.mq5의 테스트 매개변수

            그림 39. Expert Advisor MACDFilteredL1.mq5의 테스트 매개변수


            그림 40. Expert Advisor MACDFilteredL1.mq5의 테스트 결과

            그림 40. Expert Advisor MACDFilteredL1.mq5의 테스트 결과


            그림 41. Expert Advisor MACDFilteredL1.mq5의 결과를 파일에 저장하기 위한 테스트 매개변수

            그림 41. MACDFilteredL1.mq5 Expert Advisor의 결과를 파일에 저장하기 위한 매개변수 테스트


            다양한 필터 설정을 사용하여 전략 테스터에서 테스트를 실행하면 테스터 디렉터리에 x_MACD_EURUSD.txt 파일이 생성됩니다.

            해당 파일들을 C:\Data\ 폴더에 복사한 후 PlotData.py 스크립트를 실행해야 합니다.


            그림 42. 다양한 필터링 모드에 대한 Expert Advisor MACDFiltered.mq5의 테스트 결과가 포함된 파일

            그림 42. 다양한 필터링 모드에 대한 Expert Advisor MACDFiltered.mq5의 테스트 결과가 포함된 파일



            3.5.2.1. L1 필터 적용 결과 (MACD 전략)

            결과는 그림 43~45에 나타나 있습니다.

            그림 43. 다양한 필터 모드에 대한 Expert Advisor MACDFilteredL1.mq5의 잔고 차트

            그림 43. 다양한 필터 모드에 대한 Expert Advisor MACDFilteredL1.mq5의 잔고 차트


            그림 44. 다양한 필터 모드에 대한 MACDFilteredL1.mq5 Expert Advisor의 평가금액 차트

            그림 44. 다양한 필터 모드에 대한 MACDFilteredL1.mq5 Expert Advisor의 평가금액 차트


            그림 45. 다양한 필터 모드에 대한 MACDFilteredL1.mq5 Expert Advisor의 통합 잔고 및 평가금액 차트

            그림 45. 다양한 필터 모드에 대한 Expert Advisor MACDFilteredL1.mq5의 통합 잔고 및 평가금액 차트


            3.5.3. ADX 매매 전략

            또 다른 예로, Average Directional Movement Index (ADX)를 기반으로 추세 추종 전략을 구현하는 Expert Advisor ADXFilteredL1.mq5를 살펴보겠습니다.

            주요 거래 신호는 +DI 및 −DI 선 분석과 추세 강도를 나타내는 ADX 레벨 분석을 기반으로 합니다.

            신호는 다음과 같이 정의됩니다:

            • 매수: +DI가 −DI를 아래에서 위로 상향 교차합니다:
            • 매도: +DI가 −DI를 위에서 아래로 하향 교차합니다.

            또한 ADX 값도 고려됩니다. ADX가 임계값인 ADXTrendLevel보다 낮으면 시장은 약한 추세 또는 횡보세로 간주되어 신호는 무시됩니다.

            최근 마감된 두 개의 캔들 지표 값을 사용하여 현재 미완료 캔들의 영향을 배제하고 가짜 신호를 줄입니다.

            진입 및 청산 필터는 L1 추세 설정(L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda)에 따라 적용됩니다.

            다음은 Expert Advisor ADXFilteredL1.mq5의 코드입니다.

            //+------------------------------------------------------------------+
            //|                                                ADXFilteredL1.mq5 |
            //|                             Copyright 2000-2026, MetaQuotes Ltd. |
            //|                                              http://www.mql5.com |
            //+------------------------------------------------------------------+
            #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
            #property link      "https://www.mql5.com"
            #property version   "1.00"
            //--- best ADX parameters for EURUSD,H1,2025
            input int    ADXPeriod      = 65;     // ADX Period
            input double ADXTrendLevel  = 7;      // ADX Trend Level
            //--- trade volume
            input double TradeLot       = 0.1;    // Lot size
            //--- L1 filter parameters
            input int    L1TotalBars    = 1000;   // Total bars for L1 filter
            input bool   L1FilterOpen   = false;  // Use filter for Open
            input bool   L1FilterClose  = false;  // Use filter for Close
            input double L1CoefLambda   = 0.2;    // Lambda in lambda_max units
            //--- save statistics
            input bool   SaveStatistics = false;  // Save statistics to file
            //---
            #define ADX_MAGIC 1234503
            #include <Trade\Trade.mqh>
            CTrade ExtTrade;
            int    ExtHandle = INVALID_HANDLE;
            bool   ExtHedging = false;
            string ExtStrategyName="ADX";
            string ExtStrategyFileName="";
            //+------------------------------------------------------------------+
            //| Check new bar                                                    |
            //+------------------------------------------------------------------+
            bool IsNewBar()
              {
               static datetime last_time=0;
               datetime t[1];
            //---
               if(CopyTime(_Symbol,_Period,0,1,t)>0)
                 {
                  if(t[0]!=last_time)
                    {
                     last_time=t[0];
                     return(true);
                    }
                 }
               return(false);
              }
            //+------------------------------------------------------------------+
            //| CheckTrendL1                                                     |
            //+------------------------------------------------------------------+
            double CheckTrendL1()
              {
               int max_bars=L1TotalBars;
               MqlRates rates_data[];
               ArrayResize(rates_data,max_bars);
               ArraySetAsSeries(rates_data,false);
               if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars)
                 {
                  Print("CopyRates failed for L1Trend");
                  return(0);
                 }
            //--- prepare data (close prices vector)
               int data_count=max_bars;
               vector<double> data_close;
               data_close.Resize(data_count);
               for(int i=0; i<data_count; i++)
                  data_close[i] = rates_data[i].close;
            //--- calculate L1 filter
               vector<double> data_filtered;
               data_filtered.Resize(data_count);
               double dp=0.0;
               bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered);
               if(res)
                  dp = data_filtered[data_count-1] - data_filtered[data_count-2];
            //---
               return(dp);
              }
            //+------------------------------------------------------------------+
            //| GetTradeSignal (ADX)                                             |
            //+------------------------------------------------------------------+
            bool GetTradeSignal(ENUM_ORDER_TYPE &signal)
              {
               signal=WRONG_VALUE;
               double adx[],plusdi[],minusdi[];
               ArrayResize(adx,2);
               ArrayResize(plusdi,2);
               ArrayResize(minusdi,2);
            //---
               ArraySetAsSeries(adx,true);
               ArraySetAsSeries(plusdi,true);
               ArraySetAsSeries(minusdi,true);
            //--- buffer0 = ADX
               if(CopyBuffer(ExtHandle,0,1,2,adx)!=2)
                  return(false);
            //--- buffer1 = +DI
               if(CopyBuffer(ExtHandle,1,1,2,plusdi)!=2)
                  return(false);
            //--- buffer2 = -DI
               if(CopyBuffer(ExtHandle,2,1,2,minusdi)!=2)
                  return(false);
               double adx_last=adx[0];
               double plus_prev=plusdi[1];
               double plus_last=plusdi[0];
               double minus_prev=minusdi[1];
               double minus_last=minusdi[0];
            //--- strong trend required
               if(adx_last<ADXTrendLevel)
                  return(true);
            //--- +DI cross -DI
               if(plus_prev<minus_prev && plus_last>minus_last)
                  signal=ORDER_TYPE_BUY;
               else
                  if(plus_prev>minus_prev && plus_last<minus_last)
                     signal=ORDER_TYPE_SELL;
            //---
               return(true);
              }
            //+------------------------------------------------------------------+
            //| CheckForOpen                                                     |
            //+------------------------------------------------------------------+
            void CheckForOpen()
              {
               ENUM_ORDER_TYPE signal;
            //---
               if(!GetTradeSignal(signal) || signal==WRONG_VALUE)
                  return;
            //---
               if(L1FilterOpen)
                 {
                  double dp=CheckTrendL1();
                  if(signal==ORDER_TYPE_BUY && dp<0)
                     return;
                  if(signal==ORDER_TYPE_SELL && dp>0)
                     return;
                 }
            //---
               if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars)
                  return;
            //---
               double price=(signal==ORDER_TYPE_BUY)
                            ? SymbolInfoDouble(_Symbol,SYMBOL_ASK)
                            : SymbolInfoDouble(_Symbol,SYMBOL_BID);
            //---
               ExtTrade.PositionOpen(_Symbol,signal,TradeLot,price,0,0);
              }
            //+------------------------------------------------------------------+
            //| CheckForClose                                                    |
            //+------------------------------------------------------------------+
            void CheckForClose()
              {
            //--- check position
               if(!PositionSelect(_Symbol))
                  return;
            //--- check position magic
               if(PositionGetInteger(POSITION_MAGIC)!=ADX_MAGIC)
                  return;
            //--- check trade signal
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
            //---
               long type=PositionGetInteger(POSITION_TYPE);
               bool close_signal=false;
            //---
               if(type==POSITION_TYPE_BUY && signal==ORDER_TYPE_SELL)
                  close_signal=true;
            //---
               if(type==POSITION_TYPE_SELL && signal==ORDER_TYPE_BUY)
                  close_signal=true;
            //--- check L1 filter
            //--- check L1 filter
               if(L1FilterClose)
                 {
                  double dp = CheckTrendL1();
                  if(type == POSITION_TYPE_BUY && dp > 0)
                    {
                     close_signal = false;
                     PrintFormat("Close BUY signal cancelled by L1 trend dp=%.5f", dp);
                    }
                  if(type == POSITION_TYPE_SELL && dp < 0)
                    {
                     close_signal = false;
                     PrintFormat("Close SELL signal cancelled by L1 trend dp=%.5f", dp);
                    }
                 }
            //---
               if(close_signal)
                  ExtTrade.PositionClose(_Symbol,3);
              }
            //+------------------------------------------------------------------+
            //| SelectPosition                                                   |
            //+------------------------------------------------------------------+
            bool SelectPosition()
              {
               bool res = false;
               if(ExtHedging)
                 {
                  uint total = PositionsTotal();
                  for(uint i=0; i<total; i++)
                    {
                     string sym = PositionGetSymbol(i);
                     if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==ADX_MAGIC)
                       {
                        res = true;
                        break;
                       }
                    }
                 }
               else
                 {
                  if(PositionSelect(_Symbol))
                     res = (PositionGetInteger(POSITION_MAGIC)==ADX_MAGIC);
                 }
               return(res);
              }
            //+------------------------------------------------------------------+
            //| Expert initialization                                            |
            //+------------------------------------------------------------------+
            int OnInit()
              {
               ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
               ExtTrade.SetExpertMagicNumber(ADX_MAGIC);
               ExtTrade.SetMarginMode();
               ExtTrade.SetTypeFillingBySymbol(_Symbol);
            //--- prepare indicator
               ExtHandle=iADX(_Symbol,_Period,ADXPeriod);
               if(ExtHandle==INVALID_HANDLE)
                 {
                  Print("ADX handle error");
                  return(INIT_FAILED);
                 }
            //--- prepare filename
               ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName);
            //--- delete old file if exists
               if(FileIsExist(ExtStrategyFileName))
                  FileDelete(ExtStrategyFileName);
            //---
               return(INIT_SUCCEEDED);
              }
            //+------------------------------------------------------------------+
            //| PrepareStrategyFileName                                          |
            //+------------------------------------------------------------------+
            string PrepareStrategyFileName(string strategy_name)
              {
               int v=0;
               if(L1FilterOpen)
                  v=v | 1;
            //---
               if(L1FilterClose)
                  v=v | 2;
            //---
               string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt";
               return(filename);
              }
            //+------------------------------------------------------------------+
            //| Save account statistics to file                                  |
            //+------------------------------------------------------------------+
            void SaveAccountStatistics()
              {
            //--- check file name
               if(ExtStrategyFileName=="")
                  return;
            //---
               int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI);
               if(file==INVALID_HANDLE)
                 {
                  Print("File open error: ",GetLastError());
                  return;
                 }
            //--- append
               FileSeek(file,0,SEEK_END);
            //--- account data
               double balance     = AccountInfoDouble(ACCOUNT_BALANCE);
               double equity      = AccountInfoDouble(ACCOUNT_EQUITY);
               double margin      = AccountInfoDouble(ACCOUNT_MARGIN);
               double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
               double margin_lvl  = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
            //--- volume
               double volume=0.0;
               if(PositionSelect(_Symbol))
                  volume=PositionGetDouble(POSITION_VOLUME);
            //--- time
               datetime t[1];
               if(CopyTime(_Symbol,_Period,0,1,t)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               double current_close[1];
               if(CopyClose(_Symbol,_Period,0,1,current_close)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS),
                                        balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]);
            //---
               FileWrite(file,line);
            //---
               FileClose(file);
              }
            //+------------------------------------------------------------------+
            //| Expert OnTick function                                           |
            //+------------------------------------------------------------------+
            void OnTick()
              {
            //--- trade only at new bar
               if(!IsNewBar())
                  return;
            //--- check trade conditions
               if(SelectPosition())
                  CheckForClose();
               else
                  CheckForOpen();
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
              }
            //+------------------------------------------------------------------+
            //| Expert deinitialization                                          |
            //+------------------------------------------------------------------+
            void OnDeinit(const int reason)
              {
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
            //--- release indicator handle
               if(ExtHandle!=INVALID_HANDLE)
                  IndicatorRelease(ExtHandle);
              }
            //+------------------------------------------------------------------+
            

            테스트 설정, 매개변수 및 결과는 그림 46~48에 나와 있습니다.


            그림 46. Expert Advisor ADXFilteredL1.mq5의 테스터 설정

            그림 46. Expert Advisor ADXFilteredL1.mq5의 테스터 설정


            . 그림 47. Expert Advisor ADXFilteredL1.mq5의 테스트 매개변수

            그림 47. Expert Advisor ADXFilteredL1.mq5의 테스트 매개변수


            그림 48. Expert Advisor ADXFilteredL1.mq5에 대한 테스트 결과

            그림 48. Expert Advisor ADXFilteredL1.mq5에 대한 테스트 결과


            테스트를 위해서는 서로 다른 필터 설정으로 4번 실행해야 합니다:

            그림 49. Expert Advisor ADXFilteredL1.mq5의 결과를 파일에 저장하기 위한 테스트 매개변수

            그림 49. ADXFilteredL1.mq5 Expert Advisor의 결과를 파일에 저장하기 위한 테스트 매개변수

            그 후 테스터 디렉토리에 파일이 생성됩니다; 이 파일들을 C:\Data 폴더로 복사한 다음 PlotData.py를 사용하여 처리되어야 합니다.


            그림 50. 다양한 필터 설정에 따른 Expert Advisor ADXFiltered.mq5의 테스트 결과 파일

            그림 50. 다양한 필터 설정에 대한 Expert Advisor ADXFiltered.mq5의 테스트 결과가 포함된 파일



            3.5.3.1. ADX 전략에 L1 필터를 적용한 결과

            결과는 그림 51~53에 나타나 있습니다.

            그림 51. 다양한 필터 모드를 적용한 Expert Advisor ADXFilteredL1.mq5의 밸런스 차트

            그림 51. 다양한 필터 모드를 적용한 Expert Advisor ADXFilteredL1.mq5의 밸런스 차트


            그림 52. 다양한 필터 모드를 적용한 Expert Advisor ADXFilteredL1.mq5의 평가금액 차트

            그림 52. 다양한 필터 모드를 적용한 Expert Advisor ADXFilteredL1.mq5의 평가금액 차트


            그림 53. 다양한 필터 모드를 적용한 Expert Advisor ADXFilteredL1.mq5의 통합 잔고 및 평가금액 차트

            그림 53. 다양한 필터 모드를 적용한 Expert Advisor ADXFilteredL1.mq5의 통합 잔고 및 평가금액 차트



            3.5.4. EMA 교차를 기반으로 한 매매 전략

            두 개의 exponential moving averages (EMA)의 교차를 기반으로 추세 추종 거래 전략을 구현하는 Expert Advisor EMAFilteredL1.mq5를 살펴봅니다.

            이 전략은 두 개의 이동평균을 사용합니다:

            1. FastEMA — 빠른 지수 이동 평균;
            2. SlowEMA — 느린 지수 이동 평균.

            매매 신호는 다음과 같이 생성됩니다:

            • 매수: 빠른 이동평균선이 느린 이동평균선을 아래에서 위로 교차할 때;
            • 매도: 빠른 이동평균선이 느린 이동평균선을 위에서 아래로 하향 교차할 때.

            분석에는 최근 마감된 두 개의 캔들의 지표 값이 사용됩니다. 따라서 현재 미완료 캔들의 영향을 제거하고 잘못된 신호의 수를 줄일 수 있습니다.

            또한 진입 및 청산 필터는 L1 추세 필터 설정(L1TotalBars, L1FilterOpen, L1FilterClose, L1CoefLambda)에 따라 적용됩니다.

            Expert Advisor EMAFilteredL1.mq5의 코드는 아래와 같습니다.

            //+------------------------------------------------------------------+
            //|                                                EMAFilteredL1.mq5 |
            //|                             Copyright 2000-2026, MetaQuotes Ltd. |
            //|                                              http://www.mql5.com |
            //+------------------------------------------------------------------+
            #property copyright "Copyright 2000-2026, MetaQuotes Ltd."
            #property link      "https://www.mql5.com"
            #property version   "1.00"
            //--- best EMA parameters for EURUSD,H1,2025
            input int    FastEMA       = 29;     // Fast EMA
            input int    SlowEMA       = 101;     // Slow EMA
            //--- trade volume
            input double TradeLot      = 0.1;    // Lot size
            //--- L1 filter parameters
            input int    L1TotalBars    = 1000;  // Total bars for L1 filter
            input bool   L1FilterOpen  = false;  // Use filter for Open
            input bool   L1FilterClose = false;  // Use filter for Close
            input double L1CoefLambda  = 0.2;    // Lambda in lambda_max units
            //--- save statistics
            input bool   SaveStatistics = false; // Save statistics to file
            //---
            #define EMA_MAGIC 1234503
            #include <Trade\Trade.mqh>
            CTrade ExtTrade;
            int    ExtHandle = INVALID_HANDLE;
            bool   ExtHedging = false;
            int    FastHandle, SlowHandle;
            string ExtStrategyName="EMA";
            string ExtStrategyFileName="";
            //+------------------------------------------------------------------+
            //| Check new bar                                                    |
            //+------------------------------------------------------------------+
            bool IsNewBar()
              {
               static datetime last_time=0;
               datetime t[1];
            //---
               if(CopyTime(_Symbol,_Period,0,1,t)>0)
                 {
                  if(t[0]!=last_time)
                    {
                     last_time=t[0];
                     return(true);
                    }
                 }
               return(false);
              }
            //+------------------------------------------------------------------+
            //| CheckTrendL1                                                     |
            //+------------------------------------------------------------------+
            double CheckTrendL1()
              {
               int max_bars=L1TotalBars;
               MqlRates rates_data[];
               ArrayResize(rates_data,max_bars);
               ArraySetAsSeries(rates_data,false);
               if(CopyRates(_Symbol,_Period,0,max_bars,rates_data) != max_bars)
                 {
                  Print("CopyRates failed for L1Trend");
                  return(0);
                 }
            //--- prepare data (close prices vector)
               int data_count=max_bars;
               vector<double> data_close;
               data_close.Resize(data_count);
               for(int i=0; i<data_count; i++)
                  data_close[i] = rates_data[i].close;
            //--- calculate L1 filter
               vector<double> data_filtered;
               data_filtered.Resize(data_count);
               double dp=0.0;
               bool res=data_close.L1TrendFilter(L1CoefLambda,true,data_filtered);
               if(res)
                  dp = data_filtered[data_count-1] - data_filtered[data_count-2];
            //---
               return(dp);
              }
            //+------------------------------------------------------------------+
            //| GetTradeSignal (2EMA crossover)                                  |
            //+------------------------------------------------------------------+
            bool GetTradeSignal(ENUM_ORDER_TYPE &signal)
              {
               signal=WRONG_VALUE;
            //---
               double fast[],slow[];
               ArrayResize(fast,2);
               ArrayResize(slow,2);
            //---
               ArraySetAsSeries(fast,true);
               ArraySetAsSeries(slow,true);
            //---
               if(CopyBuffer(FastHandle,0,1,2,fast)!=2)
                  return(false);
            //---
               if(CopyBuffer(SlowHandle,0,1,2,slow)!=2)
                  return(false);
            //---
               if(fast[1]<slow[1] && fast[0]>slow[0])
                  signal=ORDER_TYPE_BUY;
               if(fast[1]>slow[1] && fast[0]<slow[0])
                  signal=ORDER_TYPE_SELL;
            //---
               return(true);
              }
            //+------------------------------------------------------------------+
            //| CheckForOpen                                                     |
            //+------------------------------------------------------------------+
            void CheckForOpen()
              {
               ENUM_ORDER_TYPE signal;
            //---
               if(!GetTradeSignal(signal) || signal==WRONG_VALUE)
                  return;
            //---
               if(L1FilterOpen)
                 {
                  double dp=CheckTrendL1();
            //---
                  if(signal==ORDER_TYPE_BUY && dp<0)
                     return;
                  if(signal==ORDER_TYPE_SELL && dp>0)
                     return;
                 }
            //---
               if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || Bars(_Symbol,_Period)<L1TotalBars)
                  return;
            //---
               double price=(signal==ORDER_TYPE_BUY)
                            ? SymbolInfoDouble(_Symbol,SYMBOL_ASK)
                            : SymbolInfoDouble(_Symbol,SYMBOL_BID);
            //---
               ExtTrade.PositionOpen(_Symbol,signal,TradeLot,price,0,0);
              }
            //+------------------------------------------------------------------+
            //| CheckForClose                                                    |
            //+------------------------------------------------------------------+
            void CheckForClose()
              {
            //--- check position
               if(!PositionSelect(_Symbol))
                  return;
            //--- check position magic
               if(PositionGetInteger(POSITION_MAGIC)!=EMA_MAGIC)
                  return;
            //--- check trade signal
               ENUM_ORDER_TYPE signal;
               if(!GetTradeSignal(signal))
                  return;
            //---
               long type=PositionGetInteger(POSITION_TYPE);
               bool close_signal=false;
            //---
               if(type==POSITION_TYPE_BUY && signal==ORDER_TYPE_SELL)
                  close_signal=true;
            //---
               if(type==POSITION_TYPE_SELL && signal==ORDER_TYPE_BUY)
                  close_signal=true;
            //--- check L1 filter
               if(L1FilterClose)
                 {
                  double dp = CheckTrendL1();
                  if(type == POSITION_TYPE_BUY && dp > 0)
                    {
                     close_signal = false;
                     PrintFormat("Close BUY signal cancelled by L1 trend dp=%.5f", dp);
                    }
                  if(type == POSITION_TYPE_SELL && dp < 0)
                    {
                     close_signal = false;
                     PrintFormat("Close SELL signal cancelled by L1 trend dp=%.5f", dp);
                    }
                 }
            //---
               if(close_signal)
                  ExtTrade.PositionClose(_Symbol,3);
              }
            //+------------------------------------------------------------------+
            //| SelectPosition                                                   |
            //+------------------------------------------------------------------+
            bool SelectPosition()
              {
               bool res = false;
               if(ExtHedging)
                 {
                  uint total = PositionsTotal();
                  for(uint i=0; i<total; i++)
                    {
                     string sym = PositionGetSymbol(i);
                     if(sym == _Symbol && PositionGetInteger(POSITION_MAGIC)==EMA_MAGIC)
                       {
                        res = true;
                        break;
                       }
                    }
                 }
               else
                 {
                  if(PositionSelect(_Symbol))
                     res = (PositionGetInteger(POSITION_MAGIC)==EMA_MAGIC);
                 }
               return(res);
              }
            //+------------------------------------------------------------------+
            //| Expert initialization                                            |
            //+------------------------------------------------------------------+
            int OnInit()
              {
               ExtHedging = (AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
               ExtTrade.SetExpertMagicNumber(EMA_MAGIC);
               ExtTrade.SetMarginMode();
               ExtTrade.SetTypeFillingBySymbol(_Symbol);
            //--- prepare indicators
               FastHandle=iMA(_Symbol,_Period,FastEMA,0,MODE_EMA,PRICE_CLOSE);
               SlowHandle=iMA(_Symbol,_Period,SlowEMA,0,MODE_EMA,PRICE_CLOSE);
               if(FastHandle==INVALID_HANDLE||SlowHandle==INVALID_HANDLE)
                  return(INIT_FAILED);
            //--- prepare filename
               ExtStrategyFileName=PrepareStrategyFileName(ExtStrategyName);
            //--- delete old file if exists
               if(FileIsExist(ExtStrategyFileName))
                  FileDelete(ExtStrategyFileName);
            //---
               return(INIT_SUCCEEDED);
              }
            //+------------------------------------------------------------------+
            //| PrepareStrategyFileName                                          |
            //+------------------------------------------------------------------+
            string PrepareStrategyFileName(string strategy_name)
              {
               int v=0;
               if(L1FilterOpen)
                  v=v | 1;
            //---
               if(L1FilterClose)
                  v=v | 2;
            //---
               string filename=IntegerToString(v)+"_"+strategy_name+"_"+_Symbol+".txt";
               return(filename);
              }
            //+------------------------------------------------------------------+
            //| Save account statistics to file                                  |
            //+------------------------------------------------------------------+
            void SaveAccountStatistics()
              {
            //--- check file name
               if(ExtStrategyFileName=="")
                  return;
            //---
               int file=FileOpen(ExtStrategyFileName,FILE_WRITE|FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_ANSI);
               if(file==INVALID_HANDLE)
                 {
                  Print("File open error: ",GetLastError());
                  return;
                 }
            //--- append
               FileSeek(file,0,SEEK_END);
            //--- account data
               double balance     = AccountInfoDouble(ACCOUNT_BALANCE);
               double equity      = AccountInfoDouble(ACCOUNT_EQUITY);
               double margin      = AccountInfoDouble(ACCOUNT_MARGIN);
               double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
               double margin_lvl  = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
            //--- volume
               double volume=0.0;
               if(PositionSelect(_Symbol))
                  volume=PositionGetDouble(POSITION_VOLUME);
            //--- time
               datetime t[1];
               if(CopyTime(_Symbol,_Period,0,1,t)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               double current_close[1];
               if(CopyClose(_Symbol,_Period,0,1,current_close)<=0)
                 {
                  FileClose(file);
                  return;
                 }
               string line=StringFormat("%s;%.2f;%.2f;%.2f;%.2f;%.2f;%.2f;%f",TimeToString(t[0],TIME_DATE|TIME_SECONDS),
                                        balance,equity,margin,free_margin,margin_lvl,volume,current_close[0]);
            //---
               FileWrite(file,line);
            //---
               FileClose(file);
              }
            //+------------------------------------------------------------------+
            //| Expert OnTick function                                           |
            //+------------------------------------------------------------------+
            void OnTick()
              {
            //--- trade only at new bar
               if(!IsNewBar())
                  return;
            //--- check trade conditions
               if(SelectPosition())
                  CheckForClose();
               else
                  CheckForOpen();
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
              }
            //+------------------------------------------------------------------+
            //| Expert deinitialization                                          |
            //+------------------------------------------------------------------+
            void OnDeinit(const int reason)
              {
            //--- save account statistics
               if(SaveStatistics)
                  SaveAccountStatistics();
            //--- release indicator handles
               if(FastHandle!=INVALID_HANDLE)
                  IndicatorRelease(FastHandle);
            //---
               if(SlowHandle!=INVALID_HANDLE)
                  IndicatorRelease(SlowHandle);
              }
            //+------------------------------------------------------------------+

            Expert Advisor EMAFilteredL1.mq5의 설정, 매개변수 및 테스트 결과는 그림 54~56에 나와 있습니다.

            그림 54. Expert Advisor EMAFilteredL1.mq5의 테스터 설정

            그림 54. Expert Advisor EMAFilteredL1.mq5의 테스터 설정


            그림 55. Expert Advisor EMAFilteredL1.mq5의 테스트 매개변수

            그림 55. Expert Advisor EMAFilteredL1.mq5의 테스트 매개변수


            그림 56. Expert Advisor EMAFilteredL1.mq5에 대한 테스트 결과

            그림 56. Expert Advisor EMAFilteredL1.mq5에 대한 테스트 결과


            L1 필터의 효과를 분석하려면 테스터에서 Expert Advisor를 다양한 필터 모드로 순차적으로 실행해야 합니다.

            그림 57. Expert Advisor EMAFilteredL1.mq5의 결과를 파일에 저장하기 위한 테스트 매개변수

            그림 57. Expert Advisor EMAFilteredL1.mq5의 결과를 파일에 저장하기 위한 테스트 매개변수


            결과적으로 tester 폴더에 파일이 생성됩니다. 이 파일들을 Python 스크립트 PlotData.py에 지정된 디렉터리로 복사한 후, symbol = "EURUSD", name_strategy = "EMA" 설정으로 실행해야 합니다.


            그림 58. 다양한 거래 신호 필터링 모드에서 Expert Advisor EMAFilteredL1.mq5의 테스트 결과가 담긴 파일들

            그림 58. 다양한 매매 신호 필터링 모드에서 Expert Advisor EMAFilteredL1.mq5의 테스트 결과가 담긴 파일입니다.



            3.5.4.1. EMA 전략 거래 신호에 L1 필터를 적용한 결과

            결과는 그림 59~61에 나타나 있습니다.

            그림 59. 다양한 필터링 모드에 대한 Expert Advisor EMAFilteredL1.mq5의 밸런스 차트

            그림 59. Expert Advisor EMAFilteredL1.mq5의 다양한 필터링 모드에 대한 잔고 차트


            그림 60. 다양한 필터링 모드에 대한 Expert Advisor EMAFilteredL1.mq5의 평가금액 차트

            그림 60. Expert Advisor EMAFilteredL1.mq5의 다양한 필터링 모드에 대한 평가금액 차트


            그림 61. 다양한 필터링 모드에 따른 Expert Advisor EMAFilteredL1.mq5의 통합 잔고 및 평가금액 차트

            그림 61. Expert Advisor EMAFilteredL1.mq5의 다양한 필터링 모드에 대한 통합 잔고 및 평가금액 차트입니다.




            3.5.5. 이동평균, MACD, ADX 및 EMA 거래 전략에서 L1 필터 사용에 대한 요약

            본 예시에서는 EURUSD 통화쌍, H1 시간 프레임, 2025년을 대상으로 매매 전략을 테스트했습니다(그림 62).

            그림 62. 테스트 기간 동안의 EURUSD 가격 차트 (2025년, 1시간봉, 종가)

            그림 62. 테스트 기간(2025년, 1시간, 종가) 동안의 EURUSD 가격 차트


            그림 63. 다양한 필터링 모드에서 이동평균, MACD, ADX 및 EMA 전략의 잔액차트

            그림 63. 다양한 필터링 모드에서 이동평균, MACD, ADX 및 EMA 전략의 잔고 차트


            이동평균, MACD, ADX, EMA 전략 분석 결과 포지션 청산 단계에서 L1 필터링을 적용할 때 가장 좋은 결과를 얻을 수 있는 것으로 나타났습니다(차트에서 녹색으로 표시). 청산시 필터를 사용하면 노이즈 반전과 가짜 신호를 효과적으로 억제하여 안정적인 추세의 방향으로 포지션을 유지할 수 있습니다. 이는 수익과 수익계수를 증가시키고 최대 드로다운을 줄이는 결과를 가져옵니다.

            포지션 진입 단계에서 L1 필터링을 사용하는 것(주황색으로 강조 표시됨)은 효과가 떨어지는 것으로 나타났습니다. 추가 필터링으로 인해 진입 횟수가 제한되고 매매의 질이 비례적으로 향상되지 않으며 수익성 있는 움직임의 일부를 놓치게 되기 때문입니다.


            그림 64. 다양한 필터링 모드에서 이동평균, MACD, ADX 및 EMA 전략의 잔고 및 평가금액 차트

            그림 64. 이동평균, MACD, ADX 및 EMA 전략의 잔고 및 평가금액 차트를 다양한 필터링 모드별로 보여줍니다.


            따라서 포지션 청산 시 매매 신호에 대한 L1 필터링은 트레이딩 시스템의 안정성을 높이고 단기 가격 변동에 대한 민감도를 줄이며 수익/위험 비율을 개선합니다. 기존 이동평균선과 비교했을 때 L1 필터는 일시적인 조정과 실제 추세 반전을 더 잘 구분하여 추세 추종 전략을 더욱 효율적으로 활용할 수 있도록 해줍니다.

            L1 추세 필터에 맞춰진 신호를 사용하여 매매할 때는 잔고 및 평가금액 곡선의 움직임에 주의를 기울여야 합니다(그림 64). 추세선을 따라 매매할 때 평가금액이 잔고보다 높은 경우가 많으며 이는 위험 지표를 크게 개선하고 드로다운을 줄여줍니다. 따라서 추세에 맞춰 매매하는 것은 트레이딩 시스템의 특성(드로다운 및 위험 감소)을 개선하는 데에도 도움이 됩니다.

            또한 매매 신호를 L1 추세에 맞춰 조정하면 매매 횟수는 줄어드는 반면 트레이딩의 질은 향상되어 매매 전략의 전반적인 통계적 특성에도 긍정적인 영향을 미칩니다.


            번호 전략 총 순이익(미화) % 매수와 보유
            1  매수와 보유 1363.8 100%
            2  이동평균(필터 없음) 1001.03 73.4%
            3  이동평균(L1 진입 필터) 107.65 7.89%
            4  이동평균(L1 청산 필터) 1342.5 98.43%
            5  이동평균(L1 진입 + 청산) 986.16 72.31%
            6  MACD (필터 없음) 997.79 73.16%
            7  MACD (L1 진입 필터) 140.13 10.27%
            8  MACD (L1 청산 필터) 1359.52 99.69%
            9  MACD (L1 진입 + 청산) 697.54 51.15%
            10  ADX(필터 없음) 791.99 58.07%
            11  ADX(L1 진입 필터) -50.9 -3.73%
            12  ADX(L1 청산 필터) 940.39 68.95%
            13  ADX (L1 진입 + 청산) 430.05 31.53%
            14  EMA (필터 없음) 957.3 70.19%
            15  EMA(L1 진입 필터) -173.35 -12.71%
            16  EMA(L1 청산 필터) 1258.99 92.31%
            17  EMA (L1 진입 + 청산) -131.41 -9.64%

            표 4. 이동평균, MACD, ADX, EMA 전략에서 L1 필터를 사용했을 때의 총 수익 결과와 장기 보유 전략과 비교


            표 4에 따르면 포지션 청산 시 L1 필터를 사용하면 모든 전략의 수익성이 향상되었습니다.

            매수 후 보유 전략의 결과($1363.8)를 전체 추세 변동의 100%로 간주하면 다음과 같은 결과를 얻습니다:

            1. 이동평균 수익률이 73.4%에서 98.43%로 증가;
            2. MACD 수익률이 73.16%에서 99.69%로 증가;
            3. ADX의 이익률은 58.07%에서 68.5%로 증가;
            4. EMA 수익률은 70.19%에서 92.31%로 증가.

            보시다시피, L1 필터를 사용함으로써 이동평균, MACD, EMA 전략의 수익률이 22~26% 증가했으며 추세 움직임의 대부분(98.43%, 99.69%, 92.31%)을 포착하여 장기 투자 전략의 결과에 근접했습니다. ADX 전략의 수익률이 10% 증가했습니다.

            예시에서는 가장 높은 균형값을 산출하는 매개변수를 가진 전략(즉, 가능한 최상의 솔루션 중 하나)을 살펴보았으며 파란색으로 강조 표시되어 있습니다. 결과는 가장 수익성이 높은 솔루션조차도 L1 추세와의 정렬을 통해 매매 신호를 추가로 필터링함으로써 더욱 개선될 수 있음을 보여줍니다. 일부 전략은 개선 효과가 미미했습니다(ADX의 경우 녹색 곡선이 파란색 곡선에 가까워 최적의 균형 솔루션에 근접했음을 나타냅니다). 전략의 매매 신호 품질(선택된 매개변수의 최적성)은 L1 필터링을 통해 얼마나 개선되는지로 판단할 수 있습니다.

            특히 이 경우에는 강한 추세를 보이는 EURUSD 시장을 고려했습니다(그림 62). 다른 시장 체제와 금융 상품의 경우 결과는 다를 것입니다. 또한, L1 추세는 정규화 매개변수 λ = 0.2·λmax를 사용하여 H1 시간 프레임에서 구축되었습니다. 다른 도구 및 시간 프레임의 경우 이 계수의 적절한 값은 L1 추세 지표를 사용하여 추정할 수 있습니다.


            결론

            L1 트렌드 필터링은 국부적인 노이즈를 실제 트렌드 변화에서 분리하는 도구로서 그 실용적인 유용성이 입증되었습니다.

            이 메서드는 λmax를 통해 자동 분기점과 편리한 조정 척도를 갖춘 구간별 선형 추세를 생성하므로 수동 매개변수 조정 문제를 해결합니다.

            실질적인 통합을 위해 완벽한 툴킷이 제공됩니다. 여기에는 λmax 및 L1 필터 계산 함수, 세 가지 지표(L1Trend, L1TrendSlope, L1TrendSlopeSign), 일곱 가지 L1 추세 변동성 지표(L1Volatility, L1VolatilitySmoothed, L1VolatilityAbsolute, L1VolatilityNormalized, L1VolatilityNormalizedSmoothed, L1VolatilityRegime, L1VolatilityRegimeColor), Expert Advisor 템플릿, 그리고 재현 가능한 테스트 프로토콜(필터 없음, 진입 필터, 청산 필터, 두 필터 모두 사용, 결과 저장 및 Python 시각화 스크립트의 네 가지 모드)이 포함됩니다.

            또한 L1 트렌드 필터는 머신러닝에서 데이터 라벨링에 사용될 수 있다는 점도 주목해야 합니다. 특히 "머신러닝을 이용한 추세 매매 전략 개발" 글에서는 Savitzky-Golay 필터로 평활화된 가격 미분값을 사용하여 추세를 결정합니다. L1 필터링을 사용하면 유사한 접근 방식을 구현할 수 있는데 이 방법에서는 추세가 구간별 선형 함수로 근사화되고 각 구간의 추세 강도는 해당 구간의 기울기와 자연스럽게 관련됩니다.


            실질적인 권장 사항:

            • 상대적 정규화를 사용합니다: λ = coef_lambda_max · λmax. 대부분의 작업에서 계수 값을 0.04~0.25 범위로 사용하세요. 더 세밀한 분석을 위해서는 ≈0.02~0.04, 대략적인 근사 및 영역 감지를 위해서는 ≈0.12~0.25를 사용하세요.
            • 대부분의 경우 L1 필터는 포지션 청산(수익성 있는 추세를 유지하고 조기 청산을 줄이는 것)에 적용할 때 가장 효과적입니다. 진입시 이 방법을 적용하면 매매 횟수는 줄어들지만 거래의 질은 그에 비례하여 향상되지 않는 경우가 많습니다.
            • 현재 추세 분석에는 간단한 규칙을 사용합니다. delta = x_filtered[last] − x_filtered[last−1]. delta의 부호는 지배적인 L1 추세의 방향을 나타냅니다.

            제한 사항: 효과는 사용된 도구, 기간 및 시장 상황에 따라 달라지므로 선택된 지표를 사용하여 과거 데이터에 대한 검증이 필요합니다.

            제안된 MQL5 모듈 및 테스트 프로토콜을 통해 특정 트레이딩 시스템에 대한 가설 검증 및 작동 매개변수 선택을 신속하게 수행할 수 있습니다.

            이 글에 있는 모든 코드는 공개 프로젝트 "MQL5\Shared Projects\ L1Trend "에서도 확인할 수 있습니다.


            예

            타입 파일 설명
            Script MQL5\Scripts\TestL1Trend.mq5
            모델 데이터(랜덤 워크)에 대한 L1 추세를 계산하는 테스트 스크립트
            Script MQL5\Scripts\TestL1TrendFloatDouble.mq5 double형 및 float형 벡터에 대한 모델 데이터(랜덤 워크)의 L1 추세를 계산하는 테스트 스크립트
            Script MQL5\Scripts\TestL1TrendFilterSP500.mq5 SP500 지수 시세 데이터의 L1 추세를 계산하는 테스트 스크립트
            데이터 파일 MQL5\Files\snp500.txt
            테스트 스크립트용 데이터 파일(SP500 지수 가격 시계열 로그)
            Script MQL5\Scripts\TestScalingBrownianMotion.mq5 브라운 운동에서 λmax의 멱법칙 의존성을 계산하는 스크립트
            Script MQL5\Scripts\TestScalingSymbol.mq5 주어진 종목의 가격 시계열에 대한 λmax의 멱법칙 의존성을 계산하는 스크립트
            지표 MQL5\Indicators\L1TrendFilter.mq5 L1 추세 계산용 지표
            지표
            MQL5\Indicators\L1TrendFilter_Slope.mq5 L1 추세의 변화율 계산용 지표
            지표
            MQL5\Indicators\L1TrendFilter_SlopeSign.mq5 L1 추세 변화 부호 계산용 지표
            지표
            MQL5\Indicators\L1Volatility.mq5 잔여 변동성(종가와 L1 추세값의 차이) 계산용 지표
            지표
            MQL5\Indicators\L1VolatilitySmoothed.mq5 평활화된 잔여 변동성을 계산하기 위한 지표
            지표
            MQL5\Indicators\L1VolatilityAbsolute.mq5 절대 변동성 계산용 지표
            지표
            MQL5\Indicators\L1VolatilityNormalized.mq5 ATR(평균 실제 범위)과 L1 추세를 사용하여 정규화된 변동성을 계산하는 지표
            지표
            MQL5\Indicators\L1VolatilityNormalizedSmoothed.mq5 평활화된 정규화 변동성 계산용 지표
            지표
            MQL5\Indicators\L1VolatilityRegime.mq5 시장 체제 감지 지표
            지표
            MQL5\Indicators\L1VolatilityRegimeColor.mq5 시장 체제 감지 지표의 컬러 버전
            Expert Advisor MQL5\Experts\MovingAverageFilteredL1.mq5 L1 필터를 적용한 이동평균 전략 기반의 Expert Advisor 거래
            Expert Advisor
            MQL5\Experts\MACDFilteredL1.mq5 L1 필터를 적용한 MACD 전략 기반의 Expert Advisor 거래
            Expert Advisor MQL5\Experts\ADXFilteredL1.mq5 L1 필터를 적용한 ADX 전략 기반의 Expert Advisor 거래
            Expert Advisor MQL5\Experts\EMAFilteredL1.mq5 L1 필터를 사용하여 두 EMA의 교차점을 기반으로 하는 Expert Advisor 거래
            파이썬 스크립트 MQL5\Scripts\PlotData.py 매매 신호에 L1 필터를 적용했을 때의 효과를 분석하는 파이썬 스크립트

            표 5. 이 글에 사용된 프로그램 코드에 대한 설명



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

            최근 코멘트 | 토론으로 가기 (9)
            Quantum
            Quantum | 20 4월 2026 에서 20:12
            Renat Akhtyamov #:

            대략 다음과 같습니다:

            추세 분리는 정규화 매개변수 lambda에 매우 크게 좌우됩니다. lambda가 작을수록 더 짧은 추세를 포착할 수 있습니다.
            여기서 살펴본 예시에서는 lambda=0.2*lambda_max 단위의 고정된 lambda 값이 사용되었습니다. 람다_맥스(lambda_max) 단위로 계산하면 데이터에 부분적으로 적응할 수 있습니다. 람다_맥스(lambda_max) 값 자체는 시계열의 기하학적 특성(상대적 변동폭), 즉 변동성에 따라 달라집니다.

            추세에는 다양한 단계와 고유한 수명 주기가 있다는 점을 염두에 두어야 합니다. 따라서 현재 추세에 맞춰 조정할 수 있는 메커니즘, 즉 람다를 관리하고 최적의 추세 분할을 찾아내는 방법이 필요하지만, 이 과제는 아직 해결되지 않았습니다.

            해당 시간대에서 전략 자체가 수익을 내지 못한다면, 필터도 도움이 되지 않을 것입니다.
            가장 좋은 결과는 이상적인 추세 시장에서 나와야 하며, 예시는 다음과 같았습니다: EURUSD, 2025, H1 (최적의 이동평균 매개변수: period=61).

            EURUSD

            테스터 옵션

            테스터 매개변수

            필터 적용 전 테스터 결과

            L1 종가 필터

            L1 클로즈 필터

            여기서 우리는 청산 필터가 추세 구간에서 수익을 늘리는 데 도움이 되었음을 확인할 수 있습니다.


            조정 구간에서 포지션을 추가한 동일한 전략의 변형:

            '추가' 기능을 사용한 테스터 옵션

            '포지션 추가' 기능을 사용한 테스터 옵션

            추가 없이:

            추가 시:


            횡보장 구간에는 국부적인 소규모 추세가 포함되어 있으며, 이를 올바르게 반영하려면 (출구 필터로 사용할 경우) 람다(lambda) 매개변수의 값을 더 작게 설정해야 합니다.
            또한, 횡보장 구간에서 이동평균(MovingAverage) 매개변수의 최적값도 달라야 합니다. 즉, 두 번째 구간에서 이동평균의 최적 기간이 변경되었습니다(하지만 테스터에서 최적화할 때, 도출된 매개변수들은 전체 최적화 구간에서 다른 모든 매개변수들 중 가장 높은 수익을 냅니다).

            Quantum
            Quantum | 20 4월 2026 에서 21:08

            다양한 람다 값을 사용하여 flat 구간에서 결과를 확인해 봅시다.

            필터 없음:

            출력 필터 lambda=0.2*lambda_max 적용 시

            lambda=0.001 lambda_max 필터 적용 시 (더 미세한 추세)

            이처럼 플랫 구간에서 lambda=0.001 lambda_max를 적용하면 필터가 없는 경우보다 결과를 개선하고 국부적인 미세한 추세를 반영할 수 있습니다.

            그러나 lambda=0.2*lambda_max 필터를 적용한 경우, 여기서는 필터를 적용하지 않은 전략보다 수익성이 더 낮게 나타났습니다.

            Quantum
            Quantum | 20 4월 2026 에서 21:30

            플랫 내부의 국지적 추세에서 포지션(다양한 람다)을 추가하는 전략

            필터 없음:


            lambda=0.2*lambda_max 필터를 적용하고 조정 국면에서 포지션을 추가하는 경우:

            lambda=0.001*lambda_max 필터를 적용하고 조정 구간에서 포지션을 추가한 경우:

            lambda=0.2*lambda_max 필터를 적용하고 조정 구간에서 포지션을 추가한 방식이 필터가 없는 방식보다 더 나은 결과를 보여주었습니다.

            플랫 구간 내부의 국소적인 소규모 추세(lambda=0.001*lambda_max)에 대한 조정 시 매수 추가를 적용함으로써, 필터가 없는 원래 전략의 수익을 높일 수 있었으며(lambda=0.2*lambda_max를 적용한 전략의 수익도 개선되었습니다).

            Renat Akhtyamov
            Renat Akhtyamov | 21 4월 2026 에서 08:06
            Quantum #:

            플랫 내부의 국지적 추세에서 포지션을 추가하는 방법 (다양한 람다)

            필터 없음:


            lambda=0.2*lambda_max 필터를 적용하고 조정 구간에서 포지션을 추가하는 경우:

            lambda=0.001*lambda_max 필터를 적용하고 보정값을 더한 경우:

            lambda=0.2*lambda_max인 필터를 적용하고 보정값에 추가한 변형이 필터를 적용하지 않은 변형보다 더 나은 결과를 보여주었다.

            플랫 구간 내의 국소적인 소규모 추세(lambda=0.001*lambda_max)를 보정에 추가함으로써, 필터가 없는 원래 전략의 수익을 높일 수 있었으며(lambda=0.2*lambda_max를 적용한 변형의 수익도 개선할 수 있었다).

            적어도 데모 계좌로라도 거래해 보세요

            경험을 쌓고 10개월간의 무익한 작업을 견뎌내면 이해가 올 것입니다

            Vladimir196803
            Vladimir196803 | 25 7월 2026 에서 23:50

            벡터에 대한 두 가지 새로운 기능을 소개해 주셔서 감사합니다. 정말 유용하네요. 많은 노력을 기울이셨지만, 안타깝게도 모든 지표가 심하게 재그리딩 현상이 발생합니다. 심지어 과거 데이터에서도 고점이 반대 방향으로 바뀌거나 사라지기도 합니다. 이 지표들을 재그리팅 현상이 있는 호드릭-프레스콧 필터와 비교하신 데는 일리가 있습니다 . 중심 조정된 TMA나 다른 몇 가지 지표와도 비교할 수 있겠네요. 더 긴 시간대에서는 300캔들 전으로 거슬러 올라가서 추세 전환점이 나타날 수 있어, 1년 동안(예금을 날리며) 추세와 반대 방향으로 거래할 수도 있습니다. 실제 거래에서는 지표나 이를 기반으로 한 자동매매 시스템 모두 미래를 예측하려 하기 때문에 적합하지 않습니다.

            사실 이러한 함수를 바탕으로 재그리지 않는 지표를 만들 수도 있습니다. 귀하의 지표의 경우, 단순히 1,000개의 캔들을 기다린 후 이전 기간의 끝에서 다시 계산을 하면 되지만, 이는 실제 적용에는 무용지물이며 더 흥미로운 대안들이 있습니다. 연구에 행운이 있기를 바랍니다.

            새로운 기능: MQL5의 커스텀 인디케이터 새로운 기능: MQL5의 커스텀 인디케이터
            MetaTrader5와 MQL5의 새로운 기능 전체를 나열하지는 않겠습니다. 종류도 많은 데다가, 별도의 설명이 필요한 기능들도 있거든요. 객체 지향 프로그래밍을 이용한 코드 작성법 또한 다음에 알아보도록 하겠습니다. 다른 기능들과 함께 설명하기에는 조금 어려운 이야기일 수 있으니까요. 이 글에서는 인디케이터와 인디케이터의 구조, 드로잉 타입과 프로그래밍 디테일을 MQL4와 비교해 볼게요. 초보자 분들께 많은 도움이 되면 좋겠고 기존에 사용하시던 개발자 분들도 뭔가 새로운 걸 얻어 가실 수 있길 바랍니다.
            10분 만에 MQL5 Algo Forge에 프로젝트 코드를 게시하세요: 단계별 가이드 10분 만에 MQL5 Algo Forge에 프로젝트 코드를 게시하세요: 단계별 가이드
            이 글에서는 게시된 프로젝트의 코드를 완전한 기능을 갖춘 MQL5 Algo Forge 프로젝트로 마이그레이션하는 방법에 대한 단계별 가이드를 제공합니다. MetaEditor에서 환경 및 인증을 설정하고 공유 프로젝트에서 프로젝트를 생성하고 유형을 선택하고 파일을 정리하고 README.md를 추가하고 인코딩과 빌드를 확인하고 변경 사항을 Git에 커밋하고 저장소를 공개적으로 엽니다. 이 문서는 독자의 편의를 위해 작업 구조를 구축하고 버전 기록을 보존하는 데 도움이 됩니다.
            새 MetaTrader 와 MQL5를 소개해드립니다 새 MetaTrader 와 MQL5를 소개해드립니다
            본 문서는 MetaTrader5의 간략 리뷰입니다. 짧은 시간 내에 시스템의 모든 세부 사항을 안내해드리기는 어렵습니다 - 테스트는 2009.09.09에 시작되었습니다. 이는 상징적인 일자로, 전 이것이 행운의 숫자가 될거라 믿어 의심치않습니다. 제가 새 MetaTrader 5 터미널과 MQL5 베타버전을 받은지 며칠이 지났습니다. 아직 모든 기능을 사용해본 것은 아니지만, 벌써부터 감명깊네요.
            "최적 패스"에서 견고한 솔루션으로: MetaTrader 5에서 최적화 표면 탐색하기 "최적 패스"에서 견고한 솔루션으로: MetaTrader 5에서 최적화 표면 탐색하기
            이 글에서는 MetaTrader 5의 Expert Advisor를 최적화하기 위한 엔지니어링적인 접근 방식을 살펴봅니다. 사용자 정의 지표 수집부터 최적화 프레임, 매개변수 표면 분석에 이르기까지의 과정을 다룹니다. 간단한 이벤트 기반 EMA/RSI 모델을 통해 Python에서 CSV 내보내기와 평활화 및 로컬 안정성 평가를 시연합니다. 목표는 구성의 안정적인 영역을 찾고 안정적인 구현을 위해 전방 최적화를 통해 이를 검증하는 것입니다.