Expert Advisor 개발(파트 7): 가격에 볼륨 추가 (I)
소개
어느 정도의 확신을 갖고 거래를 하고자 하는 사람은 차트에 반드시 이 지표가 있어야 합니다. 대부분의 경우 이 지표는 거래 중 테이프 읽기를 선호하는 사람들이 사용합니다. 또한 지표는 가격의 움직임에 대한 분석을 기반으로만 거래하는 사람들이 활용할 수 있습니다. 이것은 특정 가격 시간에 발생한 거래량을 분석하는 데 사용할 수 있는 매우 유용한 거래량 지표입니다. 그러나 지표를 올바르게 읽는 것은 까다로울 수 있습니다. 이와 관련해서는 기사 끝에 링크를 추가했습니다. 자세히 알아보고 싶으신 분은 링크를 따라가 보세요.
여기에서는 지표의 값을 해석하는 방법에 대해서는 설명하지 않습니다. 이는 이 기사의 범위를 벗어나기 때문입니다. 이 기사의 목적은 MetaTrader 5 플랫폼의 성능을 저하시키지 않는 방식으로 이 지표를 설계하고 생성하는 방법을 알려드리는 것입니다. 흥미로운 사실이 있습니다: 많은 사람들이 이 지표가 실시간으로 업데이트되어야 한다고 생각하지만 실제로는 약간의 지연이 허용됩니다. 그것이 아주 작을 경우에 한해서는 말입니다. 제 경험상 정보의 업데이트가 1초 정도 지연되는 것이 큰 문제가 되는 경우는 본적이 없습니다. 하지만 트루 리얼타임을 사용하는 것이 중요하다면 작은 변경을 해야 합니다. 변경은 지표 자체가 아니라 Expert Advisor가 이 지표를 호출하는 지점에서 이루어져야 실시간 호출이 가능합니다. 그러나 성능에 미치는 영향은 미미하다고 생각합니다. 그러므로 지연은 무시해도 된다고 봅니다.
인터페이스
Volume At Price 클래스 제어 인터페이스는 매우 간단하지만 완전한 제어를 위해서는 지표가 적용될 차트의 정확한 속성을 확인할 필요가 있습니다. 속성은 기본 컨트롤이 강조 표시된 상태로 다음 그림과 같이 표시됩니다.
그리드가 보이지 않으면 다음 애니메이션과 같이 지표의 크기를 조정할 수 없습니다. 인터페이스는 매우 간단하고 직관적입니다. 하나는 크기를 나타내고 다른 하나는 볼륨을 분석하는 시작점을 나타내는 두 개의 컨트롤만 있습니다.
일반적으로 이 지표를 구현 및 구성하는 것은 매우 효과적이고 매우 흥미 롭습니다. 이 글에서는 가장 기초적인 수준으로 작업한 후 다음 글에서 개선해 보도록 하겠습니다.
인터페이스에 대해 더 이상 설명할 것이 없습니다. 코드 구현으로 이동하겠습니다.
구현
지표를 만들 때 가능한 한 적은 작업을 수행하기 위해 우리는 소스 코드를 여러 부분으로 나누고 몇 가지 수정 및 추가 작업을 수행합니다. 우리가 필요로 하는 많은 부분이 이미 작성되어 있으므로 코드를 여러 부분으로 나누는 것부터 시작하겠습니다. 주요 부분은C_Wallpaper 클래스에 있습니다. 우리는 무엇을 하려는 것일까요? 비트맵을 기반으로 지표를 만들까요? 예, 컴퓨터 화면의 모든 이미지는 BITMAP으로 처리되어야 합니다. 그리고 특별한 방식으로 빌드해야 합니다. 따라서 새로운 C_Wallpaper 객체 클래스는 다음과 같이 보일 것입니다:
class C_WallPaper : public C_Canvas { protected: enum eTypeImage {IMAGEM, LOGO, COR}; //+------------------------------------------------------------------+ private : public : //+------------------------------------------------------------------+ ~C_WallPaper() { Destroy(); } //+------------------------------------------------------------------+ bool Init(const string szName, const eTypeImage etype, const char cView = 100) { if (etype == C_WallPaper::COR) return true; if (!Create(szName, 0, 0, Terminal.GetWidth(), Terminal.GetHeight())) return false; if(!LoadBitmap(etype == C_WallPaper::IMAGEM ? "WallPapers\\" + szName : "WallPapers\\Logos\\" + _Symbol, cView)) return false; ObjectSetInteger(Terminal.Get_ID(), szName, OBJPROP_BACK, true); return true; } //+------------------------------------------------------------------+ void Resize(void) { ResizeBitMap(Terminal.GetWidth(), Terminal.GetHeight()); } //+------------------------------------------------------------------+ };
코드가 훨씬 더 간결해졌습니다. C_Wallpaper와 C_VolumeAtPrice 클래스 사이에 공통적인 부분을 제거하고 모든 것을 또 다른 클래스인C_C_Canvas 클래스에 넣었습니다.
그러나 MetaTrader 5 C_Canvas 클래스를 사용하지 않는 이유는 무엇일까요? 이 질문은 실용적이기보다 개인적입니다. 저는 제가 작성하고 개발하는 모든 것을 더 많이 제어하고 싶지만 이것은 C 프로그래머에게 나쁜 습관입니다. 그래서 화면에 객체를 그리기 위한 클래스를 만들어야 합니다. 물론 MetaTrader 5에서 이미 제공하고 있는 클래스를 사용할 수도 있습니다. 이제 이 기사의 주요 주제인 C_VolumeAtPrice 클래스에 초점을 맞춰 보겠습니다. 클래스에는 다음 표에 있는 7개의 기능이 있습니다.
기능 | 설명 | 액세스 유형 |
---|---|---|
Init | 사용자 지정 값으로 클래스를 초기화. | General |
Update | 지정된 간격으로 거래량 데이터를 업데이트. | General |
Resize | 차트에서 가격 이미지의 크기를 변경하여 일부 세부 정보를 더 쉽게 분석할 수 있습니다. | General |
DispatchMessage | 객체 클래스에 메시지를 보내는 데 사용됩니다. | General |
FromNowOn | 시스템 변수 초기화 | Private |
SetMatrix | 볼륨 데이터로 매트릭스 생성 및 유지 | Private |
Redraw | 볼륨 이미지 생성 | Private |
이제 아래 코드에서 변수 선언부터 시작하여 시스템 구현을 해보겠습니다:
#define def_SizeMaxBuff 4096 //+------------------------------------------------------------------+ #define def_MsgLineLimit "Starting point from Volume At Price" //+------------------------------------------------------------------+ class C_VolumeAtPrice : private C_Canvas { #ifdef macroSetInteger ERROR ... #endif #define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B) private : uint m_WidthMax, m_WidthPos; bool m_bChartShift, m_bUsing; double m_dChartShift; struct st00 { ulong nVolBuy, nVolSell, nVolTotal; long nVolDif; }m_InfoAllVaP[def_SizeMaxBuff]; struct st01 { ulong memTimeTick; datetime StartTime, CurrentTime; int CountInfos; ulong MaxVolume; color ColorSell, ColorBuy, ColorBars; int Transparency; string szObjEvent; double FirstPrice; }m_Infos;
이 코드에서 강조 표시된 부분은 주의해야 할 부분입니다. 이 부분은 다른 파일에서 정의를 가져올 때 해당 파일에서 사용할 정의와 충돌하지 않도록 합니다. 실제로 MQL5 컴파일러는 기존의 정의를 재정의하려고 할 때 경고를 표시하며 경우에 따라 해결 방법을 파악하기 어렵습니다. 따라서 좀 더 편하게 개발하기 위해 위의 코드에서 강조 표시된 테스트를 사용합니다. 이 코드의 나머지 부분은 특별히 흥미로운 것이 없습니다. 주의해야 할 유일한 것은 def_SizeMaxBuff 정의입니다. 이는 볼륨 데이터 배열의 크기를 나타냅니다. 필요한 경우 이 값을 다른 값으로 변경할 수 있지만 테스트 결과에 따르면 이 값은 대부분의 경우에 문제없습니다. 이 정의는 낮은 가격과 가격 사이의 틱 변동을 나타내므로 현재 값은 다양한 경우를 처리할 수 있습니다.
초기화 함수: 모든 것이 시작되는 곳
모든 변수를 올바르게 초기화하는 것이 이 함수입니다. Expert Advisor에서 다음과 같이 호출됩니다:
//.... Initial data.... input color user10 = clrForestGreen; //Take Profit line color input color user11 = clrFireBrick; //Stop line color input bool user12 = true; //Day Trade? input group "Volume At Price" input color user15 = clrBlack; //Color of bars input char user16 = 20; //Transparency (from 0 to 100 ) //+------------------------------------------------------------------+ C_SubWindow SubWin; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { Terminal.Init(); WallPaper.Init(user03, user05, user04); if ((user01 == "") && (user02 == "")) SubWin.Close(); else if (SubWin.Init()) { SubWin.ClearTemplateChart(); SubWin.AddThese(C_TemplateChart::SYMBOL, user02); SubWin.AddThese(C_TemplateChart::INDICATOR, user01); } SubWin.InitilizeChartTrade(user06, user07, user08, user09, user10, user11, user12); VolumeAtPrice.Init(user10, user11, user15, user16); // ... Rest of the code
여기에는 많은 매개변수가 없으며 주로 지표가 사용할 색상에 대한 정보를 나타냅니다. 다음으로 이 함수의 내부 코드를 살펴보겠습니다. 아래 코드는 모든 것이 어떻게 초기화되는지를 보여줍니다.
void Init(color CorBuy, color CorSell, color CorBar, char cView) { m_Infos.FirstPrice = Terminal.GetRatesLastDay().open; FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal())); m_Infos.Transparency = (int)(255 * macroTransparency(cView)); m_Infos.ColorBars = CorBar; m_Infos.ColorBuy = CorBuy; m_Infos.ColorSell = CorSell; if (m_bUsing) return; m_Infos.szObjEvent = "Event" + (string)ObjectsTotal(Terminal.Get_ID(), -1, OBJ_EVENT); CreateObjEvent(); m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1); Create("VaP" + (string)MathRand(), 0, 0, 1, 1); Resize(); m_bUsing = true; };
보시다시피 여기에서는 모든 것이 매우 간단합니다. 그래도 코드를 흥미롭게 만드는 몇 가지 기능이 있습니다. 그 중 하나는Terminal.GetRatesLastDay().open입니다. 이것이 이상하게 보일 수 있지만 실제로는 객체 지향 프로그래밍(OOP)의 원칙을 따를 때 매우 일반적인 상태입니다. 이러한 원칙 중 하나는 클래스 외부의 어떤 것도 클래스의 내부 변수에 액세스할 수 없어야 한다는 것입니다. 그러나 클래스 내부의 변수 값을 가져오는 방법은 무엇일까요? 올바른 방법은 OOP에만 나타나는 형식을 사용하는 것이므로 C_Terminal 클래스 내부에서 GetRatesLastDay 함수가 어떻게 선언되는지 살펴보겠습니다. 이것은 아래 코드에서 볼 수 있습니다:
inline MqlRates GetRatesLastDay(void) const { return m_Infos.Rates; }
실제로 어떻게 작동하는지 봅시다. 예약어 inline부터 시작하겠습니다. 이는 코드가 나타나는 모든 위치에 코드를 배치해야 함을 컴파일러에 지시합니다. 컴파일러는 함수 호출을 생성하는 대신 실제로 함수의 모든 코드를 함수가 참조되는 지점으로 복사합니다. 이것은 메모리 소비를 적게 하여 코드 실행 속도를 높입니다. 그러나 특정의 경우에 실제로 발생하는 것은m_Infos.Rates변수가 참조된다는 것입니다. 이 변수에는 MqlRates유형이 있습니다. 즉 MqlRates 구조의 값에 액세스할 수 있습니다. 이 경우 변수 참조의 주소를 전달하지 않습니다. 그러나 어떤 경우에는 코드를 더 빠르게 만들기 위해 참조 주소를 전달합니다. 이 경우 클래스 내부의 변수 값을 변경할 수 있으나 이는 금지되어야 합니다. 이러한 일이 발생하지 않도록 예약어const를 사용하여 클래스 자체 없이는 변수가 절대 변경되지 않음을 보장해야 합니다. C++의 많은 예약어가 MQL5에 문서화된 형태로 존재합니다. 그 중 일부는 아직 문서화되지 않은 것도 있지만 C++에 매우 가깝기 때문에 MQL5의 일부입니다. 기사의 마지막에 C++에 대해 더 배우고 MQL5 프로그래밍에 사용하려는 사람들을 위한 링크를 추가할 것입니다.
이제 Init 함수 코드 안에 흥미로운 부분이 있습니다. 이 부분이 무엇을 하는지를 설명하기 위해 아래에 강조 표시했습니다.
m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);
EA가 시작되면 EA는 차트를 변경하지만 사용자는 시스템을 종료할 경우 시스템을 초기 상태로 재설정하는 것이 좋습니다. 차트 스크롤 설정을 저장한 다음 최소 스크롤을 만듭니다. 강조 표시된 부분이 이를 수행하므로 치수를 조정하려면 차트에서 그리드를 확인해야 합니다. 이 작업은 기사 시작 부분에 표시된 대로 대화식으로 수행됩니다. 자세한 내용은CHART_SHIFT를 참조하세요.
화면의 객체 고정
클래스의 내부 기능은 매우 간단하지만 특히 주의해야 할 몇 가지 사항이 있습니다. 첫 번째는 사용자가 볼륨 분석의 시작을 나타내는 점을 제거하는 것을 허용하지 않는 보안 시스템입니다.
점은 매우 작기 때문에 실제로 확인하려면 주의해야 합니다.
중요 참고사항: 분석 포인트를 변경하고 싶다면 차트의 차트 주기에 주목하세요. 예를 들어 분석을 9:00에서 9:02로 이동해야 하는 경우 1분 또는 2분의 차트 주기를 사용해야 합니다. 만약 5분 차트를 사용하는 경우에는 이를 수행할 수 없습니다.
또한 사용자가 실수로 이 요소를 삭제하지 않도록 주의해야 합니다. 이는 다음 코드에서 수행됩니다.
void DispatchMessage(int iMsg, string sparam) { switch (iMsg) { // ... The inside of the code case CHARTEVENT_OBJECT_DELETE: if ((sparam == m_Infos.szObjEvent) && (m_bUsing)) { m_bUsing = false; CreateObjEvent(); Resize(); m_bUsing = true; } break; } };
만약 객체가 삭제되었다는 것을 클래스가 인식하면 즉시 해당 객체를 재생성하여 사용자가 클래스에 필요한 객체 없이 남겨지지 않도록 하고 따라서 강제로 EA를 다시 시작하지 않도록 합니다. 사용자가 민감한 객체를 삭제하지 않도록 해야 할 때마다 코드에 있는 모델을 사용합니다. 하지만 EA가 이벤트를 인지할 수 있도록 추가 코드를 추가해야 합니다.
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
이 간단한 행은 MetaTrader 5가 객체가 삭제된 것을 보고하도록 합니다. 자세한 내용은CHART_EVENT_OBJECT_DELETE를 참조하세요.
가격 차트에 거래량 나타내기
이것은 클래스의 핵심이며 세 가지 기능이 있습니다. 하나는 공개이고 두 개는 비공개입니다. public 함수부터 시작해 보겠습니다. 다음과 같습니다.
inline virtual void Update(void) { MqlTick Tick[]; int i1, p1; if (m_bUsing == false) return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0) { if (m_Infos.CountInfos == 0) { macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time)); m_Infos.FirstPrice = Tick[0].last; } for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++); for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]); if (p1 == i1) return; m_Infos.memTimeTick = Tick[i1 - 1].time_msc; m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time); Redraw(); }; };
강조 표시된 라인은 시스템에 매우 중요합니다. 시스템이 작동하기 시작하면 시스템은 정확히 어디서부터 시작해야 할지 모릅니다. 이 라인은 이러한 포인트를 업데이트하고 분석이 시작된 위치와 시작 가격이 어디였는지를 사용자에게 알려 시스템이 내부 테이블을 생성할 수 있도록 합니다. 시스템은 항상 새로운 틱이 도착하기를 기다립니다. 우리에게는 그런 일이 발생할 경우 이를 화면에 표시하기 위해 구문을 분석하고 수집할 데이터가 있습니다. 함수는 다음과 같습니다:
inline void SetMatrix(MqlTick &tick) { int pos; if ((tick.last == 0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) return; pos = (int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * 2; pos = (pos >= 0 ? pos : (pos * -1) - 1); if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume; m_InfoAllVaP[pos].nVolDif = (long)(m_InfoAllVaP[pos].nVolBuy - m_InfoAllVaP[pos].nVolSell); m_InfoAllVaP[pos].nVolTotal = m_InfoAllVaP[pos].nVolBuy + m_InfoAllVaP[pos].nVolSell; m_Infos.MaxVolume = (m_Infos.MaxVolume > m_InfoAllVaP[pos].nVolTotal ? m_Infos.MaxVolume : m_InfoAllVaP[pos].nVolTotal); m_Infos.CountInfos = (m_Infos.CountInfos == 0 ? 1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos)); }
아마도 이 함수는 가격의 볼륨 값만 저장하고 유지하기 때문에 그렇게 중요하지 않을 지도 모르나 이 함수에서 강조 표시된 라인은 시스템의 핵심입니다. 이 두 줄에서 무슨 일이 일어나고 있는지 이해하기 위해 조금 더 생각해 봅시다. 다음을 고려하십시오: 무엇이 더 빠를까요 - 각 가격을 저장하고 각각의 볼륨을 기록하는 것과 볼륨만 저장하고 가격이 얼마인지는 모르는것? 두 번째 옵션이 더 빠릅니다. 그러므로 볼륨을 먼저 저장하고 가격은 어디에 있는지를 알아보겠습니다. 그러나 시스템의 첫 번째 가격은 얼마일까요? 예 초기값이 필요합니다. 초기의 값이 없을 경우 모든 것이 무너질 것입니다. 첫 번째 거래된 틱의 가격을 사용하는 것은 어떨까요? 예 훌륭합니다. 완벽합니다. 그러나 문제가 있습니다: 만약 가격이 오르면 그건 좋습니다. 모든 데이터를 배열에 쉽게 저장할 수 있습니다. 하지만 떨어지면 어떻게 될까요? 이 경우 우리는 음수의 값을 갖게 되며 우리는 음수 인덱스가 있는 배열에 액세스할 수 없습니다. 하나 대신 두 개의 배열을 사용할 수 있지만 이는 불필요한 로드로 이어질 것입니다. 간단한 해결책이 있습니다. 아래 표를 살펴봅시다:
인덱스가 양수이면 걱정할 필요가 없지만 음수일 경우 우리는 양방향 배열을 사용하기 때문에 문제가 발생합니다. 0 값은 첫 번째 틱의 가격을 나타내고 음수 값은 감소한 값이고 증가된 값이 양수 값입니다. 다음: 두 방향이 있는 경우 인덱스에 2를 곱하여 중간 열을 얻습니다. 그러나 도움이 되지 않는 것 같습니다. 하지만 음수 값을 양수로 변환하고 1을 빼면 올바른 열을 얻습니다. 자세히 살펴보면 값이 이 오른쪽 열에 곱해져 있음을 알 수 있습니다. 이는 배열에 액세스할 수 있는 완벽한 인덱스를 제공하여 우리가 알고 있는 배열에 액세스할 수 있지만 이 배열이 얼마나 커질지는 모릅니다. 그리고 이것이 바로 두 개의 강조 표시된 라인이 하는 일입니다: 시작 가격보다 높은 값과 낮은 값을 번갈아 가며 배열에 대한 인덱스를 생성합니다. 이것은 매우 좋은 솔루션이지만 화면에 데이터를 표시할 수 없으면 아무 소용이 없습니다. 다음 함수가 그와 같은 일을 합니다.
void Redraw(void) { uint x, y, y1, p; double reason = (double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * 1.0)) : 1.0); double desl = Terminal.GetPointPerTick() / 2.0; Erase(); p = m_WidthMax - 8; for (int c0 = 0; c0 <= m_Infos.CountInfos; c0++) { if (m_InfoAllVaP[c0].nVolTotal == 0) continue; ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & 1) == 1 ? -(c0 + 1) : c0) / 2)) + desl, x, y); y1 = y + Terminal.GetHeightBar(); FillRectangle(p + 2, y, p + 8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > 0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency)); FillRectangle((int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency)); } C_Canvas::Update(); };
이 함수에 의해 볼륨 차트가 나타나게 되고 강조 표시된 부분은 볼륨 캡처 중에 만들어진 계산을 반전시킵니다. 올바른 지점에 디스플레이를 표시하기 위해 가격이 약간 이동하여 바가 올바르게 배치됩니다. 나머지 함수는 루틴을 그리는 것입니다. 여기에 약간의 설명이 필요합니다. 두 개의FillRectangle호출이 있습니다. 왜일까요? 첫 번째 호출은 매수자 혹은 매도자 중 어느 쪽 볼륨이 더 큰지를 나타내고 두 번째 호출은 실제로 볼륨을 표시합니다. 그러나 매수자와 매도자로 볼륨 밴드를 나누어 함께 구축하지 않는 이유는 무엇일까요? 그 이유는 한 가격대의 거래량이 증가함에 따라 이것이 다른 더 작은 가격대의 분석을 방해하기 시작하기 때문입니다. 그러면 매도와 매수 중 어느 쪽이 더 큰지 판단하기 어려워집니다. 그러나 이런 식으로 배치하면 이 문제가 사라지고 데이터를 읽기가 더 쉬워지고 이해하기도 쉬워집니다. 결과적으로 차트는 아래 그림과 같습니다:
다른 모든 클래스 함수는 앞에서 설명한 함수를 지원하는 역할을 하므로 자세히 다루어야 할 필요는 없어 보입니다.
결론
여기에서는 매우 간단한 Volume at Price를 제시했지만 이는 매우 효과적인 도구입니다. 코딩을 배우기 시작하고 객체 지향 프로그래밍(OOP)에 집중하고 싶다면 이 코드를 주의 깊게 공부해야 합니다. 왜냐하면 모든 코드가 100% 객체 지향 접근 방식을 기반으로 되어 있고 매우 유용한 개념도 있기 때문입니다.
응용 프로그램에는 현재 개발한 단계까지의 Expert Advisor가 포함되어 있습니다.
유용한 링크
MetaQuotes 소프트웨어 사를 통해 포르투갈어가 번역됨
원본 기고글: https://www.mql5.com/pt/articles/10302