English Русский Deutsch 日本語 Italiano Türkçe
preview
파이썬 + MetaTrader 5: 데이터, 피처 및 프로토타입을 위한 고속의 연구용 프레임워크

파이썬 + MetaTrader 5: 데이터, 피처 및 프로토타입을 위한 고속의 연구용 프레임워크

MetaTrader 5통합 |
20 2
MetaQuotes
MetaQuotes

소개

파이썬은 데이터를 다루는 데 가장 편리한 도구 중 하나가 되었습니다. 파이썬은 다양한 라이브러리를 제공하며 시간과 자원을 낭비하지 않고 신속하게 통계 분석을 수행하고 가설을 검증하고 결과를 시각적으로 나타낼 수 있도록 해줍니다. 이는 금융 시장과 관련된 문제를 해결하는 데 중요합니다: 금융 시장에서는 데이터 처리 속도도 중요하지만 분석에서 실질적인 결론으로 신속하게 전환하는 것 또한 중요하기 때문입니다.

MetaTrader 5파이썬과의 직접 통합을 지원하며 이를 통해 시장 데이터를 활용한 실질적인 여러 작업을 할 수 있는 가능성을 크게 높여줍니다. 연구자나 개발자는 익숙한 파이썬 툴킷을 사용하여 거래 플랫폼과 연결이 끊기지 않고도 가격 데이터를 연구하고 통계 모델을 구축하고 실질적인 가설을 검증할 수 있습니다. 이러한 접근 방식은 작업 과정을 더욱 유연하게 만들고 통합된 순환 과정을 지원합니다: 데이터에서 가설로, 가설에서 모델로, 그리고 모델에서 실제 적용으로.

MetaTrader 5 + 파이썬

이 글에서는 다음과 같은 내용을 보여드리겠습니다.

  • MetaTrader 5파이썬이 어떻게 통합되는지;
  • 이를 활용하여 어떻게 금융 데이터를 분석하고 가설을 검증하는지;
  • ONNX를 사용하여 소규모 모델을 구축하고 훈련시킨 다음 훈련된 결과를 어떻게 EA(Expert Advisor)에 적용하는지.

이를 통해 우리는 리서치 실험 단계에서 실제 거래 시스템에 적용하는 단계로 나아갈 수 있을 것입니다.

1. 설치 및 연결

데이터 분석을 진행하기 전에 파이썬MetaTrader 5를 함께 사용할 수 있는 작업 환경을 준비해야 합니다. 이 작업은 간단하지만 정확성이 요구됩니다. 처음에 올바르게 설정하면 나중에 발생할 수 있는 수많은 사소한 문제들이 예방될 수 있습니다.

먼저 MetaTrader 5 터미널을 공식 웹사이트에서 다운로드 하여 설치해 보겠습니다.

이후 현재 파이썬 버전이 필요합니다. 이 글을 쓰는 시점에서 버전은 3.14.3입니다. 설치 시 파이썬PATH 환경 변수에 추가하는 옵션을 반드시 활성화하시기 바랍니다. 이렇게 하면 불필요한 수동 설정 없이 명령줄에서 인터프리터를 직접 사용할 수 있습니다.

파이썬 설치

핵심은 환경을 격리하는 것입니다. 경험에 따르면 데이터와 모델을 다루는 프로젝트는 금세 의존성 문제로 과부하가 걸리는 경향이 있습니다. 결과의 일관성과 재현성을 유지하기 위해 각 프로젝트별로 별도의 가상 환경을 생성합니다. 파이썬에서는 내장된 venv 도구를 사용하여 이 문제를 해결할 수 있습니다.

작업 과정은 다음과 같습니다.

  1. 명령어 실행 환경을 엽니다. 가장 쉬운 방법은 Win+R 키를 누르는 것입니다. 창이 나타나면 명령어 cmd를 입력하고 Enter 키를 누르세요. 이렇게 하면 Windows 명령 프롬프트가 열립니다. PowerShell을 사용해도 됩니다. 이 경우에도 동일한 원칙이 적용됩니다.
  2. 환경이 생성될 프로젝트 디렉토리로 이동합니다. 이는 표준 명령어를 사용하여 수행됩니다.
  3. cd /path/to/your/project
  4. 가상 환경을 생성하세요.
  5. python -m venv integration

    그리고 활성화하세요.

    integration\Scripts\activate

    앞으로 설치되는 모든 패키지는 현재 프로젝트 안에 격리됩니다.

  6. MetaTrader 5와 상호 작용하려면 파이썬 모듈을 설치하세요.
  7. pip install MetaTrader5
  8. 금융 데이터를 완벽하게 분석하려면 기본적이면서도 거의 완벽한 수준의 즉시 활용 가능한 라이브러리 스택을 구축하는 것이 좋습니다. 이러한 라이브러리 스택은 데이터 처리, 모델 구축 및 기술 분석과 같은 핵심적인 작업들을 다룹니다.

우선, NumPy를 연결하세요 - 수치 계산의 기초가 되는 라이브러리. 이것이 이후의 모든 구성 요소가 기반으로 삼는 토대가 됩니다.

이후 표 형식 데이터와 시계열 데이터를 다루는 데 필수적인 도구 Pandas를 불러옵니다 - Pandas 없이는 가격 분석이 매우 어려워집니다.

시각화를 위해서는 의 연결을 사용합니다. Matplotlib은 그래프를 완벽하게 제어할 수 있도록 해주고 Seaborn은 통계적으로 표현력이 풍부한 시각화 자료를 더 빠르게 생성할 수 있도록 해줍니다. 이 둘이 함께 작동함으로써 우리는 비로소 시장을 볼 수 있게 됩니다.

머신러닝 작업을 위해 Scikit-Learn을 추가합니다. Scikit-Learn은 모델 구축 및 검증을 위한 오랜 기간 검증된 도구로서 초기 프로토타입 및 기본 전략에 매우 적합합니다.

응용 시장 분석을 위해 TA에 연결합니다 — 기술 지표 라이브러리입니다. 이는 기존의 공식들을 수동으로 구현하지 않고도 신호를 통해 데이터를 더욱 풍부하게 해 주는 편리한 방법입니다.

라이브러리 설치는 단 하나의 명령으로 수행됩니다.

pip install numpy pandas matplotlib seaborn scikit-learn ta

라이브러리를 추가하는 것이 좋습니다. 언뜻 보기에는 보조적인 기능처럼 보이지만 실제로는 시간대를 다루는 데 매우 중요합니다.

pip install pytz

금융 데이터는 시간에 매우 민감합니다. 거래소는 서로 다른 시간대에서 운영됩니다.

파이썬datetime 객체를 생성할 때 시스템의 로컬 시간을 기본으로 사용합니다. 이러한 방식은 일상적인 업무에는 편리하지만 금융 거래에서 시스템적으로는 오류의 원인이 됩니다. MetaTrader 5는 틱 시간과 바 시간을 UTC 형식으로 저장합니다 - 시간 편차나 현지 시간대를 고려하지 않습니다.

이로 인해 전형적인 불일치가 발생합니다. 모델이 작동하는 시간 기준과 데이터가 작동하는 시간 기준이 서로 다릅니다. 실제로 이는 불쾌한 결과를 초래합니다.

따라서 여기서 규칙은 엄격하게 적용됩니다. 시간과 관련된 모든 작업은 UTC로 수행되어야 합니다. datetime 객체는 명시적으로 UTC 시간대로 생성되어야 하며 모든 로컬 값은 단일 표준으로 변환되어야 합니다. 이렇게 하면 데이터와 모델이 동일한 시간 좌표 시스템으로 정렬됩니다.

시간대를 명시적으로 관리하기 위해서는 pytz를 사용하는 것이 좋습니다. 이렇게 하면 암묵적인 변환이 제거되고 시스템 동작을 예측할 수 있게 됩니다.

MetaTrader 5으로부터 가져온 데이터는 이미 UTC 기준이며 수정 되지 않아야 합니다. 이러한 값들은 정확하게 해석되어야 하며 모델의 로직과 일관성이 있어야 합니다. 금융 시장에서 시간은 단순한 지표가 아니라 좌표축과 같습니다. 여기에 오류가 있으면 전체 해석이 망가지게 됩니다.

이 세트는 제한적으로 보일 수 있지만 실제로는 일반적인 작업의 최대 80%를 커버합니다. 이는 고전적인 접근 방식입니다: 중복을 줄이고 효율성을 높이는 방식이죠.

현재 환경에서 나가고 싶으면 표준 명령어를 사용하십시오.

deactivate

현재 상태에서 토대는 완전히 준비되어 있는 것입니다. 이제 터미널, 인터프리터 및 필요한 라이브러리가 설치되어 있습니다. 해당 환경은 격리되어 있고 언제든 다시 사용 가능합니다. 이는 향후 분석을 진행하고 가설을 검증하고 점진적으로 거래 모델을 개발하는 데 편리하고 안전한 토대가 됩니다.


2. 데이터 로딩

기반 시설 구축이 완료되면 첫 번째 실질적인 단계로 넘어갑니다 - 프로그램 작성 단계입니다. 여기서 특별한 제한은 없습니다: 익숙한 편집기면 무엇이든 괜찮습니다. 하지만 통합이라는 맥락에서 볼 때 파이썬 과의 연동을 이미 지원하고 전체 프로세스를 단일 프레임워크 내에서 유지할 수 있도록 해주는 MetaTrader 5의 내장 편집기인 MetaEditor를 사용하는 것이 좋습니다.

MetaEditor 또는 터미널에서 파이썬 스크립트를 직접 실행하려면 플랫폼 설정에서 인터프리터 경로를 한 번만 지정해 주면 됩니다. 이는 기본적인 작업이지만 여기에는 중요한 세부적인 내용이 있습니다. 이전에 생성한 가상 환경을 사용하는 경우에는 경로는 전역 파이썬 설치가 아닌 해당 가상 환경의 인터프리터를 구체적으로 가리켜야 합니다.

이러한 접근 방식은 프로젝트의 격리를 유지하고 모든 종속성을 통제된 상태로 유지합니다. 그렇지 않으면 동일한 코드가 실행 환경에 따라 다르게 동작하는 미묘한 버그가 발생할 위험이 있습니다. 금융 분야에서 이는 결코 좋지 않습니다.

첫 번째 단계에서는 기본적인 연결을 구축합니다: 파이썬MetaTrader 5 → 과거 데이터. 이 작업은 공식 자체는 간단해 보이지만 실제로는 매우 중요한 것입니다: 스크립트를 통해 터미널에 연결하여 지정된 금융 상품 및 기간에 대한 가격 데이터를 가져옴. 의미 있는 분석은 바로 이 순간부터 시작되는 것입니다.

스크립트 구조를 블록 단위로 구성하는 것이 논리적입니다. 먼저 필요한 라이브러리를 연결합니다 - 이렇게 하면 작업에 필요한 도구 세트가 구성됩니다.

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import pytz
import seaborn as sns
import matplotlib.pyplot as plt
import ta

다음으로 MetaTrader5 모듈을 통해 터미널과의 연결이 시작됩니다. 이것이 시스템으로 들어가는 진입 지점입니다. 연결이 설정되지 않으면 계속할 이유가 없죠. 따라서 상태 확인은 즉시, 그리고 어떠한 타협 없이 수행되어야 합니다.

# Display data on the MetaTrader 5 package
print("MetaTrader5 package author: ", mt5.__author__)
print("MetaTrader5 package version: ", mt5.__version__)

# Connection to MetaTrader 5 terminal
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

다음 단계는 시간 간격을 설정하는 것입니다. 여기서는 UTC 시간대를 명시적으로 나타내기 위해 pytz를 사용합니다. 이는 기술적인 세부 사항이 아니라 필수 요건입니다. 터미널은 가격 데이터를 UTC로 저장하는데 파이썬 측에서 불일치가 발생하는 경우에는 데이터 편향으로 이어집니다. 이 오류는 드러나지 않지만 그 결과는 시스템 전반에 영향을 미칩니다.

# Set time zone to UTC
timezone = pytz.timezone("Etc/UTC")
# Create 'datetime' objects in UTC time zone to avoid the implementation of a local time zone offset
utc_from = datetime(2020, 1, 1, tzinfo=timezone)
utc_to = datetime.now(timezone)  # Set to the current date and time

이후 과거 데이터에 대한 요청이 이루어집니다. 이 예시에서는 EURUSD에 대한 1시간 바를 사용합니다. 여기서 상품명에 주의를 기울여야 할 필요가 있습니다. 접미사와 접두사를 포함하여 터미널에 표시된 철자와 정확히 일치해야 합니다.

# Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

출력 결과는 가격과 타임스탬프가 포함된 구조체의 배열입니다 - 필터나 해석이 적용되지 않은 가공되지 않은 시장 데이터 입니다. 이것이야말로 첫 단계에서 필요한 데이터 유형입니다.

그 후 터미널과의 연결이 정상적으로 종료됩니다. 이는 실행 규율입니다: 정상적인 종료는 다음 시작 시 숨겨진 오류를 방지하고 시스템 동작을 예측 가능하게 만듭니다.

# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()

이어서 결과에 대한 기본적인 확인 절차가 진행됩니다. 데이터가 수신되면 빠른 검증을 위해 첫 번째 레코드가 표시됩니다. 그렇지 않으면 스크립트가 종료됩니다.

# Check if data was retrieved
if rates is None or len(rates) == 0:
    print("No data retrieved. Please check the symbol or date range.")
    quit()
# Print the first 10 raw records for a quick data sanity check
print("Display obtained data 'as is'")
for rate in rates[:10]:
    print(rate)

추가로 시각화 작업이 수행됩니다: 즉 종가와 볼륨을 나타내는 차트가 작성됩니다. 이 경우에는 볼륨을 별도의 축에 배치하는 것이 좋습니다. 우리는 볼륨 축을 따라 관측된 최대값보다 5배 더 큰 여유를 두고 최대값을 설정할 것입니다. 이 방법은 간단해 보이지만 완벽하게 작동합니다. 히스토그램이 차트 하단에 밀착 되면서 가격에 비해 관심에서 벗어납니다.

그 결과 종가 라인은 깔끔하고 읽기 쉬워지고 볼륨 정보는 시각적 표현을 복잡하게 만들지 않으면서도 충분한 정보를 제공합니다. 이는 데이터의 완전성과 이해 가능성 사이의 전형적인 균형점입니다. 차트는 정보를 담을 뿐만 아니라 불필요한 스트레스 없이 신속하게 해석될 수 있도록 만들어져야 합니다.

# Create a DataFrame from the retrieved tick data
rates_frame = pd.DataFrame(rates)
# Convert the timestamp column from seconds since epoch to datetime
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

# Use datetime as the DataFrame index for time series plotting and analysis
rates_frame.set_index('time', inplace=True)

# Plot closing price and tick volume
fig, ax1 = plt.subplots(figsize=(12, 6))

# Close price on primary y-axis
ax1.set_xlabel('Date')
ax1.set_ylabel('Close Price', color='tab:blue')
ax1.plot(rates_frame.index, rates_frame['close'], color='tab:blue', label='Close Price')
ax1.tick_params(axis='y', labelcolor='tab:blue')

# Tick volume on secondary y-axis
ax2 = ax1.twinx()  
ax2.set_ylabel('Tick Volume', color='tab:green')
max_tick = rates_frame['tick_volume'].max()
ax2.set_ylim(0, max_tick * 5)
ax2.plot(rates_frame.index, rates_frame['tick_volume'], color='tab:green', label='Tick Volume')
ax2.tick_params(axis='y', labelcolor='tab:green')

# Show the plot
plt.title('Close Price and Tick Volume Over Time')
fig.tight_layout()
plt.show()
fig.savefig('close_price.png')

종가 변동 추이

실질적인 관점에서 보면 그 절차는 매우 간단해 보입니다. 하지만 사실 이것은 데이터 품질 관리의 핵심 단계입니다. 모델에 입력되는 내용이 무엇인지 여러분은 명확하게 이해하고 계시는 겁니다: 형식, 타임스탬프, 값. 이러한 유형의 초기 감사는 전형적인 엔지니어링 접근 방식의 예입니다. 시간과 스트레스를 줄여주고 특히 트레이딩 시스템에서 중요한 돈까지 절약해 줍니다.


3. 가설 검증 및 피처 선택

이제 과거 가격 데이터를 확보했으니 초기 분석을 시작해 보겠습니다. 아주 간단하고 거의 교과서적인 가설 부터 시작해 봅시다: 시장은 이전 캔들의 움직임을 이어가는 경향이 있습니다. 단기적인 동향에 관성이 있는지 확인해 봅시다.

파이썬 툴킷을 사용하면 단 몇 줄의 코드로 그러한 가설을 검증할 수 있습니다. 로직은 다음과 같습니다. 종가들을 보고 각 봉별로 가격 차이를 살펴보세요. 가격 변동의 추이를 파악하세요. 분석에 있어 기본적으로 중요한 것입니다.

# Correlation analysis between adjacent bar moves
close = rates_frame['close'].to_numpy(dtype=float)
# last and next price move differences
diff = close[1:] - close[:-1]

다음으로 우리는 두 개의 시계열을 생성합니다: 마지막 변화다음 변화 기술적으로 이는 배열을 하나의 요소만큼 이동하여 구현됩니다. 그 결과는 각 관측치가 간단한 질문에 답하는 값의 쌍입니다: 만약 시장이 상승(또는 하락)하고 있었다면 다음 바에서 무슨 일이 일어날 것인가?

diff = np.column_stack((diff[:-1], diff[1:]))
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])

그 후 이 두 시계열 간의 Pearson 상관계수를 계산합니다. 양의 상관관계는 강한 관성을 나타내고 음의 상관관계는 대부분 되돌림의 성향이 우세하다는 것을 나타냅니다.

좀 더 명확하게 하기 위해 Seaborn을 사용하여 결과를 시각화 했습니다 - 상관 관계 히트맵이 생성됩니다. 이렇게 하면 수치 같은 세부 사항을 들여다 볼 필요 없이 의존 관계의 구조를 빠르게 파악할 수 있습니다.

correlation_matrix = data_matrix.corr('pearson')
plt.subplots(figsize=(3, 2))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Bar to Bar') 
plt.savefig('bar_to_bar.png')
plt.show()

여기서 핵심은 전략 그 자체가 아닙니다 - 그 전략이 본질적으로 원시적이라는 점입니다. 가치는 다른 곳에 있습니다. 우리는 기본적인 가설 검정 과정을 시연합니다. 우리는 가설을 세우고 데이터를 변환하고 계산을 수행하고 결과를 시각화 했습니다. 이러한 접근 방식은 분석을 체계 있게 하고 눈대중으로 결론을 내리는 것을 방지합니다.

최근 캔들과 다음 캔들 사이의 상관관계

이제 얻어진 결과를 평가해 봅시다. 관찰된 상관계수는 -0.018입니다 - 0에 가깝지만 부호가 음수입니다.

이는 인접한 바 사이에 명확한 관계가 없다는 것을 의미합니다. 더욱이 미약한 음의 부호는 미묘한 평균 회귀 효과를 나타냅니다. 한 방향으로 움직임이 나타난 후에 다음 캔들은 반대 방향으로 움직일 가능성이 높습니다. 하지만 그 효과의 크기는 너무 작아서 실제적인 관점에서 보면 통계적인 잡음으로 보이기도 합니다.

움직임의 지속에 대한 가설은 확인되지 않았습니다. H1 시간대의 시장은 기존의 움직임을 유지하기 보다는 무작위적으로 움직이는 시장에 더 가깝습니다. 이는 중요한 관찰입니다. 이는 순진한 전략들을 완전히 배제하고 향후 분석을 위한 보다 냉철한 출발점을 시사합니다.

바 하나는 너무 작은 규모입니다. 그러한 데이터는 구조보다는 잡음에 지배됩니다. 따라서 우리는 다음 논리적인 단계를 택합니다: 관찰 범위를 확대하고 개별적인 변화가 아닌 평균적인 움직임을 확인합니다.

한개의 바 대신 1개에서 23개 바에 이르는 과거 기간 동안의 평균 가격 변동을 사용합니다. 이렇게 하면 무작위 적인 변동이 완화되고 우리는 움직임의 보다 안정적인 구성 요소를 구분할 수 있습니다. 마찬가지로 우리는 1~9개 바에 걸쳐 미래 평균 가격 변동을 계산합니다. 이렇게 우리는 개별 가격 관찰에서 누계된 신호로 나아가는 것입니다.

구현은 두 개의 블록으로 깔끔하게 나뉘어져 있습니다. 첫 번째 블록은 과거의 변동에 대한 이동 평균을 계산합니다.

# Add rolling mean features for the previous and future moves
for period in range(2, 24, 1):
    data_matrix[f'last_mean_{period:02d}'] = data_matrix['last'].rolling(window=period).mean()

두 번째는 블록은 미래의 변동에 대한 것으로 미래에서 과거로 정보가 유출되는 것을 막기 위해 의무적인 전환이 필요하다는 것입니다. 이것은 매우 중요한 부분입니다: 이 부분이 없으면 분석은 무의미해집니다.

for period in range(2, 10, 1):
    data_matrix[f'next_mean_{period}'] = data_matrix['next'].rolling(window=period).mean().shift(-(period-1))

계산 후 누락된 값이 있는 행은 제거됩니다 - 이는 윈도우 함수를 사용할 때 어쩔 수 없이 생기는 현상입니다.

# Remove rows with missing values created by rolling calculations
data_matrix.dropna(inplace=True)

이후 피어슨 상관 행렬이 구성됩니다. 그리고 그로부터 필요한 부분 행렬이 추출됩니다: 즉 과거로부터 미래의 의존성을 추출. 핵심 질문에 대한 답은 다음과 같습니다: 평균의 움직임은 예측력을 가지고 있을까요?

correlation_matrix = data_matrix.corr('pearson')
# Match columns that begin with "next"
reg = r'^next.*$'
selected_cols = correlation_matrix.filter(regex=reg).columns
remaining_rows = correlation_matrix.index.difference(selected_cols)
correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]

Seaborn을 이용한 히트맵 형태의 시각화는 특히 이러한 경우에 적합합니다. 이를 통해 우리는 전체 매개변수 그리드에 걸쳐 종속성 구조를 신속하게 평가할 수 있습니다.

plt.figure(figsize=(12, 7))
plt.subplots_adjust(left=0.15, right=1, bottom=0.16, top=0.95)
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Means Last to Next Bars') 
plt.savefig('mean_to_bar.png')
plt.show()

방법론적인 관점에서 볼 때 이는 이미 상당히 성숙한 시도입니다. 우리는 단순한 바 대 바 방식의 테스트에서 벗어나 집계된 효과를 분석하는 것으로 나아가고 있습니다. 시장에 약한 패턴들이 존재한다면 바로 이 수준에서 그 패턴들이 드러나기 시작합니다.

일정 기간 동안의 평균 가격 변동과 미래 가격 변동 간의 상관관계

얻어진 결과는 흥미로워 보이지만 최종적인 결론은 여전히 유보적입니다. 여전히 음의 상관관계인 값이 나타납니다. 하지만 이제는 체계화되었습니다. 행렬의 중앙 영역(히스토리에서 8~14 바의 창과 수평선에서 5~8 바의 창)에서 효과는 강해지고 -0.02... -0.03의 값에 도달합니다. 이는 약하지만 일관된 평균 회귀 신호입니다.

논리는 아주 명확합니다. 시장이 한 방향으로 꽤 움직여 온 것이라면 다음 바들에서 부분적인 조정이 나타날 확률이 더 높습니다. 하지만 그 효과는 선형적이지 않습니다:

  • 짧은 창에서 - 소음에 묻혀버립니다:
  • 너무 긴 창에서는 흐릿 해지고 선명도가 떨어집니다.
  • 최대치는 중간 범위에서 나타납니다.

행렬의 오른쪽 아래 부분은 따로 언급할 가치가 있습니다. 거기에서 상관관계가 0에 가까워지고 어떤 곳에서는 오히려 양의 상관관계를 보이기도 합니다. 이는 과도한 평활화로 인해 예측력이 저하되는 전형적인 징후입니다: 데이터의 변동성과 함께 신호가 사라집니다.

Seaborn을 이용한 시각화는 여기서 의존성 패턴을 상당히 잘 보여줍니다. 그 그림은 알고 보니 상당히 의미있는 패턴을 보여줍니다.

결론은 간단합니다: 시장은 강한 관성을 보이지는 않습니다. 그러나 시장은 약한 평균 회귀 효과를 나타내는 것입니다. 이것은 완성된 전략을 제공하는 것은 아니지만 방향을 제시해 줍니다.

얻어진 상관관계 값이 낮기 때문에 이러한 신호를 선형 설정에 적용하여 트레이딩 할 수는 없습니다. 하지만 여기서부터 더욱 흥미로운 부분이 시작됩니다. 단순한 관계가 약하다면 우리는 여러 피처들을 조합하여 관계를 강화 시킬 수 있습니다 - 즉, 모델로 나아가는 것입니다.

핵심 과제는 유용한 피처 공간을 형성하는 것입니다. 단일 가격 시계열로는 충분하지 않습니다. 따라서 다음 단계는 시장의 숨겨진 구조를 포착할 수 있는 추가적인 피처들을 찾는 것입니다. 고전적인 기술적 지표들을 기본 세트로 사용하는 것이 합리적입니다. 이러한 방법은 오랜 기간 검증된 도구들을 사용하는 것으로 단순해 보이기는 하지만 종종 유용한 정보를 가져다줍니다.

다음 우리는 동일한 체계적인 접근 방식을 적용합니다 - 상관 관계를 통한 검증 우리는 미래 가격 변동과 다양한 지표들의 값 사이의 관계를 평가합니다. 이 경우 지표의 매개변수는 반복문 내에서 다르게 적용됩니다. 이렇게 하면 우리는 여러가지 구성 환경을 즉시 살펴보고 신호가 가장 강한 곳을 확인할 수 있습니다.

실질적인 관점에서 보면 이 과정은 매개변수를 열거한 후 필터링하는 것과 유사합니다. 하지만 본질적으로 이것은 피처 공간의 형성과 선택입니다. 여기서 상관관계는 진단 도구의 역할을 합니다. 우리는 어떤 데이터 변환이 미래의 변동과 연관되어 있는지 측정합니다. 이는 근본적으로 다른 차원의 사고방식입니다: 먼저 구조를 이해한 다음 수익을 뽑아 내는 것입니다.

코드에는 이러한 접근 방식이 상당히 체계적으로 구현되어 있습니다. 한편으로는 가격에서 파생된 단순한 것들이 사용됩니다 - 이동 평균, 이동 평균의 1차 및 2차 차분, 현재 값과의 편차 등 이는 지역적 역동성과 시장 가속화를 포착하려는 시도입니다.

# Recreate the base matrix for indicator engineering
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
# Add 11-period previous move averages and derived momentum features
data_matrix[f'last_mean_11'] = data_matrix['last'].rolling(window=11).mean()
data_matrix[f'Dlast_mean_11'] = data_matrix[f'last_mean_11'].diff()
data_matrix[f'DDlast_mean_11'] = data_matrix[f'Dlast_mean_11'].diff()
# Feature representing the gap between the rolling mean and the current move
data_matrix[f'last_last_11'] = data_matrix[f'last_mean_11'] - data_matrix['last']
data_matrix[f'Dlast_last_11'] = data_matrix[f'last_last_11'].diff()
data_matrix[f'DDlast_last_11'] = data_matrix[f'Dlast_last_11'].diff()
# Add short-term future sum targets for the next bars
for period in range(2, 10, 1):
    data_matrix[f'next_{period}'] = data_matrix['next'].rolling(window=period).sum().shift(-(period-1))

다른 한편으로는 TA 라이브러리의 고전적인 지표들이 추가됩니다: SMA, RSI, MACD를 비롯한 여러 지표를 동시에 매개변수로 해서. 이러한 방식 덕분에 최적의 기간을 추측할 필요 없이 전체 기간에 걸쳐 성능을 파악할 수 있습니다.

# Build additional technical indicators using the close price series
close = pd.DataFrame(close[:-1], columns=['close'])
indicator_cols = {}
for period in [4, 8, 12, 24, 36, 48]:
    sma = ta.trend.sma_indicator(close['close'], window=period, fillna=True)
    dsma = sma.diff()
    ddsma = dsma.diff()
    rsi = ta.momentum.rsi(close['close'], window=period, fillna=True)
    drsi = rsi.diff()
    ddrsi = drsi.diff()
    macd = ta.trend.MACD(
        close['close'],
        window_slow=2 * period,
        window_fast=period,
        window_sign=period * 3 // 4,
        fillna=True,
    )
    macd_main = macd.macd()
    dmacd = macd_main.diff()
    ddmacd = dmacd.diff()
    macd_diff = macd.macd_diff()
    dmacd_diff = macd_diff.diff()
    ddmacd_diff = dmacd_diff.diff()
    macd_signal = macd.macd_signal()
    dmacd_signal = macd_signal.diff()
    ddmacd_signal = dmacd_signal.diff()
    macd_sig_main = macd_signal - macd_main
    dmacd_sig_main = macd_sig_main.diff()
    ddmacd_sig_main = dmacd_sig_main.diff()

    indicator_cols[f'SMA_{period:02d}'] = sma
    indicator_cols[f'DSMA_{period:02d}'] = dsma
    indicator_cols[f'DDSMA_{period:02d}'] = ddsma
    indicator_cols[f'RSI_{period:02d}'] = rsi
    indicator_cols[f'DRSI_{period:02d}'] = drsi
    indicator_cols[f'DDRSI_{period:02d}'] = ddrsi
    indicator_cols[f'MACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_main
    indicator_cols[f'DMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd
    indicator_cols[f'DDMACD_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd
    indicator_cols[f'MACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_diff
    indicator_cols[f'DMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_diff
    indicator_cols[f'DDMACD_DIFF_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_diff
    indicator_cols[f'MACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_signal
    indicator_cols[f'DMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_signal
    indicator_cols[f'DDMACD_SIGNAL_{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_signal
    indicator_cols[f'MACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = macd_sig_main
    indicator_cols[f'DMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = dmacd_sig_main
    indicator_cols[f'DDMACD_Sig_Main{period:02d},{2*period:02d},{period*3//4:02d}'] = ddmacd_sig_main

# Append all indicator columns to the feature matrix in one operation
# This avoids repeated DataFrame assignment and keeps the DataFrame compact
data_matrix = pd.concat([data_matrix, pd.DataFrame(indicator_cols)], axis=1)
# Remove any rows with NaN values created by indicator calculations
data_matrix.dropna(inplace=True)

특히 2차 미분을 추가하는 것은 의미 있습니다( diffdiff from diff ). 이는 신호 자체의 변화를 포착하려는 시도입니다 - 시장 움직임의 가속 및 감속을 분석하는 것으로의 전환을 의미합니다. 금융 데이터 시리즈에서 이러한 효과는 종종 수치나 가격 자체보다 더 많은 정보를 제공합니다.

다음으로 필터링이 적용됩니다. 전체 상관 행렬에서 목표 변수와의 최대 상관관계가 지정된 임계값(이 경우 0.02)을 초과하는 피처들만 남깁니다. 이것은 중요한 포인트입니다. 약하고 불안정한 의존 관계를 제거하고 데이터 내에서 최소한의 가능성이 있는 의존 관계만 의도적으로 남겨두는 것입니다.

correlation_matrix = data_matrix.corr('pearson')
selected_cols = correlation_matrix.filter(regex=reg).columns
remaining_rows = correlation_matrix.index.difference(selected_cols)
correlation_matrix = correlation_matrix.loc[remaining_rows, selected_cols]
# Delete rows with low correlations
correlation_matrix = correlation_matrix[correlation_matrix.abs().max(axis=1) >= 0.02]

Seaborn을 통한 시각화로 프로세스가 완료됩니다.

plt.figure(figsize=(12, 7))
plt.subplots_adjust(left=0.2, right=1, bottom=0.05, top=0.95)
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Indicators to Next Bars') 
plt.savefig('trend_to_bar.png')
plt.show()

히트맵은 더 이상 혼란스러운 노이즈처럼 보이지 않고 신호의 맵으로 바뀝니다. 이렇게 하면 어떤 지표 그룹이 미래의 변동에 반응하기 시작하는지, 어떤 매개변수에서 이러한 반응이 증가하는지, 그리고 어디에서 완전히 사라지는지가 분명해집니다.

지표와 미래 가격 변동 간의 상관관계

바로 이 지점에서 가설들이 구체적인 구조를 갖추기 시작하는 것입니다. 우리는 무작위로 지표를 선택하는 단계에서 의식적으로 피처들을 형성하는 단계로 나아가고 있습니다. 이것은 아직 모델이라기 보다는 모델의 틀입니다. 다음 과제는 이러한 약하고 서로 다른 신호들을 하나의 시스템으로 통합하여 개별적으로는 거의 감지할 수 없었던 안정적인 상관관계를 추출하는 것입니다.


4. 모델 구축 및 훈련

이전 단계에서 우리는 상관관계 맵을 만들고 직접 상관관계와 역 상관관계 값이 최대인 피처들을 선택했습니다. 이것이 핵심입니다: 음의 상관관계도 동일한 신호입니다. 부호만 반대일 뿐입니다. 모델 측면에서 볼 때 이는 제한 사항이 아니라 추가적인 정보입니다.

이제 훈련으로 넘어갑니다. Scikit-Learn 생태계는 광범위한 알고리즘을 제공합니다 - 선형 모델부터 앙상블 모델에 이르기까지. " Scikit-learn 라이브러리의 회귀 모델 및 ONNX로의 내보내기" 글에서는 55개의 회귀 모델을 비교합니다. 하지만 실질적인 문제를 해결하려면 견고하고 검증된 알고리즘에 집중하는 것이 합리적입니다. 우리의 경우에는 RandomForestRegressor을 사용할 것입니다.

클래식 포레스트는 단순함과 표현력 사이의 절충안으로 비선형 종속성에서 잘 작동하고 노이즈에 강하며 공격적인 데이터 정규화를 필요로 하지 않습니다. 그리고 이것이야말로 모델링의 첫 단계에 필요한 것입니다.

다음으로 중요한 단계는 하이퍼파라미터를 선택하는 것입니다. 두 가지 매개변수를 사용하여 직접 열거해 보겠습니다: 트리 갯수( n_estimators )와 트리 깊이( max_depth ) 이는 합리적인 선택입니다: 첫 번째 매개변수는 앙상블 강도를 담당하고 두 번째 매개변수는 개별 트리가 데이터에 얼마나 깊이 몰입하는지 그리고 과적합을 어떻게 제어할지에 초점을 맞춥니다.

이를 위해 우리는 새로운 스크립트를 생성해야 합니다. MetaTrader 5에 연결하고 과거 가격 데이터를 불러오는 알고리즘은 동일하므로 설명은 생략하겠습니다.

과거 데이터를 수집한 후 피처 공간의 형성이 시작됩니다. 기본 행렬이 구성됩니다: 여기에는 가격 차이와 평활화 된 가격 차이 및 파생된 피처들이 포함됩니다.

macd_settings = [(8,16,6),(12,24,9),(36,72,27),(48,96,36)]
features = []

# Build the base feature matrix from close price changes
close = pd.DataFrame(rates_frame['close'][:-1].to_numpy(dtype=float), columns=['close'])
diff = rates_frame['close'].diff().to_numpy(dtype=float)
# Pair consecutive differences into 'last' and 'next' columns
diff = np.column_stack((diff[:-1], diff[1:]))
data_matrix = pd.DataFrame(diff, columns=['last', 'next'])
features.append('last') # Add to features list for later use
# Add a 11-period rolling mean of the previous bar move
data_matrix['last_11'] = data_matrix['last'].rolling(window=11).mean()
features.append('last_11') # Add to features list for later use 
# Add the difference between the rolling mean and current bar move
data_matrix['last_last_11'] = data_matrix['last_11'] - data_matrix['last']
features.append('last_last_11') # Add to features list for later use

다음으로 기술적 지표가 추가됩니다: SMAMACD 의 세트 구성.

# Add a 12-period simple moving average as a technical feature
data_matrix['SMA_12'] = ta.trend.sma_indicator(close['close'], window=12, fillna=True)
features.append('SMA_12') # Add to features list for later use

# Add MACD-based technical indicators for the selected parameter sets
for fast, slow, sign in macd_settings:
    macd = ta.trend.MACD(
        close['close'],
        window_slow=slow,
        window_fast=fast,
        window_sign=sign,
        fillna=True,
    )
    macd_main = macd.macd()
    dmacd = macd_main.diff()
    macd_signal = macd.macd_signal()
    dmacd_signal = macd_signal.diff()
    macd_sig_main = macd_signal - macd_main

    sufix = f"{fast:02d},{slow:02d},{sign:02d}"
    data_matrix[f'MACD_MAIN_{sufix}'] = macd_main
    features.append(f'MACD_MAIN_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_MAIN_{sufix}'] = dmacd
    features.append(f'DMACD_MAIN_{sufix}') # Add to features list for later use     
    data_matrix[f'MACD_SIGNAL_{sufix}'] = macd_signal
    features.append(f'MACD_SIGNAL_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_SIGNAL_{sufix}'] = dmacd_signal
    features.append(f'DMACD_SIGNAL_{sufix}') # Add to features list for later use
    data_matrix[f'MACD_Sig_Main_{sufix}'] = macd_sig_main
    features.append(f'MACD_Sig_Main_{sufix}') # Add to features list for later use
    data_matrix[f'DMACD_Sig_Main_{sufix}'] = macd_sig_main.diff()
    features.append(f'DMACD_Sig_Main_{sufix}') # Add to features list for later use

모든 피처들은 하나의 데이터 구조에 순차적으로 축적되며 피처의 이름은 별도의 features 목록에 설정됩니다. 이렇게 하면 추가적인 작업이 간소화되고 변수를 손실할 가능성을 줄일 수 있습니다.

같은 단계에서 목표 변수가 형성됩니다 - 주어진 기간 동안의( next_9 ) 총 가격 변동.

# Add a 9-period future return target for the next bars
data_matrix['next_9'] = data_matrix['next'].rolling(window=9).sum().shift(-8)

이렇게 이 문제는 현재 지표를 기반으로 미래의 가격 변동을 예측하는 회귀 분석으로 공식화됩니다.

다음 단계는 데이터 정리입니다. 슬라이딩 윈도우와 미분법을 적용한 후에는 필연적으로 결측값이 발생합니다. 따라서 모델 훈련이 정확하게 이루어지도록 해당 요소들은 제거되어야 합니다.

data_matrix.dropna(inplace=True)

다음으로 데이터는 시간에 따라 나뉩니다: 처음의 90%는 훈련에 사용되고 나머지 10%는 테스트에 사용됩니다. 이는 근본적으로 중요한 포인트입니다. 고전적인 머신러닝 문제와 달리 여기서는 데이터 섞기가 허용되지 않습니다. 우리는 시간의 순서를 엄격하게 유지하여 실제 과정을 시뮬레이션 합니다: 과거가 먼저 나오고 미래가 이후에 나옵니다.

# ===== 1) Data preparation =====
# Copy the raw feature matrix (preserves original data for later reference)
df = data_matrix.copy()

# Keep only features that are actually present in the DataFrame
features = [c for c in features if c in data_matrix.columns]

df = df[features + ["next_9"]]
X = df[features]
y = df["next_9"]

# ===== 2) Time-based split =====
split_idx = int(len(X) * 0.9)

X_train = X.iloc[:split_idx]
X_test = X.iloc[split_idx:]
y_train = y.iloc[:split_idx]
y_test = y.iloc[split_idx:]

이후 RandomForestRegressor 모델 하이퍼파라미터의 반복 탐색 주기가 시작됩니다. 각 매개변수 조합에 대해 전체 사이클이 수행됩니다:

  • 해당 모델은 훈련 샘플에서 훈련됩니다:
  • # ===== 3) Model =====
    results = []
    for est in range(60, 111, 5):
        for dep in range(2, 14, 1):
            print(f"\n=== Estimators: {est}, Max Depth: {dep} ===")
            model = RandomForestRegressor(
                n_estimators = est,
                max_depth = dep,
                max_leaf_nodes = None,
                min_samples_split = 6,
                min_samples_leaf = 3,
                bootstrap = True,
                random_state = 42,
                n_jobs = -1
                )
    
            model.fit(X_train, y_train)
  • 훈련 및 테스트 부분에 대한 예측을 구축하고 결과를 안정적인 형태로 가져옵니다(handling NaN and infinities);
  •         # ===== 4) Evaluation =====
            pred_train=np.nan_to_num(model.predict(X_train), nan=0.0, posinf=0.0, neginf=0.0)
            pred_test = np.nan_to_num(model.predict(X_test), nan=0.0, posinf=0.0, neginf=0.0)
  • 품질 메트릭이 계산됩니다.
  •         pt_corr = np.corrcoef(pred_test, y_test)[0, 1]
            results.append((est, dep, pt_corr))
            print("Train R2:", round(r2_score(y_train, pred_train), 6))
            print("Test  R2:", round(r2_score(y_test, pred_test), 6))
            print("Test MAE:", round(mean_absolute_error(y_test, pred_test), 8))
            print("Pred/Target corr:", round(pt_corr, 6))
    

핵심 메트릭은 테스트 샘플에서 예측값과 실제값 사이의 상관관계입니다. 이것이 바로 모델이 움직임의 방향을 포착하는 능력을 반영하는 것입니다. 근사치의 전반적인 품질과 오차 수준을 관리하기 위해 MAE도 추가로 계산됩니다.

모든 결과는 표에 저장되며 각 행은 특정 하이퍼파라미터의 조합에 해당합니다. 다음으로 이 표를 행렬의 형태로 변환하여 모델 품질이 매개변수에 어떻게 영향을 받는지 시각화 할 수 있습니다.

# --- results -> DataFrame ---
df_results = pd.DataFrame(
    results,
    columns=["Estimators", "Max Depth", "Test Correlation"]
)

# --- pivot table ---
heatmap_data = df_results.pivot(
    index="Estimators",
    columns="Max Depth",
    values="Test Correlation"
).sort_index()

마지막 단계는 Seaborn을 이용한 시각화입니다. 히트맵은 어느 매개변수 영역에서 가장 좋은 결과가 나오는지 명확하게 보여줍니다. 이를 통해 우리는 최적의 구성을 선택할 수 있을 뿐만 아니라 모델의 안정성 - 매개변수의 작은 변화에 따라 품질이 얼마나 변하는지를 평가할 수 있습니다.

# --- heatmap ---
plt.figure(figsize=(14, 10))
sns.heatmap(
    heatmap_data,
    annot=True,
    fmt=".4f",
    cmap="coolwarm",
    linewidths=0.5,
    cbar_kws={"label": "Test Correlation"}
)

plt.title("Heatmap of Test Correlation by n_estimators and max_leaf_nodes")
plt.xlabel("Max Nodes")
plt.ylabel("Estimators")
plt.tight_layout()
plt.show()

최적의 트리 개수와 최대 깊이 비율 찾기

여기서 중요한 점이 드러납니다: 모델은 무에서 유를 창조하는 것이 아닙니다. 모델은 이전에 발견된 약한 의존성만을 집계합니다. 분석 단계에서 구조에 대한 단서조차 없었다면 어떤 모델도 상황을 해결할 수 없을 것입니다. 하지만 신호가 있다면, 비록 이 신호가 약한 것이더라도 이 앙상블은 신호를 증폭하여 실용적인 용도로 사용할 수 있도록 만들 수 있습니다.

하이퍼파라미터섹션의 첫 번째 반복 그래프에서 max_depth = 10인 영역이 명확하게 보입니다. 이 값은 모델이 의존성을 포착하는 능력과 과적합을 제어하는 능력 사이에서 가장 안정적인 균형을 보여줍니다. 사실 이는 모델이 더 이상 원시적이지 않지만 아직 노이즈에 맞춰지지 않은 상태에서 작동 모드가 달성되는 지점입니다.

그러면 논리가 자연스럽게 전개됩니다. max_depth를 정의한 후에는 두 번째 단계인 max_leaf_nodes를 통해 트리 구조를 설정하는 단계로 넘어갑니다. 이와 동시에 트리의 개수(n_estimators)만큼 범위를 좁혀 이전에 안정적인 결과가 관찰된 영역만 남깁니다. 이를 통해 검색 해상도를 높일 수 있습니다: 검색 단계가 줄어들고 실제로 중요한 매개변수 영역에 집중할 수 있습니다.

이 접근 방식은 고전적인 로컬 최적화 절차를 연상시킵니다. 먼저 최대값이 결정되는 대략적인 영역을 파악한 다음 그 안에서 정밀한 미세 조정을 수행합니다. 이렇게 해서 우리는 성능이 떨어지는 것이 명백한 구성에 컴퓨팅 자원을 낭비하는 것을 방지하고 안정적인 매개변수 조합을 신속하게 찾을 수 있습니다.

코드의 변경 사항은 특정 대상을 겨냥한 것입니다: 검색 범위를 지정하고 두 번째 매개변수를 변경하는 것입니다. 스크립트 구조는 그대로 유지됩니다.

포레스트 크기와 노드 수의 최적 비율 찾기

선택 후에는 최적의 하이퍼파라미터를 확정하고 모델의 최종 훈련을 진행합니다. 구조적으로는 아무것도 변경되지 않습니다: 스크립트에서 반복 루프가 제거되고 RandomForestRegressor를 초기화할 때 매개변수 값이 지정됩니다. 데이터 준비, 샘플 분할, 훈련 및 기본 검증을 포함한 나머지 모든 로직은 변경되지 않습니다.

다음으로 좀 더 미묘하지만 근본적인 점이 드러납니다. 해당 모델은 예측 별로 신뢰도가 동일하지 않습니다. 어떤 경우에는 강한 신호를 보내고 어떤 경우에는 값이 0에 가까워 실제로는 불확실한 영역에 속합니다. 모든 예측이 동등하게 취급되면 전략은 필연적으로 잡음 속에서 거래하게 됩니다.

이는 자연스러운 가설로 이어집니다: 약한 신호는 무시하고 모델이 충분한 신뢰도를 보이거나 예상되는 변동이 비용을 상쇄하는 경우에만 작업을 진행하라. 이는 이미 기능 모델에서 트레이딩 필터 모델로의 전환을 의미합니다.

이 코드는 불필요한 복잡함 없이 깔끔하게 이 아이디어를 구현합니다. 훈련 샘플을 기반으로 절대 예측값의 임계값이 계산됩니다.

# ===== 5) Simple PnL prototype =====
# Calculate strategy metrics for a vector of thresholds without an explicit loop
percentiles = np.arange(10, 100, 5)
thresholds = np.percentile(np.abs(pred_train), percentiles)

이는 중요한 점입니다: 임계값은 훈련 데이터셋 에서 결정되어 테스트 데이터셋에 적용되므로 실험의 타당성이 유지됩니다.

이후 예측값과 해당 임계값으로 구성된 행렬이 생성됩니다. 각 레벨마다 마스크가 계산됩니다 - 어떤 신호가 필터를 통과하는지. 포지션은 일반적인 상관관계를 고려하여 예측의 부호에 따라 결정되며 이를 통해 모델이 가진 성격에 맞게 매매 방향을 유지할 수 있습니다.

# Build a matrix where each column repeats the test predictions
pred_matrix = np.tile(pred_test[:, None], (1, thresholds.size))
threshold_matrix = thresholds[None, :]

# Generate a mask per threshold and compute sign positions
mask = np.abs(pred_matrix) >= threshold_matrix
position = np.sign(pred_matrix) * np.sign(pt_corr) * mask.astype(float)

이후 수익성은 포지션과 실제 변동폭의 곱으로 계산됩니다. 고정 비용(스프레드 / 수수료)가 차감되고 누적 자본 곡선이 만들어집니다.

# Broadcast y_check to match the threshold matrix shape
y_check_matrix = np.tile(y_check.values[:, None], (1, thresholds.size))
# Subtracting swap cost from the target to get a more realistic PnL estimate
strategy_ret = position * y_check_matrix - np.abs(position)*(0.00021)

# Compute equity curves for each threshold column
equity = np.cumsum(strategy_ret, axis=0)

결과는 쌓이고 표 형식으로 표시됩니다:

  • final_equity — 최종 수익성;
  • mean_return — 평균 거래 결과;
  • win_rate — 수익 거래 비율.
# Aggregate results into a DataFrame
results = pd.DataFrame({
    'percentile': percentiles,
    'threshold': thresholds,
    'final_equity': equity[-1, :],
    'mean_return': np.sum(strategy_ret, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9),
    'win_rate': np.sum(strategy_ret > 0, axis=0)/(np.sum(strategy_ret != 0, axis=0)+1e-9)
})

print(results.to_string(index=False, float_format='%.8f'))

실질적인 관점에서 볼 때 이것은 더 이상 단순한 모델에 대한 평가가 아니라 거래 시스템의 시작이라고 할 수 있습니다. 우리는 예측 정확도를 확인할 뿐만 아니라 다양한 정렬 수준에서 수익화 되는 방식도 즉시 평가합니다.


5. ONNX로의 전환

우리는 모델을 훈련시켰습니다. 그 다음의 논리적인 단계는 의사 결정 루프에서 사람을 배제하는 것입니다 - 루프 만들기 모델 신호에 기반한 수동 거래는 자동 거래보다 거의 항상 열등합니다. 연속성이 부족하고 반응 속도가 떨어지며 심리적 요인이 추가되기 때문입니다. 실제 매매에서는 이러한 현상이 시스템적 왜곡으로 이어집니다 - 예를 들어 진입 기회 상실, 조기 이탈, 자신의 모델에 대한 불신 등이 발생합니다.

MetaTrader 5 플랫폼은 두 가지의 자동화 경로를 제공합니다. 첫 번째 방법은 파이썬 스크립트를 실행하여 거래를 직접 실행하는 것입니다. 두 번째는 모델을 ONNX 형식으로 변환한 후 MQL5 EA에서 사용하는 것입니다. 실제로 두 번째 선택지가 더 성숙한 것처럼 보입니다.

ONNX 포맷은 여러 문제를 한 번에 해결합니다. 이 모델은 간결하고 독립적인 형태로 만들어져 있습니다. 컴퓨터 간 전송이 쉽게 됩니다 - 필요한 것은 터미널뿐입니다. 전략 테스터에서 모든 테스트를 수행하는 것이 가능해집니다. 또한 성능 저하도 없습니다: 터미널은 GPU (CUDA)를 포함한 하드웨어 가속을 지원하며 이는 앙상블 모델을 사용할 때 특히 중요합니다.

변환 과정은 상당히 간단합니다. 먼저 모델의 입력에 대해 정의합니다 - 즉 피처 공간의 차원.

# Number of features used for model input
n_features = X_train.shape[1]

# Describe the model input shape for ONNX conversion
initial_type = [("float_input", FloatTensorType([None, n_features]))]

다음으로 Scikit-Learn에서 훈련된 모델은 적절한 변환기를 통해 ONNX형식으로 변환되어 디스크에 저장됩니다.

# Convert the trained sklearn model to ONNX format
onnx_model = convert_sklearn(model, initial_types=initial_type)

# Save the ONNX model to disk
with open(onnx_model_path, "wb") as f:
    f.write(onnx_model.SerializeToString())

그 다음에는 필수적인 검증 단계가 이어집니다. 해당 모델은 ONNX 런타임을 통해 로드되며 예측값을 계산하는 데에도 동일한 데이터가 사용됩니다.

# Load the ONNX model for inference
sess = rt.InferenceSession(onnx_model_path)

input_name = sess.get_inputs()[0].name

# ONNX runtime expects float32 input arrays
X_test_np = X_test.astype(np.float32).values

onnx_preds = sess.run(None, {
    input_name: X_test_np
})[0].ravel()

그런 다음 이러한 결과들을 Sklearn 모델의 원래 결과와 비교합니다.

# Compare ONNX predictions with sklearn predictions
sk_preds = model.predict(X_test)

print("Correlation:", np.corrcoef(sk_preds, onnx_preds)[0, 1])
print("Max diff:", np.max(np.abs(sk_preds - onnx_preds)))

여기에는 두 가지 핵심 기준이 있습니다:

  • 예측 간의 상관관계는 1에 가까워야 합니다.
  • 최대 오차는 무시할 수 있을 정도로 작아야 합니다.

이러한 조건이 충족되면 전송이 성공적으로 완료되었고 모델이 거래 시스템에 통합될 준비가 되었다고 볼 수 있습니다.

실질적인 관점에서 볼 때 이는 파이썬 연구 환경에서 실제 사용 환경으로의 최종적인 전환입니다. 이 모델은 더 이상 실험적인 것이 아니라 자율적이고 재현 가능하며 테스트 및 실제 거래에 적합한 인프라의 일부가 됩니다.


6. 전략 테스터에서 테스트

파이썬 측 작업이 완료되면 로직을 MetaTrader 5 런타임 환경으로 이전합니다. 이 경우 모델은 연구 도구로서의 역할을 멈추고 거래 알고리즘의 일부가 됩니다. 코드 구조는 초기화 → 데이터 준비 → 예측 → 거래 결정이라는 이미 익숙한 작업 흐름을 따라야 합니다.

초기화는 OnInit 메서드에서 수행됩니다. 이 단계에서는 리소스에서 ONNX 모델이 로드되고 OnnxCreateFromBuffer를 통해 런타임 환경이 생성됩니다.

int OnInit()
  {
//---
   if(!Symb.Name("EURUSD_i"))
      return INIT_FAILED;
   Symb.Refresh();
//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      return INIT_FAILED;
//--- load models
   onnx = OnnxCreateFromBuffer(model, ONNX_DEFAULT);
   if(onnx == INVALID_HANDLE)
     {
      Print("OnnxCreateFromBuffer error ", GetLastError());
      return INIT_FAILED;
     }
   const ulong input_state[] = {1, Inputs.Size()};
   if(!OnnxSetInputShape(onnx, 0, input_state))
     {
      Print("OnnxSetInputShape error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }
   const ulong output_forecast[] = {1, Forecast.Size()};
   if(!OnnxSetOutputShape(onnx, 0, output_forecast))
     {
      Print("OnnxSetOutputShape error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }

다음으로 입력 및 출력 형태를 명시적으로 설정합니다 - 이는 모델이 엄격하게 고정된 수의 피처들을 기대하기 때문에 매우 중요합니다. 만약 이 단계에서 오류가 발생하면 잘못된 추론으로 이어집니다.

훈련 과정에서 사용된 것과 동일한 매개변수로 설정된 SMA 지표와 MACD는 병렬로 초기화됩니다.

//--- Indicators
   if(!ciSMA.Create(Symb.Name(), TimeFrame, 12, 0, MODE_SMA, PRICE_CLOSE))
     {
      Print("SMA create error ", GetLastError());
      OnnxRelease(onnx);
      return INIT_FAILED;
     }
   ciSMA.BufferResize(2);
   for(uint i = 0; i < ciMACD.Size(); i++)
     {
      if(!ciMACD[i].Create(Symb.Name(), TimeFrame, int(macd_set[i, 0]),
                int(macd_set[i, 1]), int(macd_set[i, 2]), PRICE_CLOSE))
        {
         PrintFormat("MACD %d create error %d", i, GetLastError());
         OnnxRelease(onnx);
         return INIT_FAILED;
        }
      ciMACD[i].BufferResize(4);
     }
//---
   return(INIT_SUCCEEDED);
  }

이것은 매우 중요한 점입니다. MQL5의 피처들은 훈련 과정에서 모델에 입력된 피처들과 동일해야 합니다. 어떠한 불일치라도 있을 경우 예측 능력이 파괴됩니다.

핵심 로직은 OnTick 메서드에 집중되어 있지만 새로운 바들이 열리는 이벤트에 대해서는 IsNewBar 필터를 사용합니다. 이렇게 하면 모델이 매 틱마다 다시 계산되는 것을 방지하고 계산을 시간 프레임과 동기화할 수 있습니다.

void OnTick()
  {
//---
   if(!IsNewBar())
      return;

다음으로는 현재 포지션 수익 처리 블록이 나옵니다 - 이는 방향별 볼륨과 수익을 간단히 집계하는 것입니다. 이는 이미 진입한 거래를 통제하는 데 필요합니다.

   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   int total = PositionsTotal();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
     }

Inputs 초기 데이터 벡터는 그 후에 생성됩니다. 기본적으로 파이썬을 이용한 Feature Engineering 과정을 여기에서 수동으로 재현했습니다.

  • 기본 피처;
  • //--- prepare input data
       ciSMA.Refresh();
       for(uint i = 0; i < ciMACD.Size(); i++)
          ciMACD[i].Refresh();
       if(!Rates.CopyRates(Symb.Name(), TimeFrame, COPY_RATES_CLOSE, 1, 12))
         {
          Print("CopyRates error ", GetLastError());
          return;
         }
       Inputs[0] = float(Rates[11] - Rates[10]);
       Inputs[1] = float(Rates[11] - Rates[0]) / 11;
       Inputs[2] = float(Inputs[1] - Inputs[0]);
    
  • SMA ;
  •    Inputs[3] = float(ciSMA.Main(1));
  • MACD 블록 및 그 미분.
  •    for(uint i = 0; i < ciMACD.Size(); i++)
         {
          Inputs[4 + i * 6] = float(ciMACD[i].Main(1));
          Inputs[5 + i * 6] = float(Inputs[4 + i * 6] - ciMACD[i].Main(2));
          Inputs[6 + i * 6] = float(ciMACD[i].Signal(1));
          Inputs[7 + i * 6] = float(Inputs[6 + i * 6] - ciMACD[i].Signal(2));
          Inputs[8 + i * 6] = Inputs[6 + i * 6] - Inputs[4 + i * 6];
          Inputs[9 + i * 6] = Inputs[7 + i * 6] - Inputs[5 + i * 6];
         }
    

색인 체계를 참고해 주십시오: 각각의 피처는 엄격하게 정의된 위치를 차지합니다. 이는 모델과 실행 간의 계약입니다. 만약 순서가 어긋나면 모델은 왜곡된 입력값을 기반으로 작동하기 시작합니다.

피처 준비가 완료되면 OnnxRun이 호출됩니다.

//--- run the inference
   if(!OnnxRun(onnx, ONNX_LOGLEVEL_INFO, Inputs, Forecast))
     {
      Print("OnnxRun error ", GetLastError());
      return;
     }

출력 결과는 예측치입니다 - 예상 가격 변동폭. 다음은 실질적인 부분입니다 - 신호 해석.

이 코드는 간단하지만 효율적인 논리를 사용합니다:

  • 약한 신호를 차단하기 위해 임계값이 도입되었습니다. 우리는 훈련 결과를 바탕으로 판단합니다.
  • 상관관계의 방향이 고려됩니다. 그러므로 필요한 경우 모델은 반전될 수 있습니다.
  • 예측치가 임계값을 초과하면 포지션에 진입합니다. 신호가 사라지면 해당 포지션은 청산됩니다.
   Symb.Refresh();
   Symb.RefreshRates();
   double min_lot = Symb.LotsMin();
   double step_lot = Symb.LotsStep();
   double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point();
//--- buy control
   if(Forecast[0]*direction >= threshold)
     {
      double buy_lot = min_lot;
      if(buy_value <= 0)
         Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), 0, 0);
     }
   else
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }
//--- sell control
   if(Forecast[0]*direction <= -threshold)
     {
      double sell_lot = min_lot;
      if(sell_value <= 0)
         Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), 0, 0);
     }
   else
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
  }

따라서 이 모델은 방향성 움직임 필터로 사용됩니다. 이는 중요한 차이점입니다: 우리는 예측된 모든 값을 거래하는 것이 아니라 신호 강도 기준을 통과한 값만 거래합니다.

이렇게 생성된 EA는 핵심적인 검증 과정을 거치는데 2026년 1분기의 과거 데이터를 사용하여 MetaTrader 5 전략 테스터에서 테스트를 진행합니다. 이는 더 이상 모델에 대한 추상적인 평가가 아니라 실제에 가까운 구현 시나리오입니다.

이 테스트 형식은 근본적으로 중요합니다. 파이썬 단계에서는 지표를 통해 모델을 평가했지만 여기서는 피처 생성부터 포지션 진입 및 청산 로직에 이르는 전체 시스템을 테스트합니다. 실제로 이 전략은 최대한 실제 상황과 유사한 조건에서 첫 번째 시험대를 맞이합니다.

테스트 단계는 가설 및 데이터 분석에서 모델 생성 그리고 자동화 및 과거 데이터에 대한 검증에 이르기까지 개발 과정을 마무리합니다. 통계적으로 미약한 상호 관계가 실질적인 거래 도구로 성공적으로 전환되었는지 여부가 바로 이 지점에서 명확해 집니다.

하지만 ONNX 모델을 EA에 통합하는 것이 유일한 사용법은 아닙니다. 수동 거래를 선호하는 사람들을 위해 MetaTrader 5는 모델을 사용자 지정 지표에 직접 삽입할 수 있는 기능을 제공합니다. 이 경우 모델은 트레이더를 대신하여 결정을 내리는 것이 아니라 트레이더가 자신만의 해석을 할 수 있는 신호를 생성하는 분석 도구 역할을 합니다.

공학적인 관점에서 보면 실질적인 차이점은 거의 없습니다. ONNX 모델 연결 메커니즘과 초기 데이터 준비 및 추론 호출은 EA의 구현과 완전히 동일합니다. 단지 적용 방식만 달라집니다: 자동으로 포지션을 개설하는 대신 모델 결과를 차트로 시각화 하거나 의사 결정 시 추가되는 필터로 활용합니다.

RandomForest를 사용한 지표

이러한 접근 방식에는 장점이 있습니다. 이 방법은 모델 신호가 고전적인 분석과 유연하게 결합될 수 있도록 하며 알고리즘의 신뢰성을 위한 요구 조건을 줄여줍니다 - 모델은 해결책의 유일한 원천이 아니라 보조적인 역할을 하게 됩니다.


결론

파이썬MetaTrader 5를 통합하면 거래 솔루션 개발을 위한 완벽하고 엔지니어링 검증을 거친 프레임워크가 구축됩니다 - 개념 구상부터 실제 구현까지. 이 글에서는 데이터 수집 및 분석부터 가설 검증 및 모델 구축 그리고 실제 실행 환경에서의 구현 및 테스트에 이르기까지 이 과정을 하나씩 살펴보았습니다.

이 접근 방식의 핵심적인 장점은 역할 분담입니다. 파이썬은 데이터 처리, 피처 생성, 통계 분석 및 모델 훈련과 같은 연구 부분을 담당합니다. MetaTrader 5는 시장 데이터 접근, 전략 테스트 및 거래 인프라를 제공하여 거래를 실행할 수 있도록 지원합니다. 이는 실험실생산 현장이 결합된 전형적인 사례로 각 환경이 본래의 목적에 맞게 사용됩니다.

ONNX 형식을 사용하면 추가적인 이점을 얻을 수 있습니다. 이 모델은 개발 환경에 구애 받지 않고 터미널이 설치된 모든 장치에서 실행될 수 있도록 이식성이 뛰어납니다. 이를 통해 시스템을 확장하는 것이 간소화되고 테스트 속도가 빨라지며 환경 간 비호환성과 관련된 위험이 줄어듭니다.

이 글에서 사용된 프로그램

# 이름 타입 설명
1 Experts\Integration\Integration.mq5 Expert Advisor 터미널에서 모델을 테스트하기 위한 EA
2 Indicators\Integration\Integration.mq5 지표 차트에 신호를 표시하는 지표
3 Scripts\Integration\load_data.py 스크립트 데이터 로딩 스크립트
4 Scripts\Integration\look_model_param_rf.py 스크립트 하이퍼파라미터 열거 스크립트
5 Scripts\Integration\create_model_rf.py 스크립트 모델 훈련 및 ONNX 형식으로 내보내기 스크립트

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

파일 첨부됨 |
MQL5.zip (13.99 KB)
최근 코멘트 | 토론으로 가기 (2)
Denis Kirichenko
Denis Kirichenko | 17 4월 2026 에서 11:17

아카이브에 포함된 load_data.py 스크립트 파일에는 다음과 같은 줄들이 있습니다:

#  Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD_i", mt5.TIMEFRAME_H1, utc_from, utc_to)

반면 기사 본문에는 다음과 같이 나와 있습니다:

#  Get bars from EURUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

사소한 부분이지만 테스트할 때 바로 눈치채지 못했네요...

결국 파이썬 3.14.3 버전을 포기해야 했습니다. 저는 VS에서 파이썬을 사용하고 있는데, 거기서는 3.11 버전에서만 디버깅이 가능하거든요.
Gloria Diana
Gloria Diana | 4 5월 2026 에서 16:55
이 자료를 제공해 주셔서 감사합니다
새로운 기능: MQL5의 커스텀 인디케이터 새로운 기능: MQL5의 커스텀 인디케이터
MetaTrader5와 MQL5의 새로운 기능 전체를 나열하지는 않겠습니다. 종류도 많은 데다가, 별도의 설명이 필요한 기능들도 있거든요. 객체 지향 프로그래밍을 이용한 코드 작성법 또한 다음에 알아보도록 하겠습니다. 다른 기능들과 함께 설명하기에는 조금 어려운 이야기일 수 있으니까요. 이 글에서는 인디케이터와 인디케이터의 구조, 드로잉 타입과 프로그래밍 디테일을 MQL4와 비교해 볼게요. 초보자 분들께 많은 도움이 되면 좋겠고 기존에 사용하시던 개발자 분들도 뭔가 새로운 걸 얻어 가실 수 있길 바랍니다.
루틴 없는 알고리즘 트레이딩: MetaTrader 5에서 SQLite를 사용하여 트레이딩 분석을 빠르게 하는 방법 루틴 없는 알고리즘 트레이딩: MetaTrader 5에서 SQLite를 사용하여 트레이딩 분석을 빠르게 하는 방법
이 글에서는 SQLite를 사용하여 MQL5로 거래 일지를 관리하기 위한 최소한의 작업 세트를 소개합니다: 여기에는 거래, 신호 및 이벤트, 지수, 준비된 명세서 및 거래 테이블 구조와 표준 분석 SQL 쿼리가 포함됩니다. MetaTrader 5의 통계 대시보드와의 통합과 MetaEditor를 통한 데이터베이스 작업이 시연됩니다. 이 접근 방식을 통해 EA 코드를 복잡하게 만들지 않고도 저널을 자동화하고 계산 속도를 향상시키고 분석을 하는 것이 가능해집니다.
새 MetaTrader 와 MQL5를 소개해드립니다 새 MetaTrader 와 MQL5를 소개해드립니다
본 문서는 MetaTrader5의 간략 리뷰입니다. 짧은 시간 내에 시스템의 모든 세부 사항을 안내해드리기는 어렵습니다 - 테스트는 2009.09.09에 시작되었습니다. 이는 상징적인 일자로, 전 이것이 행운의 숫자가 될거라 믿어 의심치않습니다. 제가 새 MetaTrader 5 터미널과 MQL5 베타버전을 받은지 며칠이 지났습니다. 아직 모든 기능을 사용해본 것은 아니지만, 벌써부터 감명깊네요.
외환 거래에서 포트폴리오 최적화: VaR와 마코위츠 이론의 결합 외환 거래에서 포트폴리오 최적화: VaR와 마코위츠 이론의 결합
외환 시장에서 포트폴리오 거래는 어떻게 이루어지나요? 포트폴리오 비율 최적화를 위한 마코위츠 포트폴리오 이론과 포트폴리오 위험 최적화를 위한 VaR 모델이 어떻게 통합될 수 있을까요? 여기서 우리는 포트폴리오 이론에 기반한 코드를 개발해서 한편으로는 낮은 위험을, 다른 한편으로는 만족스러운 장기적인 수익성을 확보해 볼 것입니다.