MetaTrader 5: 전략에 맞게 시장을 구축하기 — 렌코/레인지/볼륨, 합성 상품 그리고 사용자 맞춤형 심볼 스트레스 테스트
소개
MetaTrader 5의 표준 차트는 신뢰할 수 있는 도구이지만 시간 프레임, 브로커의 호가 정보, 그리고 시간/날짜의 격자 기반의 차트에서 벗어나지 못합니다. 현대 알고리즘 거래에서는 이러한 방식은 종종 불충분합니다: 소위 변동성 노이즈, 히스토리의 공백, 고정된 시간 간격 등이 시장의 실제 역학을 왜곡하기 때문입니다. 만약 터미널이 여러분의 시장 분석 뿐만 아니라 특정 전략의 필요에 맞춰 시장의 표현 방식을 디자인할 수 있게끔 해준다면 어떨까요?
사용자 맞춤형 심볼 API 개발을 통해 MetaTrader 5는 아키텍처를 변경했습니다: 이제 트레이더는 단순히 호가를 수동적으로 소비하는 존재가 아니라 데이터의 설계자가 되는 것입니다. 사용자 맞춤형 심볼은 시각화를 위한 오프라인 차트가 아닙니다. 자체적인 틱 기록, 계약 사양 및 전략 테스터에서의 지원을 갖춘 완전한 기능을 갖춘 터미널 객체입니다.
이제 시간과는 관련 없는 차트(렌코 바, 레인지 바, 동일 볼륨 바)를 생성하고 합성 상품 및 바스켓을 구성하고 스프레드, 손절매 수준 및 증거금 요구 사항 등을 코드에서 직접 임의로 변경하여 스트레스 테스트를 실행할 수 있습니다.
이 글에서는 사용자 맞춤형 상품을 다룰 때의 실질적인 측면, 즉 데이터 저장 구조, 기본 MQL5 API, 틱 집계 알고리즘 및 히스토리 수정에 대해 살펴보겠습니다. 스탠다드 EA가 어떻게 사용자 맞춤형 심볼에서 실제 매매 종목으로 매매 주문을 연결하도록 하는지 그 방법을 살펴보겠습니다.
그림 1. 표준 구성 요소로 여러분만의 시장을 구축하세요
사용자 맞춤형 심볼이란: 아키텍처, 스토리지 및 API
MetaTrader 5의 사용자 맞춤형 심볼은 이전 세대 플랫폼에서 구현되었던 것처럼 시각적 관찰을 위한 오프라인 차트가 아닙니다. 이제 자체적인 틱 기록, 계약 명세, 그리고 전략 테스터에서 작동하는 완전한 기능을 갖춘 터미널 객체가 되었습니다.
브로커가 제공하고 호가 정보(호가 제공업체)의 품질에 의존하는 스탠다드 상품과는 달리 사용자 맞춤형 심볼을 사용하면 트레이더가 직접 데이터 설계자가 될 수 있습니다. 어떤 호가를 어떤 형식으로 어떤 정확도 수준으로 히스토리에 포함할지는 사용자가 직접 결정합니다. 이를 통해 브로커가 암호화폐, 은행 간 스프레드 등을 취급하지 않을 경우에도 이들을 분석할 뿐만 아니라 기존 데이터를 변형하는 것도 가능해집니다.(렌코, 동일 볼륨, 합성 지수)
사용자 맞춤형 심볼은 브로커의 트레이딩 서버와 분리되어 있는 것입니다. 파일은 로컬에 저장됩니다. 예를 들면 다음과 같습니다: “AppData\Roaming\MetaQuotes\Terminal\[InstanceID]\bases\Custom”.
이 솔루션은 중요한 장점을 가지고 있습니다: 그것은 바로 독립성입니다. 트레이더는 브로커를 변경하거나 터미널을 업데이트할 수 있습니다. 그렇더라도 사용자 맞춤형 심볼과 누적된 히스토리는 그대로 남아있게 됩니다. 종합 시세 창에서 해당 종목들은 항상 'Custom'이라는 별도의 폴더에 위치하여 실제 종목과 사용자가 설정한 종목을 시각적으로 구분합니다.
MQL5 API 코어
코드에서 사용자 맞춤형 심볼을 처리하려면 특수 API 함수에 대한 접근 권한이 필요합니다. 전체 목록은 문서에서 찾을 수 있습니다. 이 중 우리는 주요 예로서 세 가지 함수 그룹에 초점을 맞추겠습니다.
심볼 수명 주기 관리:
- CustomSymbolCreate() — 지정된 이름과 지정된 그룹을 가진 사용자 맞춤형 심볼을 생성합니다,
- CustomSymbolDelete() — 지정된 이름을 가진 사용자 맞춤형 심볼을 제거합니다,
- SymbolSelect() — 종합 시세에 추가합니다 (이 옵션을 사용하지 않으면 해당 종목이 GUI에 표시되지 않습니다).
데이터 다운로드 관리:
- CustomTicksAdd() — 추가 MqlTick[] 타입 배열의 데이터를 사용자 맞춤형 심볼의 가격 내역에 저장합니다. 종합 시세 창에서 사용자 맞춤형 심볼이 선택되어야 합니다.
- CustomTicksReplace() — 지정된 시간 간격 동안 사용자 맞춤형 심볼의 가격 히스토리를 MqlTick[] 유형 배열의 데이터로 완전히 바꿉니다.
- CustomRatesUpdate() — 사용자 맞춤형 심볼 히스토리에 누락된 바를 추가하거나 기존 바를 MqlRates[] 형식 배열의 데이터로 바꿉니다.
속성 관리:
- CustomSymbolSetInteger()/ CustomSymbolSetDouble ()/ CustomSymbolSetString () — 계약 속성(스프레드, 정밀도, 마진 통화 등)을 설정합니다.
API 함수를 직접 호출하려면 다양한 오류를 처리하고 상태를 확인해야 합니다. 개발 속도를 높이기 위해 우리는 CiCustomSymbol 래퍼 클래스를 사용합니다. 이는 실제 심볼 속성을 생성하고 복제하며 틱을 일괄 로드 하는 로직을 캡슐화 합니다. CiCustomSymbol 클래스의 전체 코드는 아래 첨부된 CiCustomSymbol.mqh 파일에 있습니다.
다음은 사용자 맞춤형 심볼을 초기화하는 예입니다. 심볼을 생성하고 EURUSD 심볼의 속성을 복제하고 반환 코드를 확인합니다:
#include <CiCustomSymbol.mqh> //+------------------------------------------------------------------+ //| inputs | //+------------------------------------------------------------------+ input string CustomSymbolName = "EURUSD_Custom"; // new symbol name input string OriginSymbol = "EURUSD"; // source symbol //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { CiCustomSymbol symb; //--- attempt to create a symbol //--- return codes: -1 (error), 0 (already exists), 1 (successfully created) int res = symb.Create(CustomSymbolName, "", OriginSymbol, 1000000, true); if(res == -1) { Print("Error creating symbol: ", GetLastError()); return; } if(res == 0) { Print("Symbol ", CustomSymbolName, " already exists."); } else { Print("Symbol ", CustomSymbolName, " successfully created."); //--- set specific properties (for example, fixed spread of 20 points) //--- properties are changed BEFORE loading history if(!symb.SetProperty(SYMBOL_SPREAD, 20)) Print("Failed to set spread. Error: ", GetLastError()); if(!symb.SetProperty(SYMBOL_SPREAD_FLOAT, false)) Print("Failed to set spread type (fixed). Error: ", GetLastError()); } //--- symbol added to Market Watch ('true' parameter to Create) Print("Symbol created: ", CustomSymbolName); }
전체 스크립트 코드는 아래 첨부된 CreateCustomSymbol.mq5 파일에서 확인하실 수 있습니다.
사용자 맞춤형 심볼을 다룰 때는 문서에 나와 있는 몇 가지 제한 사항에 유의해야 합니다:
MQL5 클라우드 네트워크 사용 불가:
클라우드 네트워크를 통한 사용자 맞춤형 심볼에 대한 전략 최적화는 금지됩니다. 이유는 간단합니다. 트레이더들의 컴퓨터에는 같은 이름(예: EURUSD_Custom)을 가진 심볼이 저장되어 있을 수 있지만 이런 심볼의 히스토리 내역은 완전히 다를 수 있기 때문입니다. 클라우드 네트워크를 사용하면 동기화에서 혼란해지고 과도한 트래픽 발생으로 이어질 수 있습니다. 테스팅은 로컬 또는 로컬 에이전트를 통해서만 가능합니다.
마진 계산 로직:
전략 테스트 도구는 교차 금리를 사용하여 마진과 수익을 계산합니다. 만약 AUDUSD/USDCAD를 기반으로 하는 AUDCAD.custom이라는 심볼을 생성하여 테스트 중이고 계정이 USD로 설정되어 있다면 테스터는 다음이 필요합니다:
- AUDUSD(미국 달러로 호주 달러의 가치)를 찾습니다 - 마진을 계산하기 위해
- USDCAD 환율(캐나다 달러를 미국 달러로 환산하는 방법)을 찾습니다 - 수익을 계산하기 위해
필요한 모든 통화쌍이 종합 시세에 포함되어 있고 히스토리 데이터가 불러와져 있는지 확인하는 것이 중요합니다. 그렇지 않으면 테스터가 재무적 지표를 계산할 수 없습니다.
테스트 도중에 심볼 속성은 변경되지 않습니다:
사용자 맞춤형 심볼 속성(예: SYMBOL_TRADE_STOPS_LEVEL들은 손절매 최소 거리)은 테스트를 시작하기 전에 설정됩니다. 현재 테스터 버전에서는 EA 실행 중에 이러한 설정을 동적으로 변경하는 것이 불가능합니다(예: 뉴스 발표 전에 손절매 수준을 높이는 것). 테스터는 초기화 중에 심볼 매개변수를 고정합니다.
분석적 탈선: 백테스팅의 의미
왜 이렇게 복잡한 아키텍처가 필요한가요? 사용자 맞춤형 심볼을 사용하면 매매 아이디어를 테스트하는 접근 방식이 달라집니다. 특정 브로커의 불완전한 데이터(히스토리 내역의 누락이나 비정상적인 스프레드 등)에 맞춰 전략을 세우지 말고 자신만의 이상적인 실험 환경을 구축해야 합니다.
50핍 스프레드(스캘핑을 걸러내기 위해)에서 전략이 어떻게 작동하는지 확인하거나 시간이 중요하지 않은 차트(레인지/렌코)에서 거래가 어떻게 이루어지는지 확인할 수 있습니다. 이는 시장의 잡음으로부터 전략의 핵심 논리를 분리하는 데 도움이 됩니다.
시간에 독립적인 차트: 렌코 차트, 레인지 바 차트, 그리고 동일 볼륨 차트
표준 차트 주기는 편리하지만 시장에 대한 인위적인 시간 틀을 강요합니다. 예를 들어 M1 캔들은 틱 수와 관계없이 60초마다 마감됩니다. 유동성이 낮은 상황에서는 이러한 바로 이루어진 차트가 전달하는 정보가 많지 않고 변동성이 높은 순간에는 하나의 직사각형 안에 중요한 움직임을 숨깁니다.
시간 독립적 차트는 시간이 경과한 후가 아니라 가격이나 볼륨이 지정된 임계값에 도달했을 때 새로운 바를 생성함으로써 이 문제를 해결합니다. 이를 통해 다음을 수행할 수 있습니다:
- 시장의 잡음을 걸러내고 중요한 움직임에 집중;
- 분석 단계를 현재 변동성에 맞게 자동으로 조정;
- 타이머가 아닌 실제(또는 틱) 볼륨을 기준으로 축적/분산 영역을 식별.
MetaTrader 5에서 사용자 맞춤형 심볼을 사용하여 이러한 차트를 생성하는 방법을 보여드리겠습니다. 렌코 차트, 레인지 바 차트, 동일 볼륨 차트 등 세 가지 인기 있는 차트 유형을 살펴보겠습니다. 먼저, 이 세 가지 유형의 바를 표로 비교하고 설명해 보겠습니다.
| 차트 유형 | 새로운 바 형성 기준 | 최적의 활용 사례 |
|---|---|---|
| Renko | 주어진 포인트만큼의 가격 변동 | 추세 전략, 횡보세를 제외 |
| Range Bars | 바의 고가-저가의 범위가 지정된 값에 도달 | 변동성이 큰 시장에서의 스캘핑, 단기 매매 |
| 동일 볼륨 | 지정된 틱 수 또는 실제 볼륨의 누적 | 유동성 분석 대형 기관들의 관심 분야 탐색 |
주의:
MetaTrader 5의 모든 사용자 맞춤형 심볼은 M1 차트 주기 그리드에 연결됩니다. 이는 플랫폼의 아키텍처적 한계입니다: 바가 2초 만에 형성되더라도 최대 1분 이내의 정확도로 히스토리에서 타임스탬프가 기록됩니다. 변동성이 높으면 여러 개의 바들이 같은 시간대에 나타날 수 있습니다 - EA는 이러한 점을 고려해야 합니다.
기술적 구현: 틱 데이터를 바로 집계
일반적으로 시간의 흐름과 관련 없는 바를 생성하는 알고리즘은 세 가지 유형 모두에 대해 동일합니다:
- CopyTicksRange ()/ OnTick () 함수를 통해 각각 히스토리에서 틱을 읽거나 온라인으로 틱을 수신;
- 버퍼에 데이터 축적: 시가, 고가, 저가, 종가, 거래량 추적;
- 새로운 바 차트를 형성하기 위한 조건(포인트/레인지/볼륨)을 확인;
- CustomRatesUpdate()를 통해 사용자 맞춤형 심볼 데이터베이스에 바를 저장;
- CustomTicksAdd()를 통한 틱 에뮬레이션으로 MetaTrader 5 차트에서 지표 및 EA를 활성화합니다.
사용자 맞춤형 심볼 작업을 위한 기본 클래스: 모든 유형의 바를 위한 알고리즘
//+------------------------------------------------------------------+ //| class-aggregator of ticks into custom bars | //+------------------------------------------------------------------+ class CBarAggregator { private: string symbol_name; double threshold; int bar_type; ENUM_VOLUME_MODE volume_mode; MqlRates rates_buffer[]; int buffer_limit; int buffer_idx; MqlRates current_bar; double last_close; int trend; bool is_initialized; datetime last_bar_time; int symbol_digits; public: CBarAggregator(void); bool Init(const string _symbol_name, double _threshold, int _bar_type, ENUM_VOLUME_MODE _vol_mode = VOLUME_MODE_TICK); bool ProcessTick(const MqlTick &tick); void FlushBuffer(void); void Reset(void); private: void CreateBar(const MqlTick &tick, double open_price = 0.0); void CloseAndSaveBar(const MqlTick &tick, double forced_close = 0.0); void WriteBatch(void); }; //+------------------------------------------------------------------+ //| handle an incoming tick | //+------------------------------------------------------------------+ bool CBarAggregator::ProcessTick(const MqlTick &tick) { if(tick.bid <= 0 || tick.ask <= 0) return false; if(tick.time <= 0) return false; // protection from invalid ticks if(!is_initialized) { CreateBar(tick); last_close = tick.bid; last_bar_time = tick.time; // initialize with the first tick time is_initialized = true; return false; } bool bar_closed = false; if(bar_type == 0) // renko { double diff = tick.bid - last_close; double size = threshold * _Point; int req_move = (trend != 0) ? 2 : 1; if(diff >= size * req_move) { int bricks = (int)(diff / size); for(int i = 0; i < bricks; i++) { double target_close = NormalizeDouble(last_close + size, symbol_digits); CloseAndSaveBar(tick, target_close); last_close = target_close; CreateBar(tick, last_close); trend = 1; } bar_closed = true; } else if(diff <= -size * req_move) { int bricks = (int)(MathAbs(diff) / size); for(int i = 0; i < bricks; i++) { double target_close = NormalizeDouble(last_close - size, symbol_digits); CloseAndSaveBar(tick, target_close); last_close = target_close; CreateBar(tick, last_close); trend = -1; } bar_closed = true; } } else if(bar_type == 1) // range bars { double bid = NormalizeDouble(tick.bid, symbol_digits); if(bid > current_bar.high) current_bar.high = bid; if(bid < current_bar.low) current_bar.low = bid; if((current_bar.high - current_bar.low) >= threshold * _Point) { CloseAndSaveBar(tick); CreateBar(tick); bar_closed = true; } } else if(bar_type == 2) // equal volume bars { if(volume_mode == VOLUME_MODE_REAL) current_bar.real_volume += (long)tick.volume_real; else current_bar.tick_volume++; long current_vol = (volume_mode == VOLUME_MODE_REAL) ? current_bar.real_volume : current_bar.tick_volume; if(current_vol >= (long)threshold) { CloseAndSaveBar(tick); CreateBar(tick); bar_closed = true; } } return bar_closed; } //+------------------------------------------------------------------+ //| close the bar and save to the buffer | //+------------------------------------------------------------------+ void CBarAggregator::CloseAndSaveBar(const MqlTick &tick, double forced_close) { current_bar.close = (forced_close != 0.0) ? NormalizeDouble(forced_close, symbol_digits) : NormalizeDouble(tick.bid, symbol_digits); current_bar.high = NormalizeDouble(MathMax(current_bar.high, MathMax(current_bar.open, current_bar.close)), symbol_digits); current_bar.low = NormalizeDouble(MathMin(current_bar.low, MathMin(current_bar.open, current_bar.close)), symbol_digits); current_bar.spread = MathMax(0, current_bar.spread); // make sure last_bar_time > 0 (initialized) if(last_bar_time > 0 && current_bar.time <= last_bar_time) { // shift by 1 second instead of 60 to minimize distortion current_bar.time = last_bar_time + 1; } last_bar_time = current_bar.time; // final validation before the buffer if(current_bar.high < current_bar.low || current_bar.high < current_bar.open || current_bar.high < current_bar.close || current_bar.low > current_bar.open || current_bar.low > current_bar.close) { Print("🟡 Invalid bar skipped: O=", current_bar.open, " H=", current_bar.high, " L=", current_bar.low, " C=", current_bar.close, " Time=", TimeToString(current_bar.time)); return; } if(buffer_idx < buffer_limit) { rates_buffer[buffer_idx] = current_bar; buffer_idx++; } if(buffer_idx >= buffer_limit) WriteBatch(); } //+------------------------------------------------------------------+ //| save the buffer to the symbol history | //+------------------------------------------------------------------+ void CBarAggregator::WriteBatch(void) { if(buffer_idx > 0) { datetime t_from = rates_buffer[0].time; datetime t_to = rates_buffer[buffer_idx - 1].time; if(CustomRatesUpdate(symbol_name, rates_buffer) < 1) Print("CustomRatesUpdate error: ", GetLastError()); buffer_idx = 0; } } //+------------------------------------------------------------------+
CBarAggregator 클래스의 전체 코드는 CBarAggregator.mqh 파일에 있습니다.
범용 시간 독립적 바 지표
CBarAggregator 클래스를 실제 적용하기 위해 범용 지표를 생성합니다. 이를 통해 속성에서 렌코, 레인지 및 동일 볼륨 바 모드 간에 전환할 수 있으며 지정된 히스토리 간격에 따라 사용자 맞춤형 심볼을 생성할 수 있습니다.
입력:
input ENUM_CUSTOM_CHART_MODE InputMode = CMT_RENKO; // generation mode input double InputBoxSize = 10; // bar size (points) for renko/range input long InputVolLimit = 1000; // volume limit for equal-volume input string InputSuffix = ""; // symbol name suffix input datetime InputStartTime = 0; // history start (0 - last 7 days)
바 생성 모드 열거:
enum ENUM_CUSTOM_CHART_MODE { CMT_RENKO = 0, // renko CMT_RANGE = 1, // range bars CMT_EQVOL_TICK = 2, // equal tick volumes CMT_EQVOL_REAL = 3 // equal real volumes };
실시간 작동 — 히스토리를 로딩한 후 지표는 온라인으로 계속 작동하며 새로운 틱을 처리합니다:
//+------------------------------------------------------------------+ //| handle a new tick | //+------------------------------------------------------------------+ void ProcessNewTicks() { if(last_tick_time_msc==0) return; MqlTick ticks[]; int copied=CopyTicksRange(_Symbol,ticks,COPY_TICKS_ALL,last_tick_time_msc,0); if(copied>0) { for(int i=0;i<copied;i++) { aggregator.ProcessTick(ticks[i]); last_tick_time_msc=ticks[i].time_msc; } aggregator.FlushBuffer(); } }
이 지표는 자체적으로 차트를 그리지 않습니다. 지표의 목적은 사용자 맞춤형 심볼을 생성하고 데이터를 채우는 것입니다. 시작하려면 지표를 아무 차트(예: EURUSD) 위에 드래그해서 놓으세요. 그러면 속성 창이 열립니다. 모드(예: MODE_RENKO), 박스 크기(InputBoxSize) 및 히스토리 시작 시간을 선택하세요.
지표 적용 순서:
- 지표가 바를 생성하기 시작합니다(생성 시작 및 완료에 대한 메시지가 로그에 나타납니다).
- 생성 완료 메시지가 나타나면 종합 시세(Ctrl+M)를 여세요.
- 생성된 상품(예: EURUSD_Renko_10)을 심볼 목록에서 찾아 선택하세요.
- 새 차트를 엽니다(Ctrl+N).
- 차트주기를 M1으로 설정하세요.
그림 2~4는 서로 다른 모드에서의 지표의 작동 결과를 보여줍니다.
그림 2. 렌코 바
그림 3. 레인지 바
그림 4. 동일 볼륨 바
시간 독립적 바의 특징과 사용 사례를 간략하게 살펴보겠습니다.
렌코 — 이전 바의 종가 대비 가격이 지정된 바의 크기(포인트)를 넘어설 때만 새로운 바가 그려집니다. 바의 방향(상승/하락)은 돌파 방향에 따라 결정됩니다. 그림자는 기본적으로 표시되지 않지만 바 안에서의 변동성을 분석해야 할 경우 사용할 수 있습니다. 강한 움직임이 있을 경우 1분 안에 여러 개의 바가 형성될 수 있습니다. 바의 종가는 돌파 가격이지 바의 종가가 아닙니다.
렌코 기반 전략 예시:
- 진입 - 바 마감 시 빠른 MA(9)와 느린 MA(21)의 교차점,
- 필터 - ATR(14) 값이 지정된 기준값보다 크면 횡보시의 잘못된 돌파를 필터링합니다.
- 청산 - 이동평균선 역교차 또는 고정 손절매 지점
- 특이한 점은 0번째(마지막) 렌코 바가 항상 불완전하기 때문에 매매는 종가(PRICE_CLOSE)를 기준으로 이루어진다는 것입니다.
레인지 바 — 고가와 저가 사이의 변동폭이 포인트 값으로 지정된 값에 도달하면 새로운 바가 생성됩니다. 렌코 차트와 달리 이 차트는 종가 뿐만 아니라 바 전체의 움직임을 고려합니다. 고정된 크기 값 대신 ATR을 기반으로 하는 동적 바 크기(레인지)를 사용할 수 있습니다.
Range 사이즈란? Range 사이즈는 새로운 바 차트가 형성되기 시작하는 기준 가격 변화 값을 설정합니다. 1 Range는 하나의 최소 가격 변동과 같습니다. 이 값은 다음 방정식으로 나타낼 수 있습니다: 1 Range = Tick Size.
스캘핑에 사용 시 장점:
- 변동성이 낮은 기간에는 바가 천천히 형성됩니다 - 가짜 신호가 줄어듭니다,
- 변동성이 급증하면 바가 차트에 더 많이 나타납니다 따라서 빠른 움직임을 포착할 수 있습니다,
- 바 경계에 명확한 지지/저항선이 나타납니다.
동일 볼륨 바 — 지정된 틱 수(외환 거래의 경우) 또는 실제 거래량(증권거래소 상품의 경우)이 누적된 후 새로운 바가 생성됩니다. 동일 볼륨의 바를 사용하면 움직임을 같은 기준으로 비교할 수 있습니다:
- 바가 빠르게 형성되는 영역은 높은 활동성을 나타내며 따라서 현재 레인지가 돌파될 가능성이 있습니다.
- 차트의 빈 영역은 관심이 부족하다는 것을 나타내며 따라서 추세 반전의 가능성이 있다는 것입니다.
비고:
전략 테스터는 표준 규칙에 따라 사용자 맞춤형 심볼에 대한 틱을 생성합니다: 바 구성(OHLC)을 기반으로 합니다. 하지만 시간 독립적 차트(특히 렌코 차트)의 경우 해당 차트의 종가는 그 정의에 의해면 이후의 움직임을 예측하는 지표가 됩니다. 테스터에서 0(미완성) 바의 구성을 보고는 이상적인 진입이라는 착각을 할 수 있습니다.
해결책:
- 신호는 항상 첫 번째 바(완료된 바)에서 가져와야 하며 0에서 가져와서는 안 됩니다;
- 테스터를 완료된 바 모드로 설정하세요;
- 실제 심볼 주문 라우팅을 사용하십시오(아래 참조) - 이렇게 하면 테스터에서 틱 생성 아티팩트가 제거됩니다.
합성 상품: 스프레드, 바스켓 및 시장간 연계
시장은 드물게 독립적으로 움직입니다 - 금융 상품들은 서로 연관되어 있는 경우가 많습니다. EURUSD는 GBPUSD를 끌어당기는 경향이 있고 유가는 CAD, AUD, NOK의 투자 심리에 영향을 미치며 S&P 500과 DAX는 종종 함께 움직입니다. 하지만 단순히 상관관계를 수동적으로 관찰하는 대신 이러한 요소들을 수학적으로 결합하는 단일 도구를 만든다면 어떨까요? MetaTrader 5의 사용자 맞춤형 심볼을 사용하면 표준적인 브로커의 도구를 넘어 여러분 자신만의 자체적인 분석 시스템을 구축할 수 있습니다: 스프레드, 바스켓, 차익 거래 쌍 및 역상관 지수 등.
합성의 수학:
모든 합성 상품의 기본은 선형 조합입니다:
Synth = k₁·Asset_A ± k₂·Asset_B ± ... ± kₙ·Asset_N.
보통 두 자산의 스프레드를 계산하는 데에는 간소화된 공식이 가장 흔히 사용됩니다:
Spread = Price_A - Ratio · Price_B,
여기서 Ratio는 자산의 변동성 또는 가격 스케일을 균등화 하는 정규화 계수입니다. 정규화 없이 스프레드를 계산하면 더 비싼 금융 상품 쪽으로 편향되어 오실레이터 신호와 마진 요구 사항 계산이 왜곡됩니다.
주요 기술적 과제는 틱을 동기화하는 것입니다. 서로 다른 유동성 풀에서 호가를 비동기적으로 수신하면 스프레드가 불규칙해지고 잘못된 돌파가 발생할 수 있습니다. 이 문제는 버퍼링 시간 창을 사용하고 각 합성 레그에 대해 마지막으로 알려진 가격을 고정함으로써 이 문제를 해결합니다.
합성 틱 생성기:
//+------------------------------------------------------------------+ //| synthetic tick generator based on two assets | //+------------------------------------------------------------------+ class CSyntheticTickGenerator { private: string symbol_a; string symbol_b; string synth_name; double ratio; double last_price_a; double last_price_b; int symbol_digits; double point; public: //+------------------------------------------------------------------+ //| initializer | //+------------------------------------------------------------------+ bool Init(const string _sym_a, const string _sym_b, const string _synth, double _ratio) { symbol_a=_sym_a; symbol_b=_sym_b; synth_name=_synth; ratio=_ratio; symbol_digits=(int)SymbolInfoInteger(_sym_a, SYMBOL_DIGITS); point=SymbolInfoDouble(_sym_a, SYMBOL_POINT); last_price_a=0.0; last_price_b=0.0; return true; } //+------------------------------------------------------------------+ //| handle an incoming tick | //+------------------------------------------------------------------+ void ProcessTick(const MqlTick &tick, const string source_symbol) { if(tick.bid<=0 || tick.ask<=0) return; //--- update the last known price for the corresponding leg if(source_symbol==symbol_a) { last_price_a=tick.bid; } else if(source_symbol==symbol_b) { last_price_b=tick.bid; } //--- waiting for prices to appear for both assets if(last_price_a<=0 || last_price_b<=0) return; //--- calculate synthetic price with normalization double synth_bid=NormalizeDouble(last_price_a-ratio*last_price_b, symbol_digits); double base_spread=tick.ask-tick.bid; double synth_ask=NormalizeDouble(synth_bid+base_spread, symbol_digits); //--- tick structure formation MqlTick synth_tick={0}; synth_tick.time=tick.time; synth_tick.time_msc=tick.time_msc; synth_tick.bid=synth_bid; synth_tick.ask=synth_ask; synth_tick.flags=TICK_FLAG_BID | TICK_FLAG_ASK; //--- writing a custom symbol to the database MqlTick batch[1]; batch[0]=synth_tick; CustomTicksAdd(synth_name, batch); } }; //+------------------------------------------------------------------+
샘플 전략 — 합성 스프레드는 통계적 차익거래 및 평균 회귀 전략에 이상적입니다. 전통적인 접근 방식은 스프레드가 이동 평균에서 벗어난 정도를 표준 편차(z-점수) 단위로 추적하는 것입니다.
신호 논리:
- 진입 — |z-score| > 2.0이면 스프레드 편차는 통계적으로 비정상적입니다;
- 청산 — |z-score| < 0.5은 평균 회귀 발생을 의미하며 포지션은 청산됩니다;
- 위험 관리 - 손절매는 바스켓 변동성과 연결됩니다(예: 2.5 × ATR(20)).
z-score 계산 및 신호 생성 함수는 다음과 같습니다:
//+------------------------------------------------------------------+ //| z-score calculation for synthetic spread | //+------------------------------------------------------------------+ double CalculateSpreadZScore(const string synth_symbol, int ma_period, int std_dev_period) { static int ma_handle=INVALID_HANDLE; static int std_handle=INVALID_HANDLE; //--- initialize handles on first call if(ma_handle == INVALID_HANDLE) { ma_handle=iMA(synth_symbol, PERIOD_M1, ma_period, 0, MODE_SMA, PRICE_CLOSE); if(ma_handle==INVALID_HANDLE) { Print("Error creating iMA handle: ", GetLastError()); return 0.0; } } if(std_handle == INVALID_HANDLE) { std_handle = iStdDev(synth_symbol, PERIOD_M1, std_dev_period, 0, MODE_SMA, PRICE_CLOSE); if(std_handle==INVALID_HANDLE) { Print("Error creating iStdDev handle: ", GetLastError()); return 0.0; } } double ma_buf[], std_buf[], close_buf[]; ArraySetAsSeries(ma_buf, true); ArraySetAsSeries(std_buf, true); ArraySetAsSeries(close_buf, true); //--- copy data from the 1st completed bar (index 1, quantity 1) if(CopyBuffer(ma_handle, 0, 1, 1, ma_buf) != 1) return 0.0; if(CopyBuffer(std_handle, 0, 1, 1, std_buf) != 1) return 0.0; if(CopyClose(synth_symbol, PERIOD_M1, 1, 1, close_buf) != 1) return 0.0; double spread_ma = ma_buf[0]; double spread_std = std_buf[0]; double current_spread = close_buf[0]; if(spread_std == 0.0) return 0.0; return (current_spread - spread_ma) / spread_std; } //+------------------------------------------------------------------+ //| trading signal generator | //+------------------------------------------------------------------+ int CheckSpreadSignal(const string synth_symbol) { double z = CalculateSpreadZScore(synth_symbol, 20, 20); if(z>2.0) return -1; // spread overbought -> spread short if(z<-2.0) return 1; // spread oversold -> spread long if(MathAbs(z)<0.5) return 0; // close signal return 0; }
헤지의 방향은 z-score의 부호에 따라 달라집니다. 편차가 양수이면 과매수 된 구간 a를 매도하고 과매도 된 구간 b를 매수합니다. 음수이면 그 반대로 합니다. MarketWatch에서 해당 교차 금리를 사용할 수 있는 경우 테스터가 필요한 마진을 자동으로 계산합니다.
바스켓 지수 및 동적 리밸런싱
스프레드가 두 개의 레그(leg)로 구성된 금융 상품이라면 바스켓은 N개의 심볼로 이루어진 포트폴리오입니다. 가중치 요소는 다음과 같습니다:
- 정적 - 동일 가중치(k = 1/N) 또는 심볼의 시가총액/유동성에 따라 고정;
- 동적 - 역변동성 또는 이동 거래량을 기준으로 재계산.
동적 재조정을 위해서는 k 계수를 주기적으로 재계산하고 CustomSymbolSetDouble()을 통해 심볼 속성을 조정해야 합니다. 현재 버전의 전략 테스터에서는 최적화 시작 시 계약 속성이 고정되어 있으므로 동적 가중치를 변경하는 것은 계약 사양을 변경하지 않으면서 신호를 계산하기 위해 사용자 맞춤형 지표를 사용하거나 틱 기록을 수정하여 구현해야 합니다.
분석적 탈선: 합성의 함정
상관관계가 곧 인과관계를 의미하는 것은 아닙니다 - 자산 간 과거의 연관성은 거시경제적 충격이 발생하는 순간 끊어질 수 있습니다. 수년간 안정세를 유지해 온 EURUSD/GBPUSD 합성 스프레드는 중앙은행 금리나 지정학적 이슈와 관련된 뉴스가 나올 경우 50~80포인트의 급격한 변동을 만들어낼 수 있습니다. 평균 회귀 전략은 자산(심볼)의 구조적 변화를 고려하지 않습니다.
테스터에서 마진과 수익을 계산합니다 - 전략 테스터는 자동으로 교차 금리를 검색하여 마진을 변환하고 결과를 도출합니다. USD 계정에서 SYNTH_EURGBP.custom을 테스트하면 터미널은 다음 순서로 통화쌍을 검색합니다:
- EURUSD.custom / GBPUSD.custom (사용자 맞춤형)
- EURUSD.b / GBPUSD.b (브로커 접미사 포함)
- EURUSD / GBPUSD (기본 통화쌍)
종합 시세에서 해당 종목이 누락된 경우 테스터는 마진 계산 오류를 반환하거나 수익을 0으로 설정합니다.
MQL5 클라우드 네트워크 비활성화 — 클라우드 에이전트를 통한 합성 심볼 최적화는 비활성화됩니다. 서로 다른 기기에서 사용자 맞춤형 심볼을 동일한 이름으로 저장하더라도 히스토리나 정규화 계수가 다를 수 있습니다. 이 결과 비동기화 및 과도한 트래픽이 발생할 수 있습니다. 따라서 테스트는 로컬 환경 또는 로컬 네트워크에서만 가능합니다.
뉴스 필터링 — 통계적 스프레드 차익거래는 비대칭 적인 거시 경제 이벤트(이벤트가 해당 한국가에만 영향을 미치는 경우)에 매우 취약합니다. 주요 경제 지표 발표 30~60분 전에 전략을 비활성화하는 경제 캘린더 필터나 변동성 필터를 추가하는 것이 좋습니다.
워크플로 통합 파이프라인
MQL5 거래 시스템 개발에서 "통합 파이프라인"은 외부 소스를 터미널 트레이딩 인터페이스에 연결하는 데이터 및 이벤트를 처리하는 체인입니다.
다시 말해 이는 원시 데이터(틱)가 여러 처리 단계를 거쳐 최종적으로 사용 가능한 트레이딩 분석 도구나 분석 도구로 변환되는 파이프라인입니다. 실제로 데이터에서 매매에 이르는 파이프라인 구조는 네 가지 순차적인 단계로 구성됩니다:
- 브로커로부터 원시 틱 데이터를 수신하면 SymbolInfoTick() 함수 또는/및 CopyTicksRange() 틱 기록 함수를 사용합니다. 예를 들어 EURUSD와 GBPUSD의 틱 데이터는 서로 다른 서버들로부터 제공됩니다.
- Class-aggregator(CBarAggregator 또는 CSyntheticTickGenerator)는 이러한 틱을 수신하고 시간적으로 동기화한 다음 수학 방정식을 적용합니다. 예를 들어 이를 통해 유로화 - 파운드화 스프레드를 계산합니다.
- 결과는 CustomRatesUpdate() 또는 CustomTicksAdd() 함수를 통해 터미널 데이터베이스에 기록됩니다. 예를 들어 SPREAD_EURGBP.custom 사용자 맞춤형 심볼을 생성하는 경우입니다. 이제 터미널은 이 금융 상품을 일반 통화쌍처럼 처리합니다.
- 생성된 심볼에 대해 플랫폼 표준 도구를 사용하여 전략 테스터에서 EA/지표를 실행합니다.
수정된 히스토리를 통한 스트레스 테스트
표준 백테스팅은 종종 안전하다는 착각을 불러일으킵니다. 완벽한 실행으로 완벽한 과거 데이터에서 EA를 실행하면 자산 곡선의 아름다운 그림을 얻을 수 있습니다. 하지만 실제 시장은 독성 신호가 많고 뉴스 이벤트 발생 시 스프레드가 확대되며 유동성 문제가 심각한 환경입니다.
MetaTrader 5는 여러분 자신이 직접 브로커가 될 수 있는 특별한 기회를 제공합니다. 히스토리를 분석할 뿐만 아니라 의도적으로 매매 조건을 악화시켜 전략의 강점을 시험하는 등 히스토리를 수정할 수도 있습니다. 로봇이 이상적인 데이터로 수익을 내지만 스프레드가 5핍 넓어졌을 때 원금을 잃는다면 그러한 시스템은 살아남지 못합니다.
사용자 맞춤형 심볼을 사용하여 스트레스 시나리오를 만드는 방법을 살펴보겠습니다. 거래 비용을 인위적으로 높이는 것부터 변동성이 높은 기간 동안 브로커의 제한 사항들을 모방하는 것까지 다양한 방법을 시도해 볼 수 있습니다.
스트레스 테스트의 기본 철학은 매우 간단합니다: 실제 시장에 진입하기 전에 시스템의 오류를 미리 발견해 보는 것입니다.
우리는 심볼 기반의 히스토리를 가져와 극단적인 시장 상황을 시뮬레이션 하도록 수정합니다.
수정 시나리오:
- 스프레드 확대를 통해 스캘핑과 데이 트레이딩에 중요한 비용 상승을 시뮬레이션 합니다.
- 손절매/프리즈 레벨 상향 조정 - 손절매 및 이익실현과 관련한 브로커의 제한에 대한 저항력 테스트.
- 마진 요구 조건 변경 - 레버리지 변경 시 마진콜에 대한 유연함 테스트;
- 독성 틱 주입 - 넓은 공백 또는 유동성 중단 추가.
사용자 맞춤형 심볼 방식의 주요 장점은 재현성입니다. 다양한 스트레스 매개변수를 사용하여 동일한 테스트를 실행하고 수익률, 최대 손실률, 회복률과 같은 지표를 표 형식으로 비교할 수 있습니다.
시나리오 1: 인위적인 스프레드 확대
스프레드는 단기 전략의 주요 적입니다. 실제 매매에서는 스프레드가 고정되어 있지 않습니다. 이 수치는 0에서 사실상 무한대 사이를 오르내리며 중요한 거시경제 뉴스 발표 시에는 급격하게 확대될 수 있습니다. 이를 테스트하기 위해 틱 기록을 가져와서 심볼을 복제하고 각 Ask 값에 고정된 핍 수 또는 현재 스프레드의 일정 비율을 더하여 틱을 다시 작성할 수 있습니다.
다음은 고정된 높은 스프레드를 가진 심볼의 스트레스 버전을 생성하는 샘플 스크립트입니다:
//+------------------------------------------------------------------+ //| inputs | //+------------------------------------------------------------------+ input string SourceSymbol = "EURUSD"; // Source symbol input string TargetSymbol = "EURUSD_Stress"; // New symbol name input int StressSpreadPoints = 50; // Fixed spread (in points) input datetime HistoryFromDate = D'2023.01.01'; // Start loading history input bool DeleteOldData = true; // Clear the target symbol's history before writing CiCustomSymbol stressSymb; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { double point; int digits; //--- get the properties of the original symbol for calculations point = SymbolInfoDouble(SourceSymbol, SYMBOL_POINT); digits = (int)SymbolInfoInteger(SourceSymbol, SYMBOL_DIGITS); if(point == 0) { Print("Error: Failed to get point size for ", SourceSymbol); return; } Print("--- Start generating stress symbol ---"); PrintFormat("Source: %s | Target: %s | Spread: %d pp", SourceSymbol, TargetSymbol, StressSpreadPoints); //--- create a custom symbol via a class method //--- return codes: -1 (error), 0 (already exists), 1 (created) int createRes = stressSymb.Create(TargetSymbol, "", SourceSymbol, 1000000, true); if(createRes == -1) { Print("Error creating symbol via CiCustomSymbol.Create()"); return; } stressSymb.Select(true); //--- clone the contract properties (Digits, Point, Mode etc.) if(!stressSymb.Clone(SourceSymbol)) { Print("Error cloning symbol properties"); return; } //--- clear old history if(DeleteOldData) { Print("Clear history using CiCustomSymbol..."); if(stressSymb.TicksDelete(0, LONG_MAX) < 0) Print("Failed to clear ticks: ", GetLastError()); } //--- cyclic loading and modification of ticks (in 1-day batches) datetime current_date = HistoryFromDate; datetime stop_date = TimeCurrent(); if(current_date >= stop_date) { Print("Error: Invalid history start date."); return; } int total_ticks_added = 0; MqlTick ticks[]; //--- calculate the stress spread value double stress_spread_value = StressSpreadPoints * point; Print("Starting batch history processing..."); while(current_date < stop_date && !IsStopped()) { //--- define the boundaries of the day datetime day_start = current_date; datetime day_end = current_date + PeriodSeconds(PERIOD_D1); ulong t1 = (ulong)day_start * 1000; ulong t2 = (ulong)day_end * 1000; //--- read the original symbol ticks int copied = CopyTicksRange(SourceSymbol, ticks, COPY_TICKS_ALL, t1, t2); if(copied > 0) { //--- data stress modification for(int i = 0; i < copied; i++) { //--- set Ask = Bid + Fixed Spread ticks[i].ask = ticks[i].bid + stress_spread_value; //--- mark that both prices have changed ticks[i].flags = TICK_FLAG_BID | TICK_FLAG_ASK; } //--- write to the database via a class int added = stressSymb.TicksReplace(t1, t2, ticks); if(added != copied) { PrintFormat("Error recording ticks for %s via class. Registered: %d of %d", TimeToString(day_start, TIME_DATE), added, copied); } else { total_ticks_added += added; } } //--- move on to the next day current_date = day_end; Sleep(10); } Print("--- Generation complete ---"); PrintFormat("Total ticks processed: %d", total_ticks_added); } //+------------------------------------------------------------------+
이 스크립트는 CiCustomSymbol 래퍼 클래스를 사용하여 사용자 맞춤형 심볼을 처리합니다. 전체 스크립트 코드는 기사에 첨부된 StressTest_SpreadModifier.mq5 파일에 있습니다.
시나리오 2 : 스탑레벨 및 프리즈 레벨
변동성이 높은 시기(예: 미국 비농업 고용지표 발표 전)에는 많은 증권사들이 최소 손절매가 설정되는 거리(스톱 레벨)과 주문 동결 레벨(프리즈 레벨)을 높입니다. 만약 여러분의 전략이 손절매를 5핍으로 설정했는데 브로커가 진입 시 최소 20핍을 요구한다면 서버에서 주문이 거부되거나 손절매 없이 포지션이 유지될 것입니다.
MetaTrader 5에서 이를 확인하는 방법은 무엇일까요? 계약 속성은 CustomSymbolSetInteger() 함수에 의해 지정됩니다. SYMBOL_TRADE_STOPS_LEVEL 값을 일반 값의 5~10배로 설정한 사용자 맞춤형 심볼을 생성할 수 있습니다.
//--- set an extreme stop level (for example, 500 points) long huge_stop_level = 500; if(!CustomSymbolSetInteger(stress_symbol, SYMBOL_TRADE_STOPS_LEVEL, huge_stop_level)) Print("Error setting stop level"); //--- set the freeze level (so that orders cannot be modified close to the market) long huge_freeze_level = 500; if(!CustomSymbolSetInteger(stress_symbol, SYMBOL_TRADE_FREEZE_LEVEL, huge_freeze_level)) Print("Error setting freeze level");
해당 심볼에 대해 테스터를 실행한 후에는 로그를 주의 깊게 살펴보십시오. 전략이 너무 가까이 손절매를 설정하려고 하면 테스터는 "OrderSend 오류 130"(잘못된 손절매)을 반환합니다. 이를 통해 해당 전략이 손절매를 짧게 설정하는 능력에 얼마나 의존하는지 평가할 수 있습니다.
시나리오 3: 마진 요구사항 및 레버리지
마진 요건을 변경하면 사용 가능한 레버리지가 감소할 때 마진콜에 대한 전략의 회복력을 테스트할 수 있습니다. 이는 특히 그리드 전략과 마틴게일 전략에서 맞는데 이들 전략들은 여유 자금이 부족할 경우 예치금을 잃는 경향이 있습니다. SYMBOL_MARGIN_INITIAL(초기 마진) 값을 인위적으로 부풀릴 수 있습니다.
//--- increase the margin requirement 2 times double normal_margin = SymbolInfoDouble(source_symbol, SYMBOL_MARGIN_INITIAL); double stress_margin = normal_margin * 2.0; if(!CustomSymbolSetDouble(stress_symbol, SYMBOL_MARGIN_INITIAL, stress_margin)) Print("Error setting margin");
분석적 탈선: 위험과 잡음을 구분하는 방법
스트레스 테스트 결과를 해석할 때는 극단적으로 하지 않는 것이 중요합니다:
- 부드럽게 하는 것이 좋습니다. 만약 상황이 악화됨에 따라 (스프레드 증가, 손절매 증가 등) 자기자본 곡선이 점진적으로 감소한다면 그 전략에는 안전마진이 있는 것입니다.
- 급격한 변화는 좋지 않은 것입니다. 만약 어떤 전략이 스프레드 20에서 긍정적인 결과를 보이지만 스프레드 21에서는 완전한 손실을 보인다면 이는 좁은 시장 상황에 맞춰 조정(과도하게 최적화)된 이상적인 전략이라고 할 수 있을지도 모릅니다. 그러나 실제 거래 환경에서는 그러한 로봇은 오래 살아남지 못할 것입니다.
데이터 소스
정확한 스트레스 테스트를 위해서는 틱 히스토리의 퀄러티가 중요합니다. 내장된 분봉 차트(M1)는 스프레드가 종가에 포함되어 있거나 별도의 항목으로 표시되지 않기 때문에 스프레드 테스트에 적합하지 않습니다. CopyTicks() 함수와 틱을 기반으로 구축된 사용자 맞춤형 심볼을 사용하세요.
전략 테스트 단계:
- 데이터 준비 - 고품질 틱 데이터(예: Dukascopy)를 다운로드하거나 터미널에 전체 틱 기록이 로드되어 있는지 확인하세요.
- 테스트 환경 구축 - 기초 심볼을 복제하는 스크립트(예: EURUSD → EURUSD_Stress_50)를 사용하고 스프레드, 손절매, 마진 등의 수정자를 적용합니다.
- 일괄 실행 - 전략 테스터 최적화 도구를 사용합니다.
꿀팁:
EA 코드를 변경하는 대신 테스트에 사용할 심볼을 변경하세요. 사용자 맞춤형 심볼 세트에 대한 최적화를 실행합니다: EURUSD_Normal, EURUSD_Spread_30, EURUSD_Spread_50. 결과(순이익, 최대손실률)를 표 또는 데이터베이스에 통합하세요. 모든 시나리오에서 수익성을 유지하는 전략은 실제 계좌에의 적용을 고려해 볼 수 있습니다.
워크플로우 통합: 테스터에서 라이브 차트로
사용자 맞춤형 심볼을 만들어 이를 기반으로 시간 독립적인 차트(렌코, 레인지)를 생성한 다음 테스터에서 전략을 테스트했다고 가정해 보겠습니다. 하지만 여기에 함정이 있습니다: 브로커의 거래 서버는 EURUSD_Renko_10의 존재에 대해 전혀 알지 못합니다.
사용자 맞춤형 심볼을 직접 주문하려고 하면 터미널에서 오류 4756(Unknown Symbol)가 반환됩니다. 사용자 맞춤형 심볼은 클라이언트 터미널에만 존재하는 것입니다. 그렇다면 렌코 차트를 기반으로 매매 결정을 내리는 EA를 어떻게 만들어 실제 EURUSD 거래를 할 수 있을까요?
이 질문에 답하기 위해 주문 라우팅을 이용한 트레이딩 구조를 분석해 보겠습니다. 우리는 트레이딩 쿼리에서 심볼을 대체하는 메커니즘을 개발하여 하나의 상품을 분석하고 다른 상품을 매매할 수 있도록 할 것입니다.
라우팅 문제와 가상 현실
EA를 사용자 맞춤형 심볼 차트에 배치하면 시스템 변수 _Symbol는 해당 맞춤형 상품의 이름(예: XAGUSD_Range_10)을 반환하는데 이는 당연한 결과입니다. 동시에 EA는 일반적으로 다음과 같은 작업을 위해 _Symbol 필드 또는 (동일한) Symbol() 시스템 함수를 사용합니다.
- 주문 보내기;
- 호가 요청(SymbolInfoTick());
- 진입한 포지션 확인 (PositionSelect 등).
실제 시장에서 거래되도록 하려면 이러한 모든 호출을 가로채서 _Symbol을 실제 금융 상품(예: XAGUSD)으로 바꿔야 합니다. 각 EA마다 코드를 수동으로 다시 작성하는 것은 우리의 접근 방식이 아닙니다. 해결책은 간단합니다 — CustomOrder 래퍼 클래스를 사용하면 됩니다. MQL5 API의 핵심적인 함수를 복제하는 클래스를 만들어 보겠습니다. 이 함수 내부에서는 다음과 같은 검사가 수행됩니다: 현재 차트 심볼(사용자 맞춤형)이 요청된 경우 이 요청을 실제 심볼로 대체한다.
EA 소스 코드를 변경하지 않기 위해 전처리 단계에서 표준 호출을 우리가 만든 호출로 대체하는 #define 지시문을 사용하겠습니다.
사용자 맞춤형 심볼 처리를 위한 클래스의 구현은 다음과 같습니다:
//+------------------------------------------------------------------+ //| class for routing orders | //| purpose: replacing a custom symbol with a real one | //+------------------------------------------------------------------+ class CustomOrder { private: static string workSymbol; // real symbol name public: //--- set a replacement symbol static void setReplacementSymbol(const string replacement) { workSymbol = replacement; } //--- send a trade request static bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result) { //--- replace a request symbol if(request.symbol == _Symbol && workSymbol != "") { request.symbol = workSymbol; //--- price adjustment if it is taken from a custom symbol if(request.type == ORDER_TYPE_BUY) request.price = SymbolInfoDouble(workSymbol, SYMBOL_ASK); else if(request.type == ORDER_TYPE_SELL) request.price = SymbolInfoDouble(workSymbol, SYMBOL_BID); } //--- call the original function return ::OrderSend(request, result); } //--- calculate profit static bool OrderCalcProfit(ENUM_ORDER_TYPE action, string symbol, double volume, double price_open, double price_close, double &profit) { if(symbol == _Symbol && workSymbol != "") symbol = workSymbol; return ::OrderCalcProfit(action, symbol, volume, price_open, price_close, profit); } //--- get the position string property static string PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id) { string res = ::PositionGetString(property_id); //--- if a position symbol is requested, return the chart name, not the actual one if(property_id == POSITION_SYMBOL && res == workSymbol) return _Symbol; return res; } //--- get the order string property static string OrderGetString(ENUM_ORDER_PROPERTY_STRING property_id) { string res = ::OrderGetString(property_id); if(property_id == ORDER_SYMBOL && res == workSymbol) return _Symbol; return res; } //--- select position by symbol static bool PositionSelect(string symbol) { if(symbol == _Symbol && workSymbol != "") return ::PositionSelect(workSymbol); return ::PositionSelect(symbol); } }; //+------------------------------------------------------------------+ //| static initialization | //+------------------------------------------------------------------+ string CustomOrder::workSymbol = ""; //+------------------------------------------------------------------+ //| macros for transparent integration | //+------------------------------------------------------------------+ #define OrderSend(request, result) CustomOrder::OrderSend(request, result) #define OrderCalcProfit(action, symbol, volume, open, close, profit) CustomOrder::OrderCalcProfit(action, symbol, volume, open, close, profit) #define PositionGetString(prop) CustomOrder::PositionGetString(prop) #define OrderGetString(prop) CustomOrder::OrderGetString(prop) #define PositionSelect(symbol) CustomOrder::PositionSelect(symbol) //+------------------------------------------------------------------+
구현 특징 중 하나는 매크로가 MetaTrader 5 터미널의 표준 트레이딩 기능을 동일한 매개변수로 대체한다는 것입니다. 전체 코드는 이 글에 첨부된 CustomOrder.mqh 파일에 있습니다.
EA에 샘플 통합하기
MQL5 마법사를 통해 생성된 표준 트렌드팔로워 EA가 있다고 가정해 보겠습니다. 이 EA가 렌코 차트에서 실제 자산을 거래할 수 있게 하려면 다음 세 단계를 수행해야 합니다.
1. 헤더 파일을 포함 — `#include` 지시문이 표준 라이브러리가 포함되기 전에 먼저 와야 합니다. 이렇게 하면 라이브러리 코드가 컴파일되기 전에 매크로가 호출을 대체하게 됩니다.
//+------------------------------------------------------------------+ //| TrendFollower.mq5 | //+------------------------------------------------------------------+ #include <CustomOrder.mqh> #include <Expert\Expert.mqh> #include <Expert\Signal\MySignals\SignalMACD.mqh> //--- other declarations...
2. 속성값 입력 — 어떤 실제 심볼에 주문을 보낼지를 EA에게 알려주는 매개변수를 추가합니다.
input string InpWorkSymbol = "XAGUSD"; // Actual symbol to execute
3. OnInit() 함수에서의 초기화 — 초기화 함수에서는 실제 심볼이 클래스에 전달됩니다. 또한 시각적 테스터를 위해 일종의 속임수를 사용합니다: 차트가 멈추는 것을 방지하기 위해 실제 심볼에 대한 호가 요청을 강제로 수행해야 합니다:
int OnInit() { //--- configure the router if(InpWorkSymbol != "") { CustomOrder::setReplacementSymbol(InpWorkSymbol); //--- we force loading the history of the real symbol, so that the visualization goes smoothly MqlRates rates[1]; CopyRates(InpWorkSymbol, PERIOD_M1, 0, 1, rates); } //--- standard EA initialization... // ... return(INIT_SUCCEEDED); }
이러한 변경 후 EA는 XAGUSD_Renko_10 차트를 분석하지만 모든 주문은 XAGUSD에서 처리되며 거래 내역에는 해당 심볼이 올바르게 표시됩니다.
분석적 탈선: 테스터의 미래 정보 참조편향 제거
실제 데이터로 간단히 테스트할 수 있다면 왜 이 모든 것이 필요할까요? 렌코 섹션을 다시 돌아봅시다. 우리는 사용자 맞춤형 심볼을 테스트할 때 전략 테스터가 바 구성(OHLC)을 기반으로 틱을 생성하는 것을 알았습니다. 렌코 차트에서 이는 이상적인 진입 시점이라는 착각을 불러일으킵니다. EA는 바가 마감되는 것을 보고 즉시 종가로 진입합니다. 하지만 실제로 벽돌이 완성되기까지는 시간이 걸립니다. 가격은 바가 형성되는 범위 내에서 변동할 수 있습니다.
(CustomOrder 클래스를 사용하여) 라우팅을 활성화하면 다음과 같은 현상이 발생합니다:
- EA는 사용자 맞춤형 심볼(예: 렌코 차트에서 이동평균선의 교차)이 있는 차트에서 신호를 감지합니다.
- EA가 매수 또는 매도 요청을 보냅니다.
- CustomOrder 클래스는 심볼을 EURUSD로 대체합니다.
- 거래는 실제 EURUSD의 현재 시장 가격으로 체결됩니다.
그 결과 현실적인 결과를 얻을 수 있습니다. 진입 가격은 슬리피지 또는 데이터 비동기화로 인해 렌코 차트의 가격과 다를 수 있습니다. 사용자 맞춤형 심볼 테스트와 라우팅 테스트의 차이점은 여러분이 트레이딩 가설을 세우는 데 드는 비용입니다.
사용자 맞춤형 심볼을 이용한 실시간 거래를 시작하기 전에 다음 사항을 확인하셔야 합니다:
- 히스토리 동기화 - 터미널에 실제 심볼의 전체 히스토리가 있어야 합니다. 데이터가 없는 경우 CustomOrder는 마진 또는 진입 가격을 잘못 계산할 수 있습니다;
- 코드 4756에 오류 없음 - expert 로그를 확인하세요. Unknown Symbol 오류가 표시되면 매크로가 제대로 작동하지 않은 것입니다(예: CustomOrder.mqh를 다른 라이브러리 다음에 포함시킨 경우).
- 슬리피지 - 래퍼를 통해 거래할 경우 시장 가격으로 거래하게 됩니다. EA 설정에서 허용되는 슬리피지 값을 충분히 높게 설정하여 렌코 신호가 실제 심볼의 틱 업데이트 빈도보다 더 자주 생성되는 빠른 움직임 중에도 주문이 실행되도록 하세요.
분석용으로 사용자 맞춤형 심볼을 사용하는 경우 MQL5 클라우드 네트워크에서 클라우드 최적화를 사용할 수 없습니다. 로컬 네트워크 및/또는 로컬 컴퓨터를 사용하세요.
결론
MetaTrader 5는 조용하지만 근본적인 진화를 거쳤습니다: 터미널은 단순히 호가를 수동적으로 읽어 들이는 기능을 넘어 엔지니어링 실험실로 거듭났습니다. 이제 트레이더들은 브로커가 제공하는 것에 맞춰야 할 필요 없이 특정 아이디어에 최적화된 분석 환경을 스스로 구축할 수 있게 되었습니다.
우리는 렌코 차트, 레인지 차트, 동일 볼륨 차트를 통해 캘린더 그리드로부터 벗어나고, 스프레드와 바스켓을 통해 시장 간 관계를 종합하고, 스트레스 테스트를 위해 의도적으로 과거 데이터를 깨뜨리고, 마지막으로 명확한 주문 라우팅을 통해 가상의 매매를 실제의 매매로 연결하는 방법을 배웠습니다. 각각의 단계는 시스템 개발자가 직면하는 문제들: 차트 주기 노이즈, 부족한 도구들, 완벽한 백테스트란 착각, 차트와 서버 간의 갭을 해결합니다.
사용자 맞춤형 심볼의 가장 큰 장점은 데이터에 대한 제어권입니다. 여러분은 EA의 매개변수 뿐만 아니라 거래 비용, 틱 비동기화, 브로커 제한 사항에 대한 로직 자체의 복원력도 테스트 가능합니다. 고정 스프레드 50핍, 인위적인 갭, 그리고 가변적인 스톱 레벨 조건 시에도 긍정적인 기대값을 유지하는 전략이라면 실제 시장에 적용할 준비가 된 것입니다. 그렇지 않다면 시뮬레이션 단계에서 약점을 발견함으로써 예치금을 지킬 수 있게 되는 것입니다.
작게 시작하세요: 아무 스캘핑 또는 추세 EA나 하나 선택하고 동일 볼륨 바 차트를 생성한 다음 스프레드를 늘린 스트레스 시나리오를 실행하고 CustomOrder를 통해 라우팅을 연결하세요. 지표를 비교하세요.
시간에 따른 잡음이 제거되고 거래 위험을 사전에 계산하면 자산 가치 곡선이 어떻게 변하는지 보면 놀라실지도 모릅니다. MQL5 문서, 코드베이스 및 커뮤니티 포럼은 여러분의 시장을 구축하는 데 있어 가장 중요한 아군입니다.
MetaTrader 5에서 사용자 맞춤형 심볼을 사용하는 방법을 위한 추천 자료:
- 사용자 맞춤형 심볼 문서,
- 사용자 맞춤형 심볼: 실용적인 기초,
- MetaTrader 5에서 사용자 맞춤형 심볼 생성 및 테스트하기
- 사용자 맞춤형 심볼을 사용한 매매 전략 스트레스 테스트,
- MQL5 사용자 맞춤형 심볼: 3D 바 심볼 생성하기.
이글에 첨부된 파일 목록:
| 파일 이름 | 설명 |
|---|---|
| CiCustomSymbol.mqh | CiCustomSymbol 클래스 코드가 포함된 파일 |
| CreateCustomSymbol.mq5 | 사용자 맞춤형 심볼을 생성하기 위한 샘플 스크립트 코드. |
| CBarAggregator.mqh | CBarAggregator 클래스 코드 |
| CustomChartGenerator.mq5 | 세 가지 유형의 과거 및 실시간 차트를 생성하는 지표의 코드가 포함된 파일: 렌코, 레인지, 그리고 동일볼륨 |
| CSyntheticTickGenerator.mqh | CSyntheticTickGenerator 클래스 코드가 포함된 파일 |
| StressTest_SpreadModifier.mq5 | 스프레드가 큰 스트레스 테스트를 위한 사용자 맞춤형 심볼을 생성하는 샘플 스크립트 코드가 포함된 파일 |
| CustomOrder.mqh | CustomOrder 클래스 코드가 포함된 파일 |
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/22391
경고: 이 자료들에 대한 모든 권한은 MetaQuotes(MetaQuotes Ltd.)에 있습니다. 이 자료들의 전부 또는 일부에 대한 복제 및 재출력은 금지됩니다.
새로운 기능: MQL5의 커스텀 인디케이터
MQL5에서의 이벤트 기반 아키텍처: Expert Advisor를 완전한 기능을 갖춘 트레이딩 시스템으로 전환하는 방법
새 MetaTrader 와 MQL5를 소개해드립니다
MQL5에서 CPU에서 GPU로: 연구, 최적화 및 패턴 분석 가속화를 위한 실용적인 OpenCL 프레임워크
정말 흥미로운 기사네요. 다시 한 번 읽어보고 있어요....
혹시 이 기사에 나온 지표를 활용해서 렌코 차트를 성공적으로 만들어 보신 분 계신가요?
제 경우에는 왠지 이 지표가 도저히 작동하지 않네요.
혹시 이 기사에 나온 지표를 이용해 렌코 차트를 만들어 보신 분 계신가요?
제 경우에는 왠지 이 지표가 도무지 작동하지 않네요.
정말 큰 ‘벽돌’을 놓았네요.
정말 거대한 “벽돌”을 설치했네요.
지표의 기본 설정대로 적용했습니다.
감사합니다. 정말로, ‘벽돌’을 좀 더 작게 만들면 차트가 보이네요.