English Русский Deutsch 日本語 Italiano Türkçe
preview
MQL5에서 CPU에서 GPU로: 연구, 최적화 및 패턴 분석 가속화를 위한 실용적인 OpenCL 프레임워크

MQL5에서 CPU에서 GPU로: 연구, 최적화 및 패턴 분석 가속화를 위한 실용적인 OpenCL 프레임워크

MetaTrader 5통합 |
30 0
MetaQuotes
MetaQuotes

소개

MQL5을 사용하며 CPU 에서 GPU로 전환하는 것은 당연한 수순처럼 보입니다. 그래픽 프로세서가 더 빠른 연산을 수행할 수 있다면 거래를 위한 분석 속도도 자동으로 향상될 것이기 때문입니다. 하지만 실제로는 모든 것이 훨씬 더 미묘합니다. GPU를 통해 실제로 성능이 상당히 향상될 수 있지만 이는 작업이 병렬 컴퓨팅 모델에 잘 맞는 경우에만 해당됩니다. 그렇지 않으면 속도 향상을 얻지 못하고 더욱 복잡한 아키텍처만 비슷하거나 더 높은 비용으로 만들게 될 수 있습니다.

이는 알고리즘 트레이딩에서 특히 중요합니다. 시장 데이터 분석, 여러 매개변수 대입, 대규모 가설 검증 및 반복 패턴 검색에는 종종 상당한 양의 계산이 필요합니다. 바로 이 지점에서 GPU의 잠재력이 드러납니다. GPU는 여러 요소에 대해 동일한 작업을 수행해야 하고 병렬 처리가 완료된 후 결과를 수집할 수 있는 경우에 강점을 보입니다. 이러한 시나리오에서 그래픽 카드는 단순한 장식용 장치가 아니라 완벽한 연산 장치로 변모합니다.

하지만 GPU를 사용하는 것도 대가가 따릅니다. 계산을 시작하기 전에 우리는 데이터를 준비해야 하고 장치로 전달해야 하고 커널이 호출되기를 기다린 다음 결과를 반환해야 합니다. 간단한 작업의 경우 이러한 로직은 너무 무거울 수 있습니다. CPU가 빠르게 작동하고 불필요한 오버헤드 비용 없이 작동하는 영역에서는 계산을 GPU로 옮겨도 이점이 없습니다. 때로는 오히려 방해가 되기도 하는데 특히 작업이 자주 바뀌거나 유연한 논리가 필요하거나 소량의 데이터와 관련된 경우 그렇습니다.

MQL5 환경에서 OpenCL은 애플리케이션 로직과 GPU를 연결하는 역할을 합니다. 이를 통해 계산에 많은 시간과 노력이 필요한 부분을 메인 프로그램의 밖으로 옮기고 GPU에서 일괄적으로 데이터 처리를 하도록 할 수 있습니다. 하지만 OpenCL 자체가 마법처럼 속도를 높여주는 버튼은 아닙니다. 작업 아키텍처가 처음부터 병렬 컴퓨팅의 특성을 고려하고 CPUGPU 간의 데이터 교환을 최소화할 때만 유용합니다.

여기서 GPU는 연산 회로의 별도 계층으로 간주되며 무겁고 반복적인 작업을 처리하도록 설계됩니다. 이 접근 방식은 연구, 최적화 및 패턴 찾기 작업과 같이 계산량이 많아서 연구자가 그 결과를 기다리기가 지쳐버리게 되는 경우에 유용합니다. 여기서 얻을 수 있는 실질적인 교훈은 간단합니다: 우리는 먼저 GPU로 전송할 가치가 있는 것이 무엇인지 정확히 이해해야 하며 그래야만 가속이 실질적인 효과를 발휘할 수 있다는 것입니다.

CPU vs GPU


환경 준비

OpenCL을 사용한 작업의 시작은 계산이 아니라 준비입니다. 먼저 프로그램은 사용 가능한 장치를 찾고 작업 컨텍스트를 생성하고 커널을 준비하고 데이터를 위한 메모리를 할당해야 합니다. 이 모든 것이 단순한 기술적 절차처럼 보이지만 바로 이 단계에서 생산성에 상당한 손실이 발생하는 경우가 많습니다.

가장 큰 실수는 GPU를 일반적인 함수 호출처럼 취급하는 것입니다: 즉 호출하고 결과를 얻고 계속해서 다음으로 넘어가는 방식과 같은 것입니다. 사실 그러한 요청의 뒤에서는 일련의 과정이 수반됩니다. 우리는 컨텍스트를 생성하거나 연결하고 프로그램을 준비하고 커널을 컴파일하고 메모리를 할당하고 데이터를 전송한 후에야 계산을 시작할 수 있습니다. 이렇게 계속 반복하면 GPU는 계산보다는 준비에 너무 많은 시간을 소모하게 됩니다.

따라서 제대로 구현된 경우 가능한 거의 모든 작업이 한 번에 완료됩니다. 컨텍스트는 미리 생성된 후 재사용됩니다. 코드가 변경되지 않으면 커널은 한 번만 컴파일 됩니다. 또한 꼭 필요한 경우가 아니면 메모리 버퍼를 새로 생성하지 않고 재사용하는 것이 좋습니다. 이러한 접근 방식은 오버헤드를 줄여 GPU를 활용한 작업을 매우 유용하게 만들어 줍니다.

이는 데이터 전송에도 동일하게 적용됩니다. GPU는 지속적으로 주고받는 수많은 작은 작업들을 처리하는 데에는 적합하지 않습니다. 이러한 접근 방식은 계산에 시간이 걸리는 게 아니라 데이터 교환에 시간이 걸리는 것입니다. 데이터를 한꺼번에 대량으로 수집하고 계산의 빈도를 낮추되 처리량을 높이는 것이 훨씬 효율적입니다. 한 번의 대규모 실행이 여러 번의 소규모 실행보다 거의 항상 더 나은 결과를 가져옵니다.

또 다른 흔한 문제는 불필요한 동기화입니다. 프로그램이 각 단계 후에 멈추고 GPU가 완료되기를 기다리면 장치는 유휴 상태가 되며 전체적인 가속 효과를 감소시킵니다. GPU가 작업을 받고 불필요한 중단 없이 완료하고 정말 필요할 때만 결과를 반환하도록 작업을 구성하는 것이 더 좋습니다.

이는 특히 MQL5에서 중요합니다. 매매 프로그램은 지연에 민감합니다. 아키텍처 설계의 허술함은 금방 눈에 띄게 됩니다. 컨텍스트가 지속적으로 재구성되고 메모리가 무질서하게 할당되며 매 계산마다 커널이 컴파일 된다면 GPU는 가속기가 아닌 지연이 일어나는 원인이 됩니다.

그러므로 여기서 가장 중요한 실질적인 원칙은 매우 간단합니다. 미리 준비할 수 있는 것은 무엇이든 미리 준비해야 한다는 것입니다. 재사용할 수 있는 것은 무엇이든 다시 만들어지지 말아야 합니다. 이러한 환경이 잘 정돈되어 있으면 OpenCL은 복잡한 계산 속도를 크게 향상시켜 줍니다. 이러한 준비가 부재할 경우 계산을 시작하기도 전에 그 이점을 쉽게 잃어버리게 됩니다.


메모리 관리

GPU라고 하면 많은 사람들이 주로 연산 능력만을 떠올립니다. 하지만 실제로는 계산 속도보다는 데이터가 어떻게 정확히 제공되는지가 더 중요한 경우가 많습니다. 아무리 빠른 GPU라도 메모리 처리에 어려움을 겪으면 좋은 결과를 낼 수 없습니다.

OpenCL에서 메모리는 단순히 데이터가 저장되는 장소 이상의 의미를 지닙니다. 성능은 메모리가 어떻게 사용되는지에 직접적으로 좌우됩니다. GPU에는 여러 단계의 메모리가 있습니다. 어떤 메모리는 더 빨리 일하고 어떤 메모리는 더 느리게 일합니다. 작은 내부의 메모리 영역은 빠르게 접근할 수 있지만 기기의 메인 메모리에 접근하는 것은 훨씬 더 많은 비용이 듭니다.

이는 중요한 기본 원칙을 의미합니다. 즉 GPU가 속도가 느린 메모리에 접근하는 횟수를 최소화하도록 데이터를 구성해야 합니다. 불필요한 읽기와 쓰기가 적을수록 좋은 것입니다. 특히 CPU와 GPU 간에 동일한 데이터를 불필요하게 반복적으로 전송하지 않는 것이 중요합니다. 종종 병목 현상을 일으키는 주요 원인은 연산 자체보다는 데이터의 교환입니다.

GPU 메모리 구성

간단히 말해 GPU는 대용량 데이터 배열을 한 번만 전송하고 유사한 작업을 여러 번 수행한 후 최종 결과를 반환하게 하는 작업에 적합합니다. GPU를 사용하는 프로그램은 지속적으로 작은 데이터 조각을 전송하거나 각 단계 후에 응답을 기다리는 상황을 좋아하지 않습니다. 이러한 모드에서는 가속력이 빠르게 사라집니다.

이는 특히 트레이드의 목적상 매우 민감한 사항입니다. 프로그램이 각 단계에서 먼저 데이터를 준비하고 전송하고 결과를 기다리고 이후 다시 이 모든 과정을 반복한다면 성능이 불안정해질 것입니다. 필요한 데이터를 미리 수집하여 하나의 데이터 블록으로 장치에 전송하고 계산을 수행한 후 그 결과를 메인 프로그램에서 사용하는 것이 훨씬 더 좋습니다.

그리고 이때 역할을 올바르게 분담하는 것이 중요합니다. CPU는 여전히 제어 센터의 역할을 합니다: 데이터를 수집하고 계산을 시작하며 최종 결정을 내립니다. GPU는 동일한 작업을 여러 번 빠르게 반복해야 하는 부분을 담당합니다. 이러한 구조는 모든 데이터를 하나의 장치로 옮기려는 시도보다 거의 항상 더 안정적이고 효율적입니다.

또 다른 흔한 실수는 너무 작은 커널 실행 크기입니다. 언뜻 보면 편리해 보입니다: 새로운 데이터가 도착하면 즉시 GPU 로 전송됩니다. 하지만 보낼 때마다 시간이 소요됩니다. 보내는 횟수가 너무 많으면 간접비가 모든 이익을 잠식하기 시작합니다. 따라서 대부분의 경우 GPU를 자주 실행하는 대신에 더 크고 단순한 작업을 하도록 하는 것이 더 좋습니다.

결과적으로 메모리 작업은 사소한 기술적 세부 사항이 아니라 성능을 좌우하는 주요 요소 중 하나입니다. 데이터가 올바르게 구성되어 있으면 GPU는 파이프라인처럼 작동합니다. 그렇지 않으면 아무리 강력한 기기라도 불필요한 요청과 전송에 시간을 낭비하며 제대로 작동하지 못할 것입니다.

결론: GPU는 데이터가 올바르게 들어왔을 때만 계산 속도를 향상시킵니다. 불필요한 전송, 요청 및 소규모 실행이 적을수록 실질적인 효과는 높아집니다.


프로그램 구축

응용 프로그램에서 CPUGPU는 서로 경쟁하는 것이 아니라 협력하여 작동합니다. CPU는 여전히 제어 센터의 역할을 하며 초기 데이터를 생성하고 필요한 메서드를 호출하고 결과를 받아들이고 실행 시간을 비교합니다. GPU는 연산 부분만 담당합니다. 이것은 고전적인 구조입니다: 프로그램 전체를 기기로 옮기는 대신 실제로 병렬 처리가 필요한 부분만 기기에 전달해야 합니다.

OpenCL을 사용하여 프로그램을 구축하는 방법을 가장 쉽게 이해하는 방법은 구체적인 예제를 사용해보는 것입니다. MQL5 표준 라이브러리는 행렬 곱셈과 관련한 일러스트레이션 방식의 예시 구현을 제공합니다. 메인 프로그램에서는 먼저 두 개의 행렬을 생성하고 임의의 값으로 채웁니다.

void OnStart()
  {
//--- matrix A 1000x2000
   int rows_a = 1000;
   int cols_a = 2000;
//--- matrix B 2000x1000
   int rows_b = cols_a;
   int cols_b = 1000;
//--- matrix C 1000x1000
   int rows_c = rows_a;
   int cols_c = cols_b;
//--- matrix A: size=rows_a*cols_a
   int size_a = rows_a * cols_a;
   int size_b = rows_b * cols_b;
   int size_c = rows_c * cols_c;
//--- prepare matrix A
   float matrix_a[];
   ArrayResize(matrix_a, rows_a * cols_a);
   for(int i = 0; i < rows_a; i++)
      for(int j = 0; j < cols_a; j++)
        {
         matrix_a[i * cols_a + j] = (float)(10 * MathRand() / 32767);
        }
//--- prepare matrix B
   float matrix_b[];
   ArrayResize(matrix_b, rows_b * cols_b);
   for(int i = 0; i < rows_b; i++)
      for(int j = 0; j < cols_b; j++)
        {
         matrix_b[i * cols_b + j] = (float)(10 * MathRand() / 32767);
        }

먼저 CPU에서 순차적인 계산이 수행된 후 동일한 계산이 GPU에서 수행됩니다.

//--- CPU: calculate matrix product matrix_a*matrix_b
   float matrix_c_cpu[];
   ulong time_cpu = 0;
   if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu))
     {
      PrintFormat("Error in calculation on CPU. Error code=%d", GetLastError());
      return;
     }
//--- calculate matrix product using GPU
   float matrix_c_gpu_method1[];
   float matrix_c_gpu_method2[];
   ulong time_gpu_method1 = 0;
   ulong time_gpu_method2 = 0;
   if(!MatrixMult_GPU(matrix_a, matrix_b, matrix_c_gpu_method1, matrix_c_gpu_method2,
       rows_a, cols_a, cols_b, size_a, size_b, size_c, time_gpu_method1, time_gpu_method2))
     {
      PrintFormat("Error in calculation on GPU. Error code=%d", GetLastError());
      return;
     }

계산 자체는 개별 메서드로 수행됩니다. GPU 부분은 동일한 문제에 대한 두 가지 구현 방식으로 제시됩니다: 단순 구현과 최적화된 구현. 이를 통해 우리는 코드와 실행 시간의 차이를 즉시 확인할 수 있습니다.

이제 CPU 버전을 살펴보겠습니다. 여기서는 모든 것이 매우 명확합니다: 전형적인 삼중 반복문입니다. 결과 행렬의 각 요소는 순차적으로 계산됩니다.

bool MatrixMult_CPU(const float &matrix_a[], const float &matrix_b[], float &matrix_c[],
                    const int rows_a, const int cols_a, const int cols_b, ulong &time_cpu)
  {
   int size = rows_a * cols_b;
   if(ArrayResize(matrix_c, size) != size)
      return(false);
//--- CPU calculation started
   time_cpu = GetMicrosecondCount();
   for(int i = 0; i < rows_a; i++)
     {
      for(int j = 0; j < cols_b; j++)
        {
         float sum = 0.0;
         for(int k = 0; k < cols_a; k++)
           {
            sum += matrix_a[cols_a * i + k] * matrix_b[cols_b * k + j];
           }
         matrix_c[cols_b * i + j] = sum;
        }
     }
//--- CPU calculation finished
   time_cpu = ulong((GetMicrosecondCount() - time_cpu) / 1000);
//---
   return(true);
  }

이 코드는 이 작업의 기본 개념을 보여줍니다. 행렬이 두 개 있습니다. 행과 열별 곱의 합계가 있습니다. 순차적 실행이 있습니다. CPU에서는 별도의 준비 작업 없이 투명하게 작동합니다. 하지만 바로 여기에 주요한 한계가 있습니다: 행렬 크기가 커지는 순간 순차 계산 비용이 점점 더 많이 들게 됩니다.

다음은 GPU 부분입니다. 여기서 가장 중요한 원칙을 강조하고자 합니다: 이 예제에서 OpenCL은 별도의 메서드에 숨겨져 있습니다. 메인 프로그램은 단순하게 유지되며 모든 GPU 처리는 한 곳에 집중됩니다.

bool MatrixMult_GPU(const float &matrix_a[], const float &matrix_b[], float &matrix1_c[], float &matrix2_c[],
                    const int rows_a, const int cols_a, const int cols_b, const int size_a, const int size_b,
                    const int size_c, ulong &time1_gpu, ulong &time2_gpu)
  {
   const int task_dimension = 2;
//--- prepare matrices for result
   if(ArrayResize(matrix1_c, size_c) != size_c || ArrayResize(matrix2_c, size_c) != size_c)
      return(false);
   ArrayFill(matrix1_c, 0, size_c, (float)0.0);
   ArrayFill(matrix2_c, 0, size_c, (float)0.0);

여기서 우리는 중요한 점을 바로 알 수 있습니다. 두 가지 GPU 옵션에 대한 결과 행렬이 미리 준비되어 있다는 것입니다. 이렇게 하면 계산과 메모리 준비가 섞이는 것을 방지할 수 있습니다. 이 단계에서는 프로그램이 이미 체계적인 방식으로 구성되어 있습니다: 먼저 배열 할당이 이루어지고 그 다음 OpenCL이 실행됩니다.

다음으로 OpenCL 컨텍스트가 생성되고 초기화됩니다.

//--- OpenCL
   ulong timei_gpu = GetMicrosecondCount();
   COpenCL OpenCL;
   if(!OpenCL.Initialize(cl_program, true))
     {
      PrintFormat("Error in OpenCL initialization. Error code=%d", GetLastError());
      return(false);
     }

바로 이 지점에서 OpenCL의 실질적인 의미가 명확해 집니다. 이전까지는 프로그램은 일반적인 MQL5 코드였습니다. 그러나 이제는 GPU를 위한 컴퓨팅 환경을 준비하고 있는 것입니다. 특히 중요한 것은 이것이 무료로 실행되는 것이 아니라는 점입니다. 우리는 초기화 시간을 별도로 측정합니다. 가속화에 대해 이야기하기 전에 먼저 환경 자체를 구축하는 데 드는 비용이 얼마나 되는지 솔직하게 살펴봐야할 필요가 있습니다.

이후 두 개의 커널이 생성됩니다. 첫 번째는 간단한 병렬 변형입니다. 두 번째는 로컬 그룹들을 활용하는 더욱 발전된 방식입니다.

//--- create kernels
   OpenCL.SetKernelsCount(2);
   OpenCL.KernelCreate(0, "MatrixMult_GPU1");
   OpenCL.KernelCreate(1, "MatrixMult_GPU2");

여기서 주목할 점은 OpenCL 프로그램에 사용된 알고리즘의 품질에 따라 실행 시간이 크게 좌우된다는 것입니다. 우리는 계산을 병렬화 하거나 메모리 관리를 개선할 수 있습니다 예시에는 명확한 이해를 위해 두 가지 옵션이 모두 포함되어 있습니다.

다음으로 버퍼가 준비됩니다. 입력 행렬들은 장치로 복사되고 결과값을 저장할 별도의 버퍼가 생성됩니다.

//--- create buffers
   OpenCL.SetBuffersCount(3);
//---
   if(!OpenCL.BufferFromArray(0, matrix_a, 0, size_a, CL_MEM_READ_ONLY))
     {
      PrintFormat("Error in BufferFromArray for matrix A. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferFromArray(1, matrix_b, 0, size_b, CL_MEM_READ_ONLY))
     {
      PrintFormat("Error in BufferFromArray for matrix B. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferCreate(2, size_c * sizeof(float), CL_MEM_WRITE_ONLY))
     {
      PrintFormat("Error in BufferCreate for matrix C. Error code=%d", GetLastError());
      return(false);
     }

이것은 이미 실제로 작동하는 GPU 구조입니다. 데이터는 기기로 전송되어 거기서 계산된 후 다시 반환됩니다. 가속을 가능하게 하는 것은 바로 이러한 순서입니다. 만약 순서를 작게 나누면 GPU는 계산 자체보다는 준비와 데이터 교환에 너무 많은 시간을 소비하게 될 것입니다.

이후 첫 번째 커널의 인수가 설정됩니다.

//--- prepare arguments for kernel 0
   int kernel_index = 0;
   OpenCL.SetArgumentBuffer(kernel_index, 0, 0);
   OpenCL.SetArgumentBuffer(kernel_index, 1, 1);
   OpenCL.SetArgumentBuffer(kernel_index, 2, 2);
   OpenCL.SetArgument(kernel_index, 3, rows_a);
   OpenCL.SetArgument(kernel_index, 4, cols_a);
   OpenCL.SetArgument(kernel_index, 5, cols_b);
   timei_gpu = ulong((GetMicrosecondCount() - timei_gpu) / 1000);
   PrintFormat("time of initialization GPU =%d ms", timei_gpu);

여기서의 논리는 매우 간단합니다: 커널이 데이터 버퍼와 행렬 크기를 가져옵니다. 어떤 스레드가 어떤 요소를 담당할지는 OpenCL 커널 내부에서 결정됩니다. 이 부분이 중요한 부분입니다: GPU 자체는 배열을 해석하는 방법을 알지 못합니다. 이러한 구조가 명확하게 전달되어야 합니다.

그런 다음 문제의 크기가 설정되고 첫 번째 계산 옵션이 설정됩니다.

//--- set task dimension a_rows x b_cols
   uint global_work_size[2];
//--- set dimensions
   global_work_size[0] = rows_a;
   global_work_size[1] = cols_b;
   uint global_work_offset[2] = {0, 0};
//--- GPU calculation start kernel 0
   time1_gpu = GetMicrosecondCount();
   if(!OpenCL.Execute(kernel_index, task_dimension, global_work_offset, global_work_size))
     {
      PrintFormat("Error in Execute. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferRead(2, matrix1_c, 0, 0, size_c))
     {
      PrintFormat("Error in BufferRead for matrix1 C. Error code=%d", GetLastError());
      return(false);
     }
//--- GPU calculation finished
   time1_gpu = ulong((GetMicrosecondCount() - time1_gpu) / 1000);

이것이 첫 번째 GPU 옵션입니다. 이는 기본적인 개념을 보여줍니다. 즉 하나의 스레드가 하나의 출력 요소를 계산합니다. 이 구조는 이미 병렬 처리를 하고 있지만 메모리 최적화의 모든 능력을 아직 활용하지는 못하고 있습니다. 그래서 예제에서 그 옆에 두 번째 커널이 있는 것입니다. 실행하기 전에 인수는 동일한 방식으로 지정되지만 실행 방식은 다릅니다 - 로컬 작업 그룹을 사용합니다.

//--- prepare arguments for kernel 1
   kernel_index = 1;
//--- set arguments
   OpenCL.SetArgumentBuffer(kernel_index, 0, 0);
   OpenCL.SetArgumentBuffer(kernel_index, 1, 1);
   OpenCL.SetArgumentBuffer(kernel_index, 2, 2);
   OpenCL.SetArgument(kernel_index, 3, rows_a);
   OpenCL.SetArgument(kernel_index, 4, cols_a);
   OpenCL.SetArgument(kernel_index, 5, cols_b);
   uint local_work_size[2];
   local_work_size[0] = BLOCK_SIZE;
   local_work_size[1] = BLOCK_SIZE;

실질적인 차이가 드러나는 부분이 바로 여기입니다. 첫 번째 버전은 단순히 계산을 병렬화 하는 것입니다. 두 번째 방법은 이미 계산을 블록 단위로 구성합니다. 이는 처음 별 생각 없이 봤을 때보다 훨씬 더 중요합니다. GPU는 병렬 처리 뿐만 아니라 효율적으로 메모리를 구성하는 것도 중요하게 생각합니다. 따라서 블록과 로컬 그룹은 상당한 이점을 제공합니다.

두 번째 커널 실행 메서드에 작업 그룹의 차원을 추가합니다.

//--- GPU calculation start, kernel1
   time2_gpu = GetMicrosecondCount();
   if(!OpenCL.Execute(kernel_index, task_dimension, global_work_offset, global_work_size, local_work_size))
     {
      PrintFormat("Error in Execute. Error code=%d", GetLastError());
      return(false);
     }
   if(!OpenCL.BufferRead(2, matrix2_c, 0, 0, size_c))
     {
      PrintFormat("Error in BufferRead for matrix2 C. Error code=%d", GetLastError());
      return(false);
     }
//--- GPU calculation finished
   time2_gpu = ulong((GetMicrosecondCount() - time2_gpu) / 1000);
//--- remove OpenCL objects
   OpenCL.Shutdown();
//---
   return(true);
  }

이제 가장 흥미로운 것은 OpenCL 코드 내부에서 무슨 일이 일어나는가 입니다. 첫 번째 커널 버전은 가능한 한 단순하게 만들어졌습니다.

__kernel void MatrixMult_GPU1(__global float *matrix_a,
                              __global float *matrix_b,
                              __global float *matrix_c,
                              int rows_a, int cols_a, int cols_b)
  {
   int i = get_global_id(0);
   int j = get_global_id(1);
   float sum = 0.0;
   for(int k = 0; k < cols_a; k++)
     {
      sum += matrix_a[cols_a * i + k] * matrix_b[cols_b * k + j];
     }
   matrix_c[cols_b * i + j] = sum;
  }

이는 수학적 논리를 GPU로 거의 문자 그대로 옮긴 것입니다. 각 스레드는 자신의 좌표를 받아 결과 행렬의 한 요소를 계산합니다.

두 번째 버전이 벌써 더 흥미롭습니다. 로컬 배열과 스레드 동기화를 사용합니다.

__kernel void MatrixMult_GPU2(__global float *matrix_a,
                              __global float *matrix_b,
                              __global float *matrix_c,
                              int rows_a, int cols_a, int cols_b)
  {
   int group_i = get_group_id(0);
   int group_j = get_group_id(1);
   int i = get_local_id(0);
   int j = get_local_id(1);
   __local float submatrix_a[BLOCK_SIZE][BLOCK_SIZE];
   __local float submatrix_b[BLOCK_SIZE][BLOCK_SIZE];
   int offset_b = BLOCK_SIZE * group_i;
   int offset_a_start = cols_a * BLOCK_SIZE * group_j;
   float sum = (float)0.0;

여기서는 계산이 다르게 된다는 것이 이미 분명합니다. 스레드들은 그룹으로 묶이고 데이터는 블록의 내부 메모리에 로드 됩니다. 이렇게 하면 글로벌 메모리 호출 횟수가 줄어들어 장치가 더욱 효율적으로 작동합니다.

다음으로 조각들이 로드되고 스레드가 동기화됩니다.

   for(int offset_a = offset_a_start;
       offset_a < offset_a_start + cols_a;
       offset_a += BLOCK_SIZE,
       offset_b += BLOCK_SIZE * cols_b)
     {
      submatrix_a[i][j] = matrix_a[offset_a + cols_a * i + j];
      submatrix_b[i][j] = matrix_b[offset_b + cols_b * i + j];
      barrier(CLK_LOCAL_MEM_FENCE);
      for(int k = 0; k < BLOCK_SIZE; k++)
         sum += submatrix_a[i][k] * submatrix_b[k][j];
      barrier(CLK_LOCAL_MEM_FENCE);
     }

GPU 구현 방식이 두 가지인 이유에 대해 답변하자면 다음과 같습니다. 첫 번째 것은 병렬 처리를 보여줍니다. 두 번째 것은 메모리 최적화를 보여줍니다. 그리고 실제 작업에서는 바로 이 지점에서 가속도의 운명을 결정짓는 경우가 많습니다.

계산 결과는 최종 출력 배열에 다시 기록됩니다.

   int offset_c = BLOCK_SIZE * (cols_b * group_j + group_i);
   matrix_c[offset_c + cols_b * i + j] = sum;
  };

이 두 커널은 메인 프로그램에서 실행 시간을 기준으로 비교되며 계산 정확도는 CPU 옵션과 비교하여 제어됩니다.

//--- calculate CPU/GPU ratio
   double CPU_GPU_ratio1 = 0;
   double CPU_GPU_ratio2 = 0;
   if(time_gpu_method1 != 0)
      CPU_GPU_ratio1 = 1.0 * time_cpu / time_gpu_method1;
   if(time_gpu_method2 != 0)
      CPU_GPU_ratio2 = 1.0 * time_cpu / time_gpu_method2;
   PrintFormat("time CPU=%d ms, time GPU global work groups =%d ms, CPU/GPU ratio: %f",
                                           time_cpu, time_gpu_method1, CPU_GPU_ratio1);
   PrintFormat("time CPU=%d ms, time GPU local work groups  =%d ms, CPU/GPU ratio: %f",
                                           time_cpu, time_gpu_method2, CPU_GPU_ratio2);
   PrintFormat("time matrix CPU=%d ms", time_mat);

완전성을 기하기 위해 내장 행렬 연산도 비교 대상에 추가해 보겠습니다.

//--- matrix
   matrix<float> A, B, C;
   if(!A.Assign(matrix_a) || !B.Assign(matrix_b))
     {
      PrintFormat("Error of copy data to matrices. Error code=%d", GetLastError());
      return;
     }
   if(!A.Reshape(rows_a, cols_a) || !B.Reshape(rows_b, cols_b))
     {
      PrintFormat("Error of copy data to matrices. Error code=%d", GetLastError());
      return;
     }
   ulong time_mat = GetMicrosecondCount();
   C = A.MatMul(B);
   time_mat = ulong((GetMicrosecondCount() - time_mat) / 1000); 

실제 실험에서 CPU와 두 개의 GPU로 구성된 하이브리드 시스템에서 행렬 곱셈의 시간을 비교했습니다: NVIDIA GeForce RTX 4060 노트북 GPUIntel Iris Xe 그래픽. 단순한 CPU 구현을 기준으로 사용했을 때 약 2056~2180ms의 시간이 소요되었습니다. 여기서 주목할 만한 점은 최적화된 행렬 연산이 약 32~34ms의 안정적인 시간을 보였다는 것입니다. 이는 해당 등급의 CPU에서 벡터화 처리에 대해 기대되는 성능 수준에 근접한 것으로 볼 수 있습니다.

1000*2000 행렬 곱셈 결과

GPU로 계산을 옮겼을 때 결과는 엇갈렸는데 이는 OpenCL의 효율성이 컴퓨팅 블록을 어떻게 구성하는지에 크게 좌우된다는 것을 보여줍니다. RTX 4060의 경우 글로벌 워크 그룹을 사용했을 때 실행 시간은 약 38ms였는데 이는 CPU 행렬 연산에 비해 형식적으로 이점이 없다고 볼 수 있습니다. 하지만 로컬 작업 그룹으로의 전환은 상황을 완전히 바꿔 놓았습니다 - 소요 시간은 11ms로 단축되었습니다. 이는 로컬 메모리의 데이터 재사용을 통해 전역 메모리 부담을 줄이고 GPU 연산 장치를 더욱 효율적으로 활용할 수 있게 해주는 타일형 접근 방식의 완전한 작동을 이미 보여줍니다.

이와 유사하지만 더욱 두드러지게 나타나는 것은 Intel Iris Xe 통합 그래픽입니다. 글로벌 그룹 모드에서는 실행 시간이 213ms였지만 로컬 그룹을 사용했을 때는 45ms로 단축되었습니다. 일반적으로 외장 GPU에 비해 성능이 다소 떨어지는 현상이 지속됨에도 불구하고 여기서는 상대적인 가속 효과가 더욱 두드러지게 나타나는데 이는 생산성이 낮은 GPU가 메모리 액세스 최적화에 얼마나 민감한지를 보여줍니다.

GPU 초기화 시간은 특별히 주목할 만한 부분입니다. RTX 4060의 경우 약 99ms였고 Iris Xe의 경우 약 4ms였습니다. 이 요소는 전체 계산 파이프라인의 효율성에 영향을 미치므로 애플리케이션 시나리오에서 무시할 수 없습니다.

전반적으로 결과는 메모리 제약형 실행 모드에서 연산 효율형 실행 모드로의 전환이라는 전형적인 양상을 보여줍니다. CPU는 정해진 작업 규모에서 여전히 경쟁력이 있지만 GPU는 로컬 컴퓨팅 장치를 올바르게 구성했을 때만 그 우위를 발휘합니다. 이는 특히 RTX 4060에서 확연히 드러나는데 최적화되지 않은 구현과 최적화된 구현 간의 성능 차이가 약 3~4배에 달하여 그래픽 가속기의 비효율적인 사용과 완벽한 사용을 구분하는 경계가 명확하게 나타납니다.


캔들스틱 패턴 테스트

행렬 곱셈의 예시는 GPU가 계산 속도를 어떻게 향상시킬 수 있는지를 잘 보여줍니다. 하지만 이것 만으로는 트레이더에게 충분하지 않습니다. 이 접근 방식이 시장과 관련되어 적용될 수 있는지 여부를 이해하는 것이 더 중요합니다. 이제 다음 단계는 추상적인 수학에서 캔들스틱 패턴 분석으로 넘어가는 것입니다.

캔들스틱 패턴은 일반적으로 잘 알려진 이름을 가진 미리 만들어진 형태를 말합니다. 사진으로 보면 쉽게 알아볼 수 있고 책에서는 종종 그럴듯하게 보입니다. 하지만 이를 좀 더 엄밀하게 살펴보면 의문이 제기됩니다: 그러한 모델들이 실제로 통계적으로 얼마나 뒷받침되는 것인가? 정확한 기준은 어디에 있나? 패턴이 이론상으로만 효과가 있는 것이 아니라 실제 데이터에서도 효과가 있는지 어떻게 알 수 있는가?

책에서 익숙한 형태들을 미리 찾아보는 대신 문제를 다른 방식으로 접근할 수 있습니다. 시장에 이미 나와 있는 기성 템플릿을 적용하지 말고 이전에 유사한 상황에서 어떻게 작동했는지 살펴보세요.

최근 몇 개의 캔들 - 즉 현재 시장 상황을 나타냅니다. 이것이 우리의 참고 패턴입니다. 그런 다음 프로그램은 히스토리를 살펴보고 모양이 유사한 영역을 찾습니다. 비교는 패턴의 이름이 아니라 실제 캔들의 특징에 따라 이루어집니다: 몸통, 위쪽 그림자, 아래쪽 그림자를 기준으로 합니다. 게다가 시장은 거의 똑같은 패턴을 완벽하게 반복하지 않기 때문에 약간의 편차는 허용됩니다.

만약 히스토리에 유사한 부분이 있다면 프로그램은 추측을 하지 않습니다. 결과를 확인합니다. 이 상황에서는 목표 수익(Take Profit)손절매(Stop Loss) 수준 및 시간 제한을 설정하여 매매를 시뮬레이션 합니다. 그런 다음 과거에 그러한 사례들이 수익으로 끝났는지 손실로 끝났는지를 계산합니다.

바로 이 지점에서 OpenCL이 특히 유용해집니다. 이 작업에는 유사한 검사가 많이 포함되어 있습니다: 히스토리의 여러 시점을 살펴보고 현재 템플릿과 비교한 다음 각 옵션에 대한 매매 결과를 계산해야 합니다. CPU를 사용하면 가능하지만 데이터 양이 많아지면 계산이 어려워집니다. 반대로 GPU의 경우 이는 자연스러운 부하입니다: 병렬로 수행될 수 있는 여러 독립적인 계산이 있기 때문입니다.

더욱이 테스트는 거래 매개변수의 한 가지 조합 뿐만 아니라 전체 옵션 목록을 한 번에 테스트합니다. 즉 프로그램은 수익 실현 가격손절매 가격의 다양한 값을 동시에 고려합니다. 이를 통해 우리는 유사한 시장 상황에서 어떤 매개변수가 가장 합리적이었는지를 평가할 수 있습니다.

먼저 OpenCL 쪽의 논리부터 살펴보겠습니다 - 바로 이 부분에서 작업이 실제 형태를 갖추게 됩니다. 이 설계에서 CPU는 여전히 지휘자의 역할을 하지만 검색, 비교, 모델링과 같은 모든 어려운 작업은 GPU가 수행합니다.

이것이 분석 파이프라인입니다.

__kernel void PatternStats3D(__global const float4 *price,
                             __global const float  *tp,
                             __global const float  *sl,
                             __global float        *global_stats,
                             const int bars,
                             const float tolerance,
                             const int horizon)
  {
   const int lid = get_local_id(0);
   const int itp = get_global_id(1);
   const int isl = get_global_id(2);
   const int total_loc = get_local_size(0);
   const int tp_count = get_global_size(1);
   const int sl_count = get_global_size(2);

원본 데이터는 매우 간결한 형태로 정리되어 있습니다. 히스토리는 float4 벡터 배열로 전달됩니다. 각 항목은 시가, 고가, 저가, 종가를 나타내는 캔들입니다. 이것은 중요합니다. 우리는 데이터를 별도의 배열로 분리하지 않으며 접근 방식을 복잡하게 만들지 않습니다. GPU는 밀집된 구조에서 더 잘 작동하며 이 경우에는 그 장점을 최대한 활용합니다.

tpsl 배열은 각각 별도로 전달됩니다. 이와 같이 우리는 곧바로 과제의 두 번째와 세 번째 차원을 설정하게 됩니다. 각 스레드는 자체적인 히스토리 부분 뿐만 아니라 특정한 매매 매개변수 조합과도 연관되어 작동합니다. 결과적으로 계산 공간은 3차원이 됩니다: history × TP × SL.

그러면 본격적인 작업이 시작됩니다. 그룹 내의 각 스레드는 자체적인 lid를 가지며 그룹의 크기와 같은 단계로 히스토리를 탐색하기 시작합니다.

   __local int buf_stat[BLOCK_SIZE][STAT_DIM];
   int local_stat[STAT_DIM];
   for(int i=0;i<STAT_DIM;i++)
     local_stat[i]=0;
//---
   float4 pattern[PATTERN_SIZE];
   if(bars < (PATTERN_SIZE + horizon))
      return;
   for(int i = 0; i < PATTERN_SIZE; i++)
      pattern[i] = price[bars - 1 - PATTERN_SIZE + i];
//--- border
   for(int i = lid; i < (bars - horizon - PATTERN_SIZE); i += total_loc)
     {
      bool match = true;
      //--- pattern check
      for(int k = 0; k < PATTERN_SIZE ; k++)
        {
         float4 a = price[i + k];
         float body_a  = a.w - a.x;
         float body_b  = pattern[k].w - pattern[k].x;
         if(fabs(body_a - body_b) > tolerance)
           {
            match = false;
            break;
           }
         float upper_a = a.y - fmax(a.w, a.x);
         float upper_b = pattern[k].y - fmax(pattern[k].w, pattern[k].x);
         if(fabs(upper_a - upper_b) > tolerance)
           {
            match = false;
            break;
           }
         float lower_a = fmin(a.w, a.x) - a.z;
         float lower_b = fmin(pattern[k].w, pattern[k].x) - pattern[k].z;
         if(fabs(lower_a - lower_b) > tolerance)
           {
            match = false;
            break;
           }
        }

이것은 고전적인 방법입니다. 각각의 바마다 별도의 스레드를 만들지는 않습니다. 그렇게 하면 비용이 너무 많이 듭니다. 대신 각 스레드가 자체 데이터 스트립을 처리합니다. 불필요한 커널 실행 없이 부하가 고르게 분산됩니다.

통로가 시작되기 전에 기준이 정해집니다. 히스토리에서 마지막 캔들을 봅시다. 이것이 바로 우리가 관심을 갖고 있는 현재 시장 상황입니다. 외부 패턴 없음. 추측 없음. 실제 가격 현황.

다음은 핵심 부분입니다: 비교입니다. 히스토리 상의 각 포지션에 대해 해당 부분이 표준과 유사한지 여부를 확인합니다. 또한 비교는 캔들 구조를 기준으로 합니다:

  • 바디;
  • 위쪽 그림자;
  • 아래쪽 그림자.

이 모든 것은 허용 오차(tolerance)를 바탕으로 이루어집니다. 이는 미묘하지만 중요한 뉘앙스입니다. 정확히 일치하는 것을 요구하지는 않습니다. 시장은 완벽한 복제품을 만들어내지 않습니다. 우리는 픽셀 단위의 동일함이 아니라 형태에 관심이 있습니다.

만약 하나 이상의 요소가 허용 오차 범위를 벗어나면 매칭은 거부됩니다. 빠르고 불필요한 계산이 필요 없습니다. 일치하는 항목이 발견되면 두 번째 단계가 시작됩니다 - 매매 모델링. 진입 지점은 패턴 이후 새로운 캔들의 시가입니다.

      //--- simulate a trade
      if(match)
        {
         local_stat[0] += 1;
         int open = i + PATTERN_SIZE;
         float4 bar = price[open];
         float entry = bar.x;
         float tp_val = tp[itp];
         float sl_val = sl[isl];
         float buy_tp  = entry + tp_val;
         float buy_sl  = entry - sl_val;
         float sell_tp = entry - tp_val;
         float sell_sl = entry + sl_val;
         bool buy_tp_hit  = 0, buy_sl_hit  = 0;
         bool sell_tp_hit = 0, sell_sl_hit = 0;
         for(int k = 0; k < horizon; k++)
           {
            bar = price[open + k];
            float high = bar.y;
            float low  = bar.z;
            // SL is checked first (worst-case)
            buy_sl_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (low  <= buy_sl);
            buy_tp_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (high >= buy_tp);
            sell_sl_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (high >= sell_sl);
            sell_tp_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (low  <= sell_tp);
            if((buy_tp_hit | buy_sl_hit) & (sell_tp_hit | sell_sl_hit))
               break;
           }
         // forced closing by time
         buy_sl_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (entry  > bar.w);
         buy_tp_hit  |= (buy_tp_hit  == 0) & (buy_sl_hit  == 0) & (entry  < bar.w);
         sell_sl_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (entry  < bar.w);
         sell_tp_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (entry  > bar.w);
         //---
         local_stat[1] += (int)buy_tp_hit;
         local_stat[2] += (int)buy_sl_hit;
         local_stat[3] += (int)sell_tp_hit;
         local_stat[4] += (int)sell_sl_hit;
        }
     }

다음으로 매수 및 매도에 대한 TPSL 수준을 계산합니다. 이 점에 유의하세요: 양쪽 모두 동시에 계산됩니다. 이렇게 하면 계산이 간소화되고 시장 동향을 완벽하게 파악할 수 있습니다.

그런 다음 탐색 깊이를 제한하고 히스토리 전방향 탐색이 시작됩니다. 각 단계에서 다음 사항을 확인합니다:

  • SL에 도달했는지 여부;
  • TP에 도달했는지 여부.

SL을 먼저 확인합니다. 이는 우연이 아니라 최악의 시나리오를 의도적으로 가정한 것입니다. 이러한 접근 방식은 평가를 더욱 보수적으로 만들어 줍니다 - 현실에 더 가까워집니다.

양쪽의 결과가 결정되면 반복은 끝납니다. 추가 작업은 없습니다.

TPSL에 도달하지 않으면 시간에 따라 해당 포지션이 강제로 청산됩니다. 이것 또한 중요한 요소입니다. 우리는 보유중인 포지션을 남겨두지 않습니다 - 모든 상황에서 결과가 나와야 합니다.

모든 결과는 local_stat 비공개 배열에 누적됩니다. 이것은 매우 중요합니다. 현재 단계에서는 동기화가 이루어지지 않고 있습니다. 각 스레드는 독립적으로 빠르게 작동합니다.

다음으로 모든 것이 만들어진 목적인 로컬 집계로 넘어가겠습니다. 첫 번째 BLOCK_SIZE 스레드는 결과를 buf_stat에 기록합니다. 이것은 로컬 메모리이며 속도가 빠르고 그룹과 공유됩니다.

//--- write to 'local'
   if(lid < BLOCK_SIZE)
      for(int k = 0; k < STAT_DIM; k++)
         buf_stat[lid][k] = local_stat[k];
   barrier(CLK_LOCAL_MEM_FENCE);

그 다음에는 스레드 수가 버퍼 크기보다 많을 경우 데이터를 압축하는 추가 과정이 진행됩니다. 이는 임의의 그룹 크기를 고정된 크기의 리덕션 구간에 맞춰 강제 정리하는 깔끔한 방법입니다.

   for(int i = BLOCK_SIZE; i < total_loc; i += BLOCK_SIZE)
     {
      if(lid >= i && lid < (i + BLOCK_SIZE))
         for(int k = 0; k < STAT_DIM; k++)
            buf_stat[lid-i][k] += local_stat[k];
      barrier(CLK_LOCAL_MEM_FENCE);
     }

이후에는 고전적인 축소 방법이 실행됩니다 - 단계적 축소를 이용한 쌍별 합산.

//--- reduction
   for(int stride = BLOCK_SIZE / 2; stride > 0; stride >>= 1)
     {
      if(lid < stride)
        {
         for(int k = 0; k < STAT_DIM; k++)
           {
            buf_stat[lid][k] += buf_stat[lid + stride][k];
            buf_stat[lid + stride][k] = 0;
           }
        }
      barrier(CLK_LOCAL_MEM_FENCE);
     }

출력은 전체 그룹에 대한 집계 통계를 포함하는 하나의 스트림입니다. 이 프로그램은 마지막 단계를 수행합니다 - 결과 기록하기.

//--- write the result
   if(lid == 0)
     {
      int idx = (itp * sl_count + isl) * STAT_DIM;
      int count = buf_stat[0][0];
      global_stats[idx + 0] = count;
      float norm = (count > 0) ? (1.0f / ((float)count)) : 0.0f;
      for(int k = 1; k < STAT_DIM; k++)
         global_stats[idx + k] = buf_stat[0][k] * norm;
     }
  }

여기서 중요한 변화가 일어납니다:

  • 일치하는 수의 총합은 다음과 같이 그대로 유지됩니다;
  • 나머지 값들은 확률로 변환됩니다.

계산이 완료되면 출력 결과는 원시 데이터 세트가 아니라 바로 사용 가능한 통계 자료입니다. 우리가 볼 수 있는 것은:

  • 히스토리 데이터에서 유사한 상황이 몇 번 발생했는지;
  • 매수 시 이익실현 주문이 얼마나 자주 발생했는지;
  • 손절매 주문 얼마나 자주 발생했는지;
  • 매도 거래의 움직임은 어떠했는지;
  • 어떤 매개변수 조합이 더 좋아 보였는지.

이후 메인 프로그램이 결정을 내립니다. 보기 좋은 패턴 보다는 통계를 살펴봅니다. 데이터가 너무 적으면 신호는 무시됩니다. 표본이 충분하면 매수 확률과 매도 확률을 비교합니다. 그런 다음 보다 적절한 이익실현 값과 손절매 값을 선택합니다. 그 후에야 포지션에 진입할 수 있습니다.

이것이 중요한 점입니다. 이러한 구조에서는 의사 결정이 추측이나 엄격한 규칙에 기반하지 않습니다. 유사한 조건에서 시장이 일반적으로 어떻게 움직이는지를 테스트하는 데 기반을 두고 있습니다. 이 접근 방식이 수익을 보장하는 것은 아니지만 시스템 데이터 분석이라는 개념에 훨씬 더 합치합니다.

테스트 결과 테스트 결과

테스트 결과가 이를 뒷받침합니다. 이 시스템은 완벽해 보이지 않으며 자산 가치 상승 곡선도 그다지 보기 좋지 않습니다. 이것은 오히려 좋은 점일 수도 있습니다 - 우리 앞에는 과적합된 모델이 아니라 일반적인 시장 변동, 일련의 손실, 그리고 하락에 대처할 수 있는 상당히 단순한 작동 구조가 있기 때문입니다. 하지만 테스트는 여전히 긍정적인 결과를 보여줍니다.

여기서 특히 중요한 것은 최종 수익이 적당했다는 점이 아니라 우리가 고전적인 지표나 미리 정해진 패턴에 의존하지 않고 통계적으로 의미 있는 매매 메커니즘을 구축할 수 있었다는 점입니다. 이러한 구성에서 OpenCL은 가속기의 역할을 하여 이러한 분석을 실리적으로 시간 내에 가능하게 만듭니다.

즉 여기서 GPU가 필요한 이유는 전략의 수익성 때문이 아닙니다. 과거 데이터에서 일치하는 경우를 신속하게 검증하고 시장 데이터를 측정 가능한 가설로 변환하는 것이 필요합니다.

테스트 결과

결론: OpenCL은 캔들스틱 패턴 분석에 유용합니다. 다양한 유사 상황을 빠르게 실행하고 매매 결과를 계산하며 시각적 추측보다는 통계에 의존할 수 있게 해주기 때문입니다.


결론

MQL5OpenCL은 특정 유형의 작업을 위한 도구로 인식되어야 합니다. 이 기술은 방대한 데이터, 반복적인 작업, 그리고 계산을 효과적으로 병렬화 할수 있는 능력이 요구되는 상황에서 그 진가를 발휘합니다. 다른 모든 경우에는 CPU가 더 합리적인 선택입니다: CPU는 더 간단하고 유연하며 규모가 작거나 확장성이 떨어지는 계산의 경우 종종 더 빠릅니다.

이 글은 아키텍처적 한계를 이해하는 것부터 시작하여 GPU로 컴퓨터 연산을 오프로드 하는 실용적인 구조에 이르기까지 전반적인 내용을 다룹니다. 결과는 GPU 성능 뿐만 아니라 컴퓨팅 연산 구성의 품질에 의해서도 결정되는 것입니다. 잦은 초기화, 불필요한 데이터 전송, 그리고 지나치게 작은 작업들은 모든 이점을 없에 버립니다. 반대로 컨텍스트를 재사용하고 대규모 배열을 다루고 데이터를 신중하게 전달할 때는 GPU가 자신의 영역에서 활약하기 시작합니다.

이는 행렬 곱셈의 예에서 특히 분명하게 드러납니다. 충분한 양의 데이터가 있다면 병렬 처리는 CPU에 비해 몇 배의 성능 향상을 제공합니다. 하지만 해당 구조의 실제적인 가치는 합성 테스트에서만 드러나는 것이 아닙니다. 실제 매매에 더 가까운 작업에서 GPU는 대규모 최적화, 가설 검증 및 데이터 상에서 안정적인 패턴 검색과 관련된 연구 속도를 높일 수 있도록 해줍니다. 바로 이 지점에서 그래픽 카드가 그 자체로 유용한 것이 아니라 컴퓨팅의 규모를 확장하는 수단으로서 유용하다는 점이 분명해집니다.

핵심 결론은 실용적이라는 것입니다. GPU로 전환한다고 해서 거래 시스템이 수익성 있게 되는 것이 아니며 의미 있는 시장 모델을 대체하지도 않습니다. 대신 CPU에서 처리하기에는 비용이 너무 많이 들거나 속도가 너무 느린 방대한 양의 열거 및 분석 작업을 하기 좋습니다. 진정한 가치는 여기에 있습니다: 기적을 약속하는 것이 아니라 MQL5에서 보다 폭넓고 체계적인 연구 주기를 가능하게 하는 것입니다.


이 글에서 사용된 프로그램

# 이름 타입 설명
1 PatternStats.mq5 Expert Advisor EA 테스트
2 PatternStats.cl Code Base OpenCL 프로그램 코드 라이브러리

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

파일 첨부됨 |
MQL5.zip (4.34 KB)
새로운 기능: MQL5의 커스텀 인디케이터 새로운 기능: MQL5의 커스텀 인디케이터
MetaTrader5와 MQL5의 새로운 기능 전체를 나열하지는 않겠습니다. 종류도 많은 데다가, 별도의 설명이 필요한 기능들도 있거든요. 객체 지향 프로그래밍을 이용한 코드 작성법 또한 다음에 알아보도록 하겠습니다. 다른 기능들과 함께 설명하기에는 조금 어려운 이야기일 수 있으니까요. 이 글에서는 인디케이터와 인디케이터의 구조, 드로잉 타입과 프로그래밍 디테일을 MQL4와 비교해 볼게요. 초보자 분들께 많은 도움이 되면 좋겠고 기존에 사용하시던 개발자 분들도 뭔가 새로운 걸 얻어 가실 수 있길 바랍니다.
MetaTrader 5와 MQL5 경제 달력: 뉴스를 재현 가능한 있는 트레이딩 시스템으로 구현하는 방법 MetaTrader 5와 MQL5 경제 달력: 뉴스를 재현 가능한 있는 트레이딩 시스템으로 구현하는 방법
이 글에서는 MetaTrader 5에 내장된 경제 달력를 활용한 뉴스 트레이딩에 대한 체계적인 접근 방식에 대해 알아봅니다: 구체적으로는 데이터 구조, API 함수, 시간 동기화 규칙 및 이벤트 필터링에 대해 알아봅니다. 서버에 과부하를 일으키지 않고 캐싱 및 점진적인 업데이트를 하는 방법이 다루어집니다. 또한 동일한 알고리즘을 사용하여 결정적 테스트를 위해 히스토리를 .EX5 리소스로 내보내는 작동 메커니즘도 제공합니다.
새 MetaTrader 와 MQL5를 소개해드립니다 새 MetaTrader 와 MQL5를 소개해드립니다
본 문서는 MetaTrader5의 간략 리뷰입니다. 짧은 시간 내에 시스템의 모든 세부 사항을 안내해드리기는 어렵습니다 - 테스트는 2009.09.09에 시작되었습니다. 이는 상징적인 일자로, 전 이것이 행운의 숫자가 될거라 믿어 의심치않습니다. 제가 새 MetaTrader 5 터미널과 MQL5 베타버전을 받은지 며칠이 지났습니다. 아직 모든 기능을 사용해본 것은 아니지만, 벌써부터 감명깊네요.
파이썬 + MetaTrader 5: 데이터, 피처 및 프로토타입을 위한 고속의 연구용 프레임워크 파이썬 + MetaTrader 5: 데이터, 피처 및 프로토타입을 위한 고속의 연구용 프레임워크
이 글은 파이썬과 MetaTrader 5를 통합함으로써 어떻게 연구의 유연성과 거래의 실행을 단일 워크플로로 결합하는지 그 방법을 보여줍니다. 파이썬은 데이터 분석, 피처 선택 및 모델 훈련에 사용되고 MetaTrader 5는 테스트 및 거래의 자동화에 사용됩니다. 이러한 접근 방식은 솔루션을 실제로 적용하는 과정을 간단하게 하고 재현성을 높여주며 거래 시스템의 개발을 더욱 빠르고 체계적으로 만들어 줍니다.