English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
preview
Expert Advisor 개발 기초부터(24부): 시스템 견고성(I)

Expert Advisor 개발 기초부터(24부): 시스템 견고성(I)

MetaTrader 5트레이딩 | 30 1월 2024, 17:10
158 0
Daniel Jose
Daniel Jose

소개

어떤 사람들은 간단하다고 생각할지 모르지만 여러가지가 그렇게 간단하지는 않습니다. 주문 시스템도 그중 하나입니다. 많은 사람들에게 유용하지만 어떤 사람들에게는 충분하지 않은 기본 시스템이었던 트레이딩 EA 처음부터 개발하기 기사에서 처럼 완벽하게 잘 작동하는 일반적인 시스템을 만들 수도 있습니다. 이제 모든 것이 바뀌기 시작한 순간이 왔습니다. 새로운 주문 시스템에 대한 이 시리즈의 첫 번째 부분에 우리는 이르렀습니다. 이는 트레이딩 EA 처음부터 개발하기(18부) 문서에서 확인할 수 있습니다. 바로 이 지점에서 우리는 MetaTrader 5의 지원을 받으면서 EA에 의해 관리되는 시스템을 개발하기 시작했습니다. 이 시스템의 아이디어는 차트에서 주문에 제한을 두지 않는 것이었습니다. 저 같은 경우 처음에는 이 시스템이 다소 대담해 보였고, EA가 아닌 MetaTrader 5에서 객체를 유지 관리하는 시스템을 만든다는 사실 자체가 무의미하고 비효율적으로 보였습니다.

그러나 시스템은 개발 중이었고 우리는 트레이딩 EA 처음부터 개발하기(23부) 기사에서 주문, 포지션 또는 손절매 수준(이익실현 및 손절매)을 쉽게 관리할 수 있는 고스트 시스템을 개발했습니다. 개발은 매우 흥미로웠지만 문제가 있었습니다. MetaTrader 5에서 지원하는 객체 수와 비교하여 사용 및 표시되는 객체의 수를 보면 지원되는 객체의 수가 항상 더 많기 때문에 여러분은 확실히 놀라게 될 것입니다.

대부분의 경우 문제는 그렇게 심각하지 않습니다. 하지만 시장 변동성이 큰 시기에는 시스템을 안정적이지 못하게 만든 두 가지 문제가 있었습니다. 일부 상황에서는 이 두가지 문제가 사용자가 잘못된 행동을 하게 만들기도 했습니다. 그 원인은 트레이더가 펜딩 주문을 추가하면 시스템이 이 주문을 서버로 전송하고 서버가 응답하는 데 평소보다 시간이 더 필요하기 때문입니다. 그래서 시스템은 어떤 순간에는 주문이 있다고 표시하고 다른 순간에는 주문이 없다고 표시합니다. 그리고 이러한 표시가 포지션에서 이뤄진 경우 (주문과 포지션의 차이점은 문서를 참조하세요). 우리는 과연 서버가 예상대로 명령을 실행했는지 알 수 없었기 때문에 훨씬 더 번거로웠습니다.

이 문제를 해결하는 방법에는 여러 가지가 있습니다. 일부는 더 간단하고 일부는 더 복잡합니다. 어쨌든 우리는 EA를 신뢰해야 하며 그렇지 않으면 우리는 어떤 상황에서도 EA를 사용해서는 안 됩니다.


1.0. 계획

여기서 가장 큰 문제는 속도와 안정성이라는 두 가지 특성을 모두 갖춘 시스템을 설계하는 것입니다. 어떤 종류의 시스템에서는 이 두 가지를 모두 달성하는 것이 매우 어렵거나 불가능하기도 합니다. 그래서 많은 경우 우리는 균형을 맞추려고 노력합니다. 하지만 돈이 문제입니다. 이는 돈이 걸린 문제이기 때문에 우리는 굳이 이러한 요건을 갖추지 못한 시스템을 도입하여 위험을 감수하고 싶지 않은 것입니다. 우리는 우리가 실시간으로 작동하는 시스템을 다루고 있다는 점을 기억해야 합니다. 이는 개발자가 빠져 들게 되는 가장 어려운 시나리오입니다. 개발자는 항상 이벤트에 즉각적으로 반응하고 개선하려고 할 때 무너지지 않을 정도의 안정성을 보여 주면서 매우 빠른 시스템을 갖기 위해 노력해야 하기 때문입니다. 이렇게 이 작업이 매우 어렵다는 것이 분명합니다.

함수가 가장 적절한 방식으로 호출되고 실행되도록 하여 불필요한 시간에 불필요한 호출을 피함으로써 우리는 속도를 향상시킬 수 있습니다. 이렇게 하면 해당 언어 내에서 최대한 빠른 시스템이 되도록 할 수 있습니다. 하지만 우리가 더 빠른 시스템을 원한다면 기계어 수준으로 내려가야 하는데 이 경우 어셈블리를 의미합니다. 그러나 이것은 종종 불필요하며 우리는 C 언어를 사용하여 똑같이 좋은 결과를 얻을 수 있습니다.

이러한 견고성을 달성하는 방법 중 하나는 코드를 가능한 한 많이 재사용하여 다양한 경우에 지속적으로 테스트하는 것입니다. 하지만 이것은 오직 한 가지 방법 뿐입니다. 또 다른 방법은 OOP(객체 지향 프로그래밍)를 사용하는 것입니다. 상속의 경우를 제외하고 각 객체 클래스가 객체 클래스 데이터를 직접 조작하지 않도록 바른 방법으로 적절하게 수행하여 매우 강력한 시스템을 갖추게 되면 그것으로 충분합니다. 이로 인해 때때로 실행 속도가 감소하기도 하지만 클래스에 의해 제공되는 캡슐화에 의해 생성되는 기하급수적인 증가로 인해 이 감소는 무시할 수 있을 정도로 매우 작습니다. 이 캡슐화는 우리가 필요로 하는 견고함을 제공합니다.

보시다시피 속도와 견고성을 모두 달성하는 것은 그렇게 간단하지 않습니다. 하지만 좋은 점은 언뜻 생각할 수 있는 것처럼 많은 것을 희생할 필요가 없다는 것입니다. 우리는 시스템 문서를 확인하고 개선하기 위해 변경할 수 있는 사항을 간단히 확인할 수 있습니다. 우리가 바퀴를 재발명 하려고 하지 않는다는 단순한 사실만으로도 이는 이미 좋은 시작입니다. 하지만 프로그램과 시스템은 지속적으로 개선되고 있다는 점을 기억하세요. 따라서 우리는 항상 사용 가능한 것을 최대한 활용하려고 노력한 다음 마지막 경우에만 정말로 바퀴를 재발명해야 합니다.

이 글에서 변경된 내용을 설명하는 것이 불필요하다고 생각하시거나 제가 코드를 실제로 옮기지 않고 많이 바꾼다고 생각하는 분들이 계실까 봐 설명해 드리겠습니다: 코딩을 할 때는 최종 코드가 어떻게 작동할지 전혀 알 수 없습니다. 우리에게는 달성해야 할 목표만 있을 뿐입니다. 목표를 달성한 후에는 우리는 목표를 어떻게 달성했는지 살펴보고 더 나은 목표를 위해 개선하기 위해 노력합니다.

실행 파일이든 라이브러리든 상용 시스템의 경우 변경 사항을 적용하여 업데이트로 릴리스 합니다. 상용 시스템이기 때문에 사용자는 목표에 도달하기 위해서 필요한 관련된 경로를 실제로 알 필요가 없습니다. 모르는 것이 좋습니다. 하지만 저는 시스템이 개방형 시스템이기 때문에 처음부터 매우 효율적인 시스템을 개발할 수 있다고 여러분들이 생각하게 만들고 싶지 않습니다. 프로그래머나 개발자가 사용 언어에 대한 지식이 아무리 많아도 시간이 지남에 따라 시스템에는 개선할 수 있는 부분이 항상 존재하기 때문에 이런 식으로 생각하는 것은 적절하지 않으며 심지어 모욕에 가깝습니다.

이 순서를 서너 개의 글로 요약할 수 있는 것으로 생각하지 마세요. 만약 그렇다면 차라리 제가 가장 적절하다고 생각한 방식으로 코드를 만들어서 상업적으로 공개하는 것이 더 나을 것이기 때문입니다. 그것은 저의 의도가 아닙니다. 저는 경험이 많은 다른 프로그래머의 코드를 보면서 프로그래밍을 배웠고 이것이 얼마나 중요한지 잘 알고 있습니다. 단순히 완성된 솔루션을 가지고 시스템이 어떻게 작동하는지 이해하는 것보다 시간이 지남에 따라 어떻게 발전하는지 아는 것이 훨씬 더 중요합니다.

이러한 관찰이 끝나면 우리는 개발로 넘어갈 것입니다.


2.0. 구현

2.0.1. 새로운 포지션 지표 모델링

새로운 코드 형식에서 가장 먼저 주목해야 할 점은 매크로가 된 함수가 변경되었다는 점입니다.

inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev, bool isGhost = false)
{
        return StringFormat("%s%c%c%c%llu%c%c%c%s", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)(isGhost ? ev + 32 : ev), def_SeparatorInfo, (isGhost ? def_IndicatorGhost : def_IndicatorReal));
}

컴파일러가 'inline'이라는 예약어 덕분에 참조되는 모든 지점에서 이 코드를 사용하더라도 여러분은 이 함수가 코드에서 여러 번 호출되기 때문에 당연한 것으로 간주해서는 안 됩니다. 실제로 최대한 빠르게 실행되도록 해야 하므로 새 코드는 다음과 같습니다:

#define macroMountName(ticket, it, ev, Ghost) 								 \
		StringFormat("%s%c%llu%c%c%c%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo,          \                                                                                                                                                                                                                                                                                                
                                                       ticket, def_SeparatorInfo,                        \                                                                                                                                                                                                                                        
                                                       (char)it, def_SeparatorInfo,                      \ 
                                                       (char)(Ghost ? ev + 32 : ev), def_SeparatorInfo,  \ 
                                                       (Ghost ? def_IndicatorGhost : def_IndicatorReal))

이전 버전의 매크로의 데이터와 이 버전의 데이터가 다르다는 점에 유의하세요. 이러한 변경에는 이유가 있으며 이 글의 뒷부분에서 자세히 설명하겠습니다.

하지만 이 수정으로 인해 다른 함수의 코드도 약간 변경해야 합니다.

inline bool GetIndicatorInfos(const string sparam, ulong &ticket, eIndicatorTrade &it, eEventType &ev)
                        {
                                string szRet[];
                                char szInfo[];
                                
                                if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false;
                                if (szRet[0] != def_NameObjectsTrade) return false;
                                ticket = (ulong) StringToInteger(szRet[1]);
                                StringToCharArray(szRet[2], szInfo);
                                it = (eIndicatorTrade)szInfo[0];
                                StringToCharArray(szRet[3], szInfo);
                                ev = (eEventType)szInfo[0];

                                return true;
                        }

여기서 변경된 것은 인덱스에만 적용되었으며 이 인덱스는 무엇이 티켓이고 무엇이 지표인지를 나타내는 데 사용됩니다. 복잡하지 않습니다. 그렇지 않으면 이 함수를 사용할 때 우리는 일관성 없는 데이터를 갖게 됩니다.

궁금하실 수도 있습니다: "이러한 변화가 필요한 이유는 무엇일까? 시스템이 완벽하게 작동하지 않았나?". 네, 그렇습니다. 하지만 여기에는 우리가 통제할 수 없는 것들이 있습니다. 예를 들어 MetaTrader 5 개발자가 EA에서 사용되지 않아서 도움이 되지 않는 일부 함수를 개선한 경우입니다. 규칙은 휠을 다시 발명하지 않고 대신에 사용 가능한 리소스를 사용하는 것입니다. 따라서 우리는 항상 언어가 제공하는 함수(이 경우 MQL5)를 사용하고 자체 함수를 만들지 않도록 노력해야 합니다. 이것은 터무니없는 것처럼 보일 수 있지만 실제로 잠시 멈추고 생각해보면 플랫폼이 때때로 일부 함수들을 개선하고 이러한 동일한 함수를 사용하는 경우에는 추가적인 노력 없이도 프로그램의 성능이 향상되고 보안이 강화된다는 것을 알 수 있습니다.

따라서 목적이 수단을 정당화합니다. 하지만 위의 변경 사항으로 인해 EA가 MQL5 라이브러리의 개선 사항을 활용할 수 있을까요? 이 질문에 대한 답은 ' 아니요' 입니다. 위의 변경 사항은 객체 이름 모델링이 올바른지 확인하여 향후 MQL5 및 MetaTrader 5 개발자가 제공할 수 있는 개선 사항을 효과적으로 사용할 수 있도록 하기 위해 필요합니다. 다음은 여러 유용한 것들 중 하나입니다:

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
                ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));
        else ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)it));
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        m_InfoSelection.bIsMovingSelect = false;
        ChartRedraw();
}

기억이 나지 않거나 처음 접하는 분들을 위해 동일한 코드의 이전 버전이 아래에 나와 있습니다. 코드는 다음과 같습니다:

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
#define macroDestroy(A, B)      {                                                                               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND, B));                            \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE, B));                              \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE, B));                             \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT, B));                              \
                if (A != IT_RESULT)     ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE, B));      \
                else ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_PROFIT, B));                       \
                                }

        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        {
                macroDestroy(IT_RESULT, true);
                macroDestroy(IT_RESULT, false);
                macroDestroy(IT_PENDING, true);
                macroDestroy(IT_PENDING, false);
                macroDestroy(IT_TAKE, true);
                macroDestroy(IT_TAKE, false);
                macroDestroy(IT_STOP, true);
                macroDestroy(IT_STOP, false);
        } else
        {
                macroDestroy(it, true);
                macroDestroy(it, false);
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
#undef macroDestroy
}

코드가 더 간결해진 것처럼 보일 수 있습니다. 하지만 그 뿐만이 아닙니다. 코드의 감소는 당연한 일이지만 그 진면목은 훨씬 더 깊습니다. 기존의 코드는 플랫폼 리소스를 더 잘 사용하는 새로운 코드로 대체되었습니다. 하지만 이전에 사용했던 객체 이름 모델에서는 이러한 개선이 불가능했기 때문에 우리는 MQL5 함수의 이점을 기대할 수 있도록 모델링을 변경했습니다. 어떤 이유로든 이 함수가 개선되는 경우 EA의 구조를 변경할 필요 없이 EA는 이 수정 사항에 따른 혜택을 받을 수 있습니다. 저는 ObjectsDeleteAll 함수에 대해 이야기하고 있습니다. 올바르게 사용하면 MetaTrader 5가 정리를 수행합니다. 너무 많은 세부 사항을 지정할 필요 없이 객체 이름만 지정하면 나머지는 MetaTrader 5가 알아서 처리합니다. 이 함수가 사용되는 지점은 새 코드에서 강조 표시되어 있습니다. 사용할 접두 코드를 알리기 위해 모델링을 어떻게 수행했는지 확인하세요. 객체 이름 모델링이 수정되기 전에는 불가능한 것이었습니다.

아래에 강조 표시된 새로운 코드 조각의 한 가지 세부 사항에 주목해 주시기 바랍니다.

if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));

강조 표시된 부분을 제가 왜 추가했을까요?

시스템에서 값이 1로 시작하는 티켓을 만들면 펜딩 중인 주문이 접수되는 즉시 모든 객체가 화면에서 제거되기 때문입니다. 명확하지 않나요? 펜딩 주문에 사용되는 입력 값은 1입니다. 즉, 0은 EA에서 다른 테스트를 수행하는 데 사용되었던 것입니다. 지표 0은 실제로 0이 아닌 1의 값을 갖습니다. 따라서 초기 값은 1입니다. 이제 문제가 생겼습니다: 거래 시스템이 티켓을 생성한다고 가정해 보겠습니다. 1221766803. 그러면 이 티켓을 나타내는 객체는 접두사로 다음과 같은 값을 갖게 됩니다: SMD_OT#1221766803. EA가 ObjectsDeleteAll 함수를 실행하여 지표 0을 삭제하면 객체의 이름은 SMD_OT#1 이 되고 새로 생성된 시스템을 포함하여 이 값으로 시작하는 모든 객체가 삭제됩니다. 이 문제를 해결하기 위해 이름 끝에 추가 문자를 추가하는 방식으로 우리가 지표 0을 삭제하는지 아니면 또다른 지표를 삭제하는지를 함수가 알 수 있도록 이름을 약간 조정하여 ObjectsDeleteAll 함수에 알릴 것입니다.

따라서 지표 0을 삭제하려는 경우 함수는 다음의 값을 수신합니다. SMD_OT#1#. 이렇게 하면 문제를 방지할 수 있습니다. 동시에 위 예제의 경우 함수는 다음과 같은 이름을 갖게 됩니다. SMD_OT#1221766803*. 간단한 것 같지만 이로 인해 여러분은 왜 EA가 새로 주문한 지표 객체를 계속 삭제하는 것인지 당황 스러울 수 있습니다.

이제 한 가지 흥미로운 점에 대해 이야기해 보겠습니다. 함수 끝에는 ChartRedraw라는 호출이 있습니다. 여기서 사용되는 용도는 무엇인가요? MetaTrader 5는 차트 자체를 새로고침하지 않나요? 맞습니다. 새로 고침을 합니다. 하지만 언제가 될지 우리는 정확히 알 수 없습니다. 여기 또다른 문제가 있습니다: 차트에 객체를 배치하거나 삭제하는 모든 호출은 동기식이며 즉 특정 시간에 실행되지만 반드시 우리가 예상하는 시간이 아닐 수도 있다는 문제점이 있습니다. 그러나 주문 시스템에서는 주문을 표시하거나 관리하는 데 객체를 사용하므로 우리는 객체가 차트에 있는지 확인해야 합니다. 우리에게는 MetaTrader 5가 이미 차트에 객체를 배치하거나 제거했다고 생각할 여유가 없기 때문에 이를 확인하기 위해 플랫폼이 강제로 새로 고침을 수행합니다.

따라서 ChartRedraw를 호출하면 플랫폼이 차트의 객체 목록을 강제로 새로 고치도록 하여 우리는 특정 객체가 차트에 있는지 여부를 확인할 수 있습니다. 그럼 이제 다음 주제로 넘어가겠습니다.


2.0.2. 적은 객체 - 빠른 속도

이전 버전의 초기화 함수는 번거로웠습니다. 반복적인 확인이 많았고 일부 항목은 중복되었습니다. 몇 가지 사소한 문제 외에도 이 시스템은 기존의 자신의 자원을 거의 재사용하지 않았습니다. 따라서 저는 새로운 모델링을 활용하기 위해 초기화 중에 생성되는 객체의 수를 줄이기로 결정했습니다. 이제 시스템은 다음과 같이 보입니다:

void Initilize(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false);
        for (int c0 = OrdersTotal(); c0 >= 0; c0--) IndicatorInfosAdd(OrderGetTicket(c0));
        for (int c0 = PositionsTotal(); c0 >= 0; c0--) IndicatorInfosAdd(PositionGetTicket(c0));
}

모든 것이 달라진 것 같고 실제로도 그렇습니다. 이제 우리는 이전에는 충분히 사용되지 않았던 함수를 재사용하고 있습니다 - 이 함수는 차트에 지표를 추가하는 함수입니다. 이 특별한 기능을 살펴보겠습니다.

inline void IndicatorAdd(ulong ticket)
{
        char ret;
                                
        if (ticket == def_IndicatorTicket0) ret = -1; else
        {
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_PENDING, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_RESULT, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if ((ret = GetInfosTradeServer(ticket)) == 0) return;
        }
        switch (ret)
        {
                case  1:
                        CreateIndicatorTrade(ticket, IT_RESULT);
                        PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr);
                        break;
                case -1:
                        CreateIndicatorTrade(ticket, IT_PENDING);
                        PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr);
                        break;
        }
        ChartRedraw();
        UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
}

위의 코드를 자세히 살펴보세요. 코드에 불필요한 검사가 포함되어 있는 것처럼 보일 수 있습니다. 하지만 이러한 검사가 존재하는 이유는 간단합니다. 이 함수가 펜딩 주문 또는 포지션 지표를 만들 수 있는 유일한 방법입니다. 강조 표시된 두 줄이 지표가 존재 하는지 여부를 확인합니다. 이를 위해 선으로 사용되는 객체에 값이 저장되어 있는지 확인합니다. 이것이 객체가 위치한 가격의 값입니다. 표시 객체가 차트에 있는 경우 이 값은 0이 아니어야 합니다. 다른 모든 경우에는 객체가 존재하지 않거나 또 다른 이유로 인해 0과 같지만 이는 중요하지 않습니다. 이제 우리가 차트를 강제로 새로 고쳐야 했는지를 이해하셨나요? 이 작업을 수행하지 않으면 EA가 불필요하게 객체를 추가하게 될 것입니다. 그러므로 우리는 플랫폼이 어느 시점엔가 이 작업을 수행할 때까지 마냥 기다릴 수는 없는 것입니다. 우리는 반드시 차트가 업데이트 되었는지 확인해야 합니다. 그렇지 않은채 이러한 검사가 완료되는 경우 객체의 현재 상태와 일치하지 않는 항목이 보고되어 시스템의 신뢰성이 떨어집니다.

이러한 검사로 인해 EA의 속도가 느려지는 것처럼 보이지만 이는 개념적인 오류입니다. 만약 우리가 이러한 검사를 수행하고 이미 생성 대기열에 있을 수 있는 객체를 강제로 만들려고 하지 않으려면 플랫폼에 "지금 업데이트"라고 알려주면 됩니다. 그런 다음 우리가 필요할 때 객체가 이미 생성되었는지 확인하고 이미 생성된 경우에는 필요에 따라 사용합니다. 이를 "올바른 프로그래밍"이라고 합니다. 이렇게 하면 플랫폼의 작업량을 줄일 수 있고 객체가 생성되었는지 여부를 불필요하게 확인하지 않아도 되므로 EA의 안정성을 높일 수 있습니다. 왜냐하면 작업하려는 데이터가 있다는 것을 우리가 알기 때문입니다.

검사 결과 지정된 티켓과 일치하는 객체가 없는 것으로 나타나므로 객체가 만들어질 것입니다. 우리가 지표 0을 생성하는지 다른 지표를 생성하는지를 체크하는 또 다른 검사가 시작 시점에 있다는 점을 유의하세요. 이렇게 하면 MetaTrader 5에서 지원하는 우리에게는 불필요한 객체가 없게 됩니다: 우리에게는 차트에서 실제로 사용하는 객체만 있게 됩니다. 지표 0을 생성하면 우리는 이를 매우 특수하고 구체적인 조건에서만 생성하게 되므로 추가적인 테스트가 필요하지 않습니다. 객체 0은 SHIFT 또는 CTRL + 마우스를 사용하여 주문을 놓는 위치를 정하는 데 사용됩니다. 곧 어떻게 작동하는지 보여드릴 테니 걱정하지 마세요.

위 코드에서 한 가지 중요한 세부 사항이 있습니다: 우리가 업데이트 함수를 호출하기 전에 차트를 업데이트하는 이유는 무엇일까요? 별로 상관 없는 문제입니다. 이를 이해하기 위해 아래의 UpdateIndicators 함수를 살펴보겠습니다.

void UpdateIndicators(ulong ticket, double tp, double sl, double vol, bool isBuy)
{
        double pr;
        bool b0 = false;
                                
        pr = macroGetLinePrice(ticket, IT_RESULT);
        pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING));
        SetTextValue(ticket, IT_PENDING, vol);
        if (tp > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_TAKE, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_TAKE);
                PositionAxlePrice(ticket, IT_TAKE, tp);
                SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp));
        }
        if (sl > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_STOP, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_STOP);
                PositionAxlePrice(ticket, IT_STOP, sl);
                SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl));
        }
        if (b0) ChartRedraw();
}

이 함수는 기본적으로 한계를 가리키는 지표를 처리합니다. 이제 강조 표시된 두 줄을 살펴보십시오. 차트가 업데이트되지 않으면 이 줄이 트리거 되지 않고 0 값을 반환하게 되고 만약 트리거가 되면 나머지 코드가 작동하지 않고 제한 지표가 화면에 올바르게 표시되지 않습니다.

하지만 제한 지표를 만들기 전에 우리는 몇 가지 점검을 수행하여 실제로 제한 지표를 만들어야 하는지 아니면 조정만 하면 되는지 파악해야 합니다. 이는 기본 객체를 만들 때와 동일한 방식으로 수행됩니다. 그리고 여기에서도 객체를 만들 때 차트를 강제로 업데이트하여 차트가 항상 최신 상태로 유지되도록 해야 합니다.

여러분은 아래와 같이 궁금하실 수도 있습니다: "강제 업데이트가 이렇게 많은데 정말 필요한 건가요?" 이에 대한 답은 ' ' 입니다. 그 이유는 바로 아래의 함수 때문입니다:

inline double SecureChannelPosition(void)
{
        double Res = 0, sl, profit, bid, ask;
        ulong ticket;
                                
        bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
        ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
        for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET));
                SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN));
                sl = PositionGetDouble(POSITION_SL);
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                {
                        if (ask < sl) ClosePosition(ticket);
                }else
                {
                        if ((bid > sl) && (sl > 0)) ClosePosition(ticket);
                }
                Res += profit;
        }
        return Res;
};

여러분은 이 기능이 별거 아니라고 생각할 수도 있습니다. 확실하신가요? 틀렸어요! 이 함수에는 키 포인트가 포함되어 있습니다. 우리는 객체가 차트에 있는지 반드시 확인해야 합니다. 그렇지 않으면 객체를 생성하는 모든 코드가 여러 번 호출되어 큰 대기열이 생성되고 MetaTrader 5는 이 모든 것을 관리해야 하며 일부 데이터가 손실되거나 쓸모 없게 될 수도 있습니다. 이 모든 것이 시스템을 불안정하게 만들고 보안을 약화시켜 시스템을 신뢰할 수 없게 만듭니다. 객체를 생성하는 함수의 호출이 강조 표시됩니다. 전략적으로 중요한 때에 MetaTrader 5가 차트를 업데이트하도록 강제하지 않으면 위의 함수가 OnTick 이벤트에 의해 호출되고 변동성이 높은 기간 동안에는 OnTick에서 오는 호출 수가 상당히 많기 때문에 대기열에 객체가 초과되어 전혀 좋지 않은 문제가 발생할 수 있습니다. 따라서 ChartRedraw 호출을 통해 데이터를 강제로 새로 고치고 ObjectGetDouble을 통해 유효성을 검사하여 대기열에 너무 많은 객체가 있게 되는 가능성을 줄입니다.

시스템이 어떻게 작동하는지 살펴보지 않더라도 여러분은 이렇게 생각할 수 있습니다: "이제 실수로 TradeLine 객체를 삭제할 경우 EA가 이를 알아차리고 ObjectGetDouble을 통한 확인에 실패하여 지표가 작동하지 않게 되면 지표가 다시 생성되므로 좋지 않을까." 이것이 바로 아이디어입니다. 그러나 사용자가 객체가 무엇인지 실제로 알지 못한 채 객체 목록 창에있는 객체를 삭제하는 것은 권장하지 않습니다. 왜냐하면 객체 (TradeLine 제외)를 삭제하면 EA는 지표가 없다는 것을 알지 못하고 해당 버튼 이외의 다른 액세스 방법이 없기 때문에 액세스 할 수 있는 수단이 없이 그대로 남겨 둘 수 있기 때문입니다.

위의 스크립트가 바로 뒤에 와서 클래스 내에서 전체 메시지 흐름을 유지 관리하는 함수가 없게 된다면 정말 악몽과도 같은 스크립트였을 것입니다. 그러나 이것이 유일한 진입 포인트는 아닙니다. 저는 DispatchMessage 함수에 대해 이야기하고 있는데 한 번 살펴보겠습니다.

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong   ticket;
        double  price;
        bool    bKeyBuy,
                bKeySell,
                bEClick;
        datetime        dt;
        uint            mKeys;
        char            cRet;
        eIndicatorTrade it;
        eEventType      ev;
                                
        static bool bMounting = false, bIsDT = false;
        static double valueTp = 0, valueSl = 0, memLocal = 0;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        Mouse.GetPositionDP(dt, price);
                        mKeys   = Mouse.GetButtonStatus();
                        bEClick  = (mKeys & 0x01) == 0x01;    //Left mouse click
                        bKeyBuy  = (mKeys & 0x04) == 0x04;    //SHIFT pressed
                        bKeySell = (mKeys & 0x08) == 0x08;    //CTRL pressed
                        if (bKeyBuy != bKeySell)
                        {
                                if (!bMounting)
                                {
                                        Mouse.Hide();
                                        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
                                        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        m_InfoSelection.it = IT_PENDING;
                                        m_InfoSelection.pr = price;
                                }
                                m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
                                m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
                                m_InfoSelection.bIsBuy = bKeyBuy;
                                if (!bMounting)
                                {
                                        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
                                        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
                                        m_InfoSelection.bIsMovingSelect = bMounting = true;
                                }
                                MoveSelection(price);
                                if ((bEClick) && (memLocal == 0))
                                {
                                        RemoveIndicator(def_IndicatorTicket0);
                                        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
                                }
                        }else if (bMounting)
                        {
                                RemoveIndicator(def_IndicatorTicket0);
                                Mouse.Show();
                                memLocal = 0;
                                bMounting = false;
                        }else if ((!bMounting) && (bKeyBuy == bKeySell))
                        {
                                if (bEClick) SetPriceSelection(price); else MoveSelection(price);
                        }
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (GetIndicatorInfos(sparam, ticket, it, ev))
                        {
                                if (GetInfosTradeServer(ticket) == 0) break;
                                CreateIndicatorTrade(ticket, it);
                                if ((it == IT_PENDING) || (it == IT_RESULT))
                                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                                ChartRedraw();
				m_TradeLine.SpotLight();
                                m_InfoSelection.bIsMovingSelect = false;
                                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        ReDrawAllsIndicator();
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
                        {
                                case EV_CLOSE:
                                        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
                                        {
                                                case IT_PENDING:
                                                case IT_RESULT:
                                                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                                                        break;
                                                case IT_TAKE:
                                                case IT_STOP:
							m_InfoSelection.ticket = ticket;
							m_InfoSelection.it = it;
                                                        m_InfoSelection.bIsMovingSelect = true;
                                                        SetPriceSelection(0);
                                                        break;
                                        }
                                        break;
                                case EV_MOVE:
                                        if (m_InfoSelection.bIsMovingSelect)
                                        {
                                                m_TradeLine.SpotLight();
                                                m_InfoSelection.bIsMovingSelect = false;
                                        }else
                                        {
                                                m_InfoSelection.ticket = ticket;
                                                m_InfoSelection.it = it;
                                                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                                                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
                                        }
                                        break;
                        }
                        break;
        }
}

이 함수는 너무 많은 변화를 거쳤기 때문에 내부에서 어떤 일이 일어나고 있는지 설명하기 위해 저는 작은 부분으로 나눠서 설명해야 합니다. 나약 여러분이 이미 프로그래밍 경험이 있다면 이 함수의 기능을 이해하는 것이 어렵지 않을 것입니다. 그러나 초보 MQL5 프로그래머라면 이 함수를 이해하는 것이 약간 어려울 수 있으므로 다음 항목에서 차분하게 설명하겠습니다.


2.0.3. DispatchMessage 함수 분석

이 항목에서는 DispatchMessage 함수에서 일어나는 일에 대해 설명할 것입니다. 만약 여러분이 단순히 코드를 보고 어떻게 작동하는지 이해했다면 이 주제는 새로운 내용을 제공하지 않습니다.

우리가 먼저 다룰 것은 변수입니다. 로컬 변수 다음으로 먼저 정적 변수가 있습니다.

static bool bMounting = false, bIsDT = false;
static double valueTp = 0, valueSl = 0, memLocal = 0;

클래스에서 프라이빗 변수로 선언할 수도 있지만 이들은 이 시점에서만 사용되므로 클래스의 다른 함수가 이 변수를 보는 것은 의미가 없습니다. 또한 정적으로 선언해야 합니다. 왜냐하면 함수가 다시 호출될 때 해당 값을 기억해야 하기 때문입니다. 만약 '정적' 키워드를 추가하지 않으면 함수가 종료되는 즉시 해당 키워드의 값이 사라지게 됩니다. 이 작업이 완료되면 MetaTrader 5는 EA에 표시하는 이벤트를 처리 하기 시작합니다.

첫 번째 이벤트는 아래에서 확인할 수 있습니다:

case CHARTEVENT_MOUSE_MOVE:
        Mouse.GetPositionDP(dt, price);
        mKeys   = Mouse.GetButtonStatus();
        bEClick  = (mKeys & 0x01) == 0x01;    //Left mouse click
        bKeyBuy  = (mKeys & 0x04) == 0x04;    //SHIFT pressed
        bKeySell = (mKeys & 0x08) == 0x08;    //CTRL pressed

여기서 우리는 마우스와 연결된 일부 키(키보드에서)와 마우스에서 데이터를 수집하고 분리합니다. 이 작업을 마치면 테스트로 시작하는 긴 코드가 나옵니다.

if (bKeyBuy != bKeySell)

SHIFT 또는 CTRL 키를 동시에 누르지 않고 두 키를 동시에 누르면 EA는 여러분이 특정 가격에 주문하고 싶다는 것을 알게 됩니다. 그렇다면 자세히 확인하세요.

if (!bMounting)
{
        Mouse.Hide();
        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        m_InfoSelection.it = IT_PENDING;
        m_InfoSelection.pr = price;
}

지표 0이 아직 설정되지 않은 경우 이 테스트는 통과됩니다. 마우스가 숨겨지면 차트 트레이드의 값이 캡처됩니다. 이 값은 트레이더가 차트 트레이드를 통해 표시한 레버리지 레벨에 따라 포인트로 변환됩니다. 주문이 이루어질 초기 값이 표시됩니다. 이 순서는 사용 주기당 한 번만 수행해야 합니다.

다음 단계는 이익실현과 손절매를 할 수준을 만들고 매수 또는 매도할지 여부를 표시하는 것입니다.

m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
m_InfoSelection.bIsBuy = bKeyBuy;

우리가 마우스를 다른 가격대로 이동하면 이에 따라 우리는 이익실현과 손절매도 이동해야 하기 때문에 이들 수준은 사이클 외부에서 생성됩니다. 그런데 이상한 점은 위의 코드가 어셈블리 테스트에 없다는 것입니다. 그 이유는 무엇일까요? 그 이유는 화면에 지표가 있는 상태에서 마우스를 움직이지 않고 시프트 키를 눌렀다가 떼고 CTRL 키를 누르거나 그 반대로 변경하면 이익실현 및 손절매 지표의 값이 바뀌기 때문입니다. 이를 방지하려면 해당 조각을 테스트에서 제외해야 합니다. 그러나 이로 인해 우리는 아래와 같이 새로운 어셈블리 테스트를 수행해야 합게 됩니다:

if (!bMounting)
{
        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
        m_InfoSelection.bIsMovingSelect = bMounting = true;
}

테스트가 두 가지인 이유는 무엇일까요? 하나만 해도 될까요? 이것이 이상적이지만 위 코드에서 강조 표시된 함수만으로는 우리는 이 작업을 수행할 수 없습니다. 이 부분을 이해하려면 IndicatorAdd를 살펴봐야 합니다. 지표 0을 생성한 후 이를 선택됨으로 설정하고 이미 실행 및 빌드되었음을 표시합니다. 이후 우리는 다음 줄을 살펴보면 됩니다.

MoveSelection(price);

그러나 SHIFT 또는 CTRL을 눌러 펜딩 주문을 하는 동일한 방법 내에서도 마지막 단계가 있습니다.

if ((bEClick) && (memLocal == 0))
{
        RemoveIndicator(def_IndicatorTicket0);
        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
}

이렇게 하면 목표하는 지점에 정확히 펜딩 주문이 추가됩니다. 이때 두 가지 조건이 충족되어야 합니다. 첫 번째는 마우스 왼쪽 버튼 클릭이고 두 번째는 우리가 이를 한 번에 같은 가격으로 하지 않았다는 것입니다. 즉 동일한 호가로 두 개 이상의 주문을 하려면 동일한 호가에서는 주문이 이루어지지 않으므로 다른 호가로 새 주문을 해야 합니다. 

차트에서 지표 0이 제거됨과 동시에 매개변수가 올바르게 채워진 주문이 거래 서버로 전송되게 됩니다.

이제 다음 단계로 넘어가 보겠습니다...

if (bKeyBuy != bKeySell)
{

// ... code described so far ....

}else if (bMounting)
{
        RemoveIndicator(def_IndicatorTicket0);
        Mouse.Show();
        memLocal = 0;
        bMounting = false;
}

지표 0이 설정되었지만 SHIFT 또는 CTRL만 눌러서 조건이 충족되지 않은 경우 강조 표시된 코드가 실행되어 객체 목록에서 지표에서 0이 제거되고 동시에 마우스가 재설정되고 정적 변수가 초기 상태로 유지됩니다. 즉 시스템이 깨끗해집니다.

마우스 이벤트 처리의 다음 단계이자 마지막 단계는 아래와 같습니다:

if (bKeyBuy != bKeySell)
{

// ... previously described code ...

}else if (bMounting)
{

// ... previously described code ...

}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
        if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}


강조 표시된 코드는 메시지 처리에서의 마지막 마우스 단계입니다. 지표 0을 설정하지 않았거나 SHIFT 또는 CTRL 키를 다른 상태로 설정한 경우 즉 동시에 누르거나 놓을 수 있는 상태로 한 경우 다음과 같은 동작이 발생합니다: 왼쪽 클릭을 하면 가격이 지표로 전송되고 마우스만 움직이면 가격이 지표를 이동하는 데 사용됩니다. 그렇다면 우리는 어떤 지표를 사용해야 할까요? 어떤 지표인지 곧 알려드리겠지만, 궁금하실 까봐 말씀드리자면 지표 0은 이 선택 항목을 사용하지 않습니다. 이해가 되지 않는다면 이 섹션의 앞부분으로 돌아가서 이 메시지가 처리되는 작동 방식을 읽어보세요.

아래는 다음 메시지입니다:

case CHARTEVENT_OBJECT_DELETE:
        if (GetIndicatorInfos(sparam, ticket, it, ev))
        {
                if (GetInfosTradeServer(ticket) == 0) break;
                CreateIndicatorTrade(ticket, it);
                if ((it == IT_PENDING) || (it == IT_RESULT))
                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                ChartRedraw();
		m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
        }
        break;

EA에는 지표가 잘못 제거되는 것을 방지하기 위한 작은 보안 시스템이 있다고 전에 말씀드린 것을 기억하시나요? 이 시스템은 객체가 삭제될 때 MetaTrader 5가 전송하는 이벤트와 관련한 메시지를 처리하는 코드에 포함되어 있습니다.

이 경우 MetaTrader 5는 삭제된 객체의 이름인 sparam 매개변수를 사용하여 보고합니다. 이 매개변수는 삭제된 객체가 지표인지 지표라면 어떤 것인지 확인할 수 있습니다. 어떤 객체가 영향을 받았는지는 중요하지 않습니다. 우리가 알고 싶은 것은 어떤 지표가 영향을 받았는지 그 후에 지표와 관련된 주문이나 포지션이 있는지 확인하고 그렇다면 전체 지표를 다시 생성할 것입니다. 극단적인 경우로 영향을 받는 지표가 기본 지표인 경우에 해당 지표를 즉시 재배치하고 MetaTrader 5가 무슨 지표이든 상관없이 즉시 차트에 해당 지표를 배치하도록 합니다. 우리는 선택 표시를 제거하고 표시 임계값 데이터에 대한 업데이트를 주문합니다. 

다음으로 처리할 이벤트는 매우 간단합니다. 화면의 모든 지표의 크기를 조정하도록 요청하도록 하는 것입니다. 코드가 아래에 나와 있습니다.

case CHARTEVENT_CHART_CHANGE:
        ReDrawAllsIndicator();
        break;

다음은 객체 클릭 이벤트입니다.

case CHARTEVENT_OBJECT_CLICK:
        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
        {
//....
        }
        break;

위와 같이 시작됩니다: MetaTrader 5가 어떤 객체를 클릭했는지 우리에게 알려주므로 우리는 EA가 어떤 유형의 이벤트를 처리할지 확인할 수 있습니다. 지금까지 우리는 CLOSE와 MOVE 이벤트 두 개를 살펴보았습니다. 먼저 CLOSE 이벤트를 살펴 보겠습니다. 이 이벤트는 화면에서 지표의 끝을 닫고 정의합니다.

case EV_CLOSE:
        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
        {
                case IT_PENDING:
                case IT_RESULT:
                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                        break;
                case IT_TAKE:
                case IT_STOP:
			m_InfoSelection.ticket = ticket;
			m_InfoSelection.it = it;
                        m_InfoSelection.bIsMovingSelect = true;
                        SetPriceSelection(0);
                        break;
        }
        break;

이 이벤트는 다음을 수행합니다: 티켓을 사용하여 서버에서 종료해야 할 항목을 검색하고 종료할 항목이 있는지 확인합니다. 왜냐하면 이때 서버가 이미 이 작업을 수행했지만 EA가 아직 알지 못하는 경우가 발생할 수 있기 때문입니다. 우리에게는 무언가 클로즈 해야 할 것이 있으므로 차트에서 지표를 클로즈 하거나 제거하기 위해 클래스에 필요한 확인 사항과 올바른 방법을 알릴 수 있어야 합니다.

이제 이 주제의 마지막 단계에 이르렀습니다.

case EV_MOVE:
        if (m_InfoSelection.bIsMovingSelect)
        {
                m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
        }else
        {
                m_InfoSelection.ticket = ticket;
                m_InfoSelection.it = it;
                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
        }
        break;

MOVE는 바로 이 작업을 수행하는 이벤트로 이동할 지표를 선택합니다. MOVE는 선택만 할 뿐 이동 자체는 마우스 이동 이벤트 중에 수행됩니다. 이 글의 시작 부분에서 우리가 지표 0을 다루지 않는다는 조건이 있으며 그럼에도 불구하고 무언가가 여전히 움직일 것이라고 말씀드렸음을 상기하십시오. 이것이 MOVE 이벤트에서 이 시점에 표시됩니다. 우리는 이동하도록 선택된 항목이 있는지 여기에서 확인합니다. 이 경우 선택되어 있던 지표는 선택이 중지되고 마우스 이동 이벤트를 수신하지 않으며 새 지표도 선택되지 않습니다. 이 경우 마우스 데이터를 수신하는 새 지표의 데이터가 구조에 저장되고 이 지표는 자신이 선택되었음을 나타내는 변경 사항을 수신합니다. 이 변화는 선의 두께에서 볼 수 있습니다.


2.0.4. 새로운 Mouse Object 클래스

위에서 다룬 개선 사항 외에도 우리가 알아볼만한 다른 개선 사항도 있습니다.

대부분의 트레이더는 EA에 마우스 기반의 보조지표 시스템이 필요하지 않지만 일부 트레이더들은 필요로 하기도 하며 시스템이 완벽하게 작동하기를 원할 수 있습니다. 그러나 그러한 트레이더 중에 실수로 마우스 지표를 구성하는 일부 객체를 삭제하게 되면 이는 커다란 실패로 이어질 수 있습니다. 다행히도 EVENT 시스템을 사용하면 이러한 상황을 피할 수 있습니다. 객체 삭제 이벤트가 감지되어 EA로 전송되면 객체가 속한 클래스가 객체를 다시 생성하여 시스템의 안정성을 확보하게 됩니다. 그러나 포인트 목록을 가능한 한 작게 유지하고 필요할 때 생성한 다음 더 이상 필요하지 않을 때는 삭제하는 것이 좋습니다. 지금까지 우리는 이렇게 해왔지만 Mouse 클래스가 빠져 있었습니다.

상수 이름 생성 시스템을 대체할 몇 가지 정의를 만드는 것부터 시작하겠습니다.

#define def_MousePrefixName "MOUSE "
#define def_NameObjectLineH def_MousePrefixName + "H"
#define def_NameObjectLineV def_MousePrefixName + "TMPV"
#define def_NameObjectLineT def_MousePrefixName + "TMPT"
#define def_NameObjectBitMp def_MousePrefixName + "TMPB"
#define def_NameObjectText  def_MousePrefixName + "TMPI"

그 후 새로운 초기화 함수는 다음과 같습니다:

void Init(color c1, color c2, color c3)
{
        m_Infos.cor01 = c1;
        m_Infos.cor02 = c2;
        m_Infos.cor03 = c3;
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true);
        ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);
        Show();
}

이전 버전보다 훨씬 간단 해졌습니다. 이 시점에서 마우스 시스템을 표시하는 호출이 있습니다. 호출은 이전 코드에서 강조 표시된 지점에서 수행됩니다. 실제로 가격 축에 표시 시스템을 생성하는 코드를 호출합니다.

inline void Show(void)
{
        if (ObjectGetDouble(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_PRICE) == 0)
        {
                ObjectCreate(Terminal.Get_ID(), def_NameObjectLineH, OBJ_HLINE, 0, 0, 0);
                ObjectSetString(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_TOOLTIP, "\n");
                ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_BACK, false);
        }
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, m_Infos.cor01);
}

이 코드는 매우 흥미롭습니다: 마우스 포인터 객체가 가격에 존재하는지 여부를 확인합니다. 확인에 성공하면 차트에 라인이 있거나 마우스와 관련된 무언가가 있다는 의미이므로 우리는 가로선의 색상을 조정하기만 하면 됩니다. 이 점검을 수행하는 이유가 무엇일까요? 이를 이해하려면 마우스에 연결된 객체를 숨기거나 제거하는 함수를 살펴보세요. 아래의 함수를 참조하세요:

inline void Hide(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        ObjectsDeleteAll(Terminal.Get_ID(), def_MousePrefixName + "T");
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
}

이것은 흥미로운 운영 스타일입니다. 마우스에 연결되어 있고 지정된 이름을 가진 모든 객체는 MetaTrader 5 차트에서 삭제되므로 객체 목록이 항상 작아집니다. 그러나 가로줄은 삭제되지 않고 색상만 변경됩니다. 따라서 마우스를 표시하는 함수는 객체를 생성하기 전에 확인을 수행합니다. 왜냐하면 실제로 객체 목록에서 제외되는 것이 아니라 숨겨져 있을 뿐이기 때문입니다. 그러나 다른 모든 객체는 객체 목록에서 삭제됩니다. 그렇다면 우리는 이러한 다른 객체를 어떻게 사용할 수 있을까요? 단순히 세부 사항을 찾고자 하는 짧은 순간에 목록에 있는 객체를 1-2 번만 보관하는 것은 의미가 없습니다. 이를 연구하고 생성한 다음 목록에서 삭제하여 보다 안정적인 시스템을 구축하는 것이 좋습니다.

우스꽝스러워 보일 수 있지만 우리가 보여주는 주문 시스템은 객체 사용을 기반으로 하며 목록에 객체가 많을수록 특정 객체에 액세스하려는 경우에 MetaTrader 5가 목록을 검색하는 데 더 많은 작업을 수행해야 합니다. 따라서 우리는 차트나 객체 목록에 여분의 객체를 남기지 않고 시스템을 최대한 가볍게 유지할 것입니다.

이제 다음과 같이 시작되는 DispatchMessage 함수에 주목하세요:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        int     w = 0;
        uint    key;
        static int b1 = 0;
        static double memPrice = 0;

그 직후에 첫 번째 이벤트 처리를 시작하는 코드가 있습니다.

switch (id)
{
        case CHARTEVENT_MOUSE_MOVE:
                Position.X = (int)lparam;
                Position.Y = (int)dparam;
                ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
                ObjectMove(Terminal.Get_ID(), def_NameObjectLineH, 0, 0, Position.price = Terminal.AdjustPrice(Position.price));
                if (b1 > 0) ObjectMove(Terminal.Get_ID(), def_NameObjectLineV, 0, Position.dt, 0);
                key = (uint) sparam;
                if ((key & 0x10) == 0x10)    //Middle button....
                {
                        CreateObjectsIntern();
                        b1 = 1;
                }

마우스 가운데 버튼을 누르면 호출을 생성합니다. 하지만 지금은 그렇지 않습니다. 그런 다음 우리는 이 함수의 기능에 대해 살펴 보겠습니다. 우리는 존재하지 않는 객체를 이동하려고 합니다. 왜냐하면 이러한 객체는 MetaTrader 5에서 지원하는 객체 목록에 없기 때문입니다. 이 호출은 마우스 가운데 버튼을 누를 때만 수행됩니다. 참고 b1 변수는 트레이더가 우리의 학습과 관련된 집합 내에 있는 시점을 제어합니다.

사용자가 마우스 왼쪽 버튼을 클릭하고 첫 번째 단계가 완료되면 다음과 같은 코드가 실행됩니다:

if (((key & 0x01) == 0x01) && (b1 == 1))
{
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 0, Position.dt, memPrice = Position.price);
        b1 = 2;
}

이 코드는 추세선을 배치하고 b1 변수 값이 변경되는 다음 단계를 호출합니다. 이 시점에서 우리는 다음 부분으로 넘어갈 수 있습니다.

if (((key & 0x01) == 0x01) && (b1 == 2))
{
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 1, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectMove(Terminal.Get_ID(), def_NameObjectBitMp, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
        ObjectSetString(Terminal.Get_ID(), def_NameObjectText, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
        ObjectMove(Terminal.Get_ID(), def_NameObjectText, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}

위의 이 부분이이 실제로 화면에 표시되는 우리가 학습한 내용입니다. 이 부분에 있는 모든 객체는 우리의 연구가 끝나면 존재하지 않으며 이 루틴 내에서 생성되고 파괴됩니다. 이렇게 하는 것이 그다지 효율적이지 않은 것 같지만 저는 이러한 학습 단계에서 처리 시간이 줄거나 늘어나는 것을 느끼지 못했습니다. 실제로 주문 시스템에는 약간의 개선이 이루어졌는데 이는 비교 오차 범위 내에 있는 매우 미묘한 부분입니다. 따라서 이러한 변경 사항이 실제로 처리 측면에서 개선을 가져왔다고 말할 수는 없습니다.

그러나 마우스 왼쪽 버튼을 누르고 있는 동안 연구가 수행되며 마우스 왼쪽 버튼을 놓는 즉시 다음 조각이 실행된다는 점에 유의하세요.

if (((key & 0x01) != 0x01) && (b1 == 2))
{
        b1 = 0;
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
        Hide();
        Show();
}
Position.ButtonsStatus = (b1 == 0 ? key : 0);

여기서는 우리는 우리의 학습을 위해 사용된 모든 객체를 객체 목록에서 제거합니다. 화면에 마우스 라인을 다시 표시해 보겠습니다. 강조 표시된 코드는 마우스 버튼을 캡처할 때 EA 내부의 함수나 서브루틴이 잘못된 판독값을 얻는 것을 방지하므로 좋은 아이디어입니다. 연구가 진행 중인 경우 EA는 버튼 상태를 무시해야 합니다. 이를 위해 우리는 강조 표시된 선을 사용합니다. 완벽한 솔루션은 아니지만 없는 것보다는 낫습니다.

연구를 실행하기 위해 객체를 생성하는 코드는 고려하지 않았습니다. 그러나 이것은 매우 간단한 함수이므로 기사에서는 이에 초점을 맞추지 않겠습니다.


결론

변경 사항은 사소해 보이지만 모두 시스템에 큰 변화를 가져옵니다. 한 가지 기억해야 할 점이 있습니다: 명령 시스템은 화면의 그래픽 객체를 기반으로 하므로 EA가 처리하는 객체가 많을수록 특정 객체를 요청할 때 성능이 저하된다는 것입니다. 상황을 더욱 복잡하게 만드는 것은 시스템이 실시간으로 작동한다는 점입니다. 즉, EA의 시스템이 빠를수록 성능은 더 좋아집니다. 따라서 EA가 해야 할 일이 적을수록 좋은 것입니다. 이상적으로는 주문 시스템에서만 작동할 수 있어야 하며 다른 모든 것을 더 높은 수준으로 끌어 올려야 하며 MetaTrader 5가 이를 처리해야합니다. 물론 많은 작은 변경을 해야 하지만 너무 복잡하지는 않기 때문에 우리는 작업을 점진적으로 수행할 것입니다. 이 내용은 EA의 신뢰성을 향상하기 위한 내용을 다루는 이후의 몇 개의 글에서만 다룰 예정입니다.

한 가지 확실히 말씀드릴 수 있는 것은 앞으로 EA는 주문 시스템만 담당하게 될 것입니다. 다음 기사에서 우리는 주문 시스템이 대규모 객체 생성기라는 시각에서 EA 작동 중에 목록에 존재하는 객체의 수를 더 줄이고 MetaTrader 5에서 생성되는 부하를 최소화하는 방식으로 이 시스템을 변경하는 방법을 살펴볼 것입니다.

따라서 코드 자체는 여전히 변경될 수 있으므로 이 글에 수정 사항을 첨부하지는 않겠습니다. 하지만 걱정하지 마세요. 다음 글을 기다리셔도 좋습니다. 이러한 변경으로 인해 전문가 EA의 전반적인 성능이 크게 향상될 것입니다. 이 시리즈의 다음 글에서 뵙겠습니다.


MetaQuotes 소프트웨어 사를 통해 포르투갈어가 번역됨
원본 기고글: https://www.mql5.com/pt/articles/10593

MQL5에서 행렬 및 벡터: 활성화 함수 MQL5에서 행렬 및 벡터: 활성화 함수
여기서는 머신 러닝에서의 활성화 함수에 대해서만 설명하겠습니다. 인공 신경망에서 뉴런 활성화 함수는 입력 신호 또는 입력 신호 세트의 값을 기반으로 출력 신호의 값을 계산합니다. 우리는 이 프로세스의 내부의 작동 방식에 대해 자세히 살펴볼 것입니다.
앨리게이터로 트레이딩 시스템 설계 방법 알아보기 앨리게이터로 트레이딩 시스템 설계 방법 알아보기
이번 글이 가장 인기 있는 보조지표를 기반으로 트레이딩 시스템을 설계하는 방법에 대한 시리즈의 마지막 기사입니다. 우리는 앨리게이터 지표를 기반으로 트레이딩 시스템을 만드는 방법을 배워볼 것입니다.
MQL5에서 ONNX 모델을 앙상블하는 방법의 예시 MQL5에서 ONNX 모델을 앙상블하는 방법의 예시
ONNX(Open Neural Network eXchange)는 신경망을 위해 만들어진 개방형 형식입니다. 이 글에서는 하나의 Expert Advisor에서 두 개의 ONNX 모델을 동시에 사용하는 방법을 소개하겠습니다.
엑셀러레이터 오실레이터를 사용하여 거래 시스템을 설계하는 방법을 알아보세요 엑셀러레이터 오실레이터를 사용하여 거래 시스템을 설계하는 방법을 알아보세요
이 기사는 인기 있는 기술 지표를 기반으로 거래 시스템을 설계하는 방법과 관련한 시리즈의 새로운 글입니다. 우리는 엑셀러레이터 오실레이터 지표라는 새로운 지표에 대해 알아보고 이를 활용하여 거래 시스템을 설계하는 방법을 알아볼 것입니다.