English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
WCF 서비스를 통해 MetaTrader5에서 .NET 애플리케이션으로 인용문 내보내기

WCF 서비스를 통해 MetaTrader5에서 .NET 애플리케이션으로 인용문 내보내기

MetaTrader 5 | 5 7월 2021, 13:02
114 0
Alexander
Alexander

개요

MetaTrader4에서 DDE 서비스를 사용하던 프로그래머께서는 더이상 지원되지 않는다는 걸 아마 아실 거예요. 인용문을 내보내는 정해진 방법도 마땅히 없고요. 이에 대한 해결책으로 MQL5 개발자들은 여러분이 이를 구현하는 자체 dll을 사용할 것을 제안합니다. 어차피 구현해야 된다면 머리를 잘 굴려보자고요!

왜 .NET인가?

저는 오랜 시간 .NET으로 프로그래밍을 했는데요. 이 플랫폼을 이용하면 인용문 내보내기가 훨씬 합리적이고, 재밌고, 간단하게 구현될 수 있겠더라고요. 안타깝게도 MQL5는 .NET 형식을 지원하지 않습니다. 개발자들도 다 이유가 있었겠죠. 그렇기 때문에 .NET 형식을 지원하는 win32 dll을 이용하도록 하겠습니다.

왜 WCF인가?

윈도우 커뮤니케이션 파운데이션(WCF)을 선택한 이유는요. 우선 확장과 적응이 쉬워서 입니다. 그리고 어려운 작업으로 시험해 보고 싶기도 했고요. 게다가, 마이크로소프트에 의하면 WCF가 .NET 리모팅에 비해 성능이 좋다더군요.

시스템 요건

우리 시스템에 어떤게 필요할지 생각해 보죠. 제 생각에는 크게 두 가지 요건으로 나뉠 것 같아요.

    1. 기본 구조인 MqlTick을 이용해 틱을 내보내는 편이 낫겠죠.
    2. 이미 내보내진 심볼 리스트를 아는 것도 좋을테고요.

    시작합시다.

    1. 일반 클래스 및 계약

    우선, 새로운 클래스 라이브러리를 생성하고 QExport.dll이라는 이름을 붙일게요. MqlTick 구조는 DataContract로 정의합니다.

        [StructLayout(LayoutKind.Sequential)]
        [DataContract]
        public struct MqlTick
        {
            [DataMember] 
            public Int64 Time { get; set; }
            [DataMember]
            public Double Bid { get; set; }
            [DataMember]
            public Double Ask { get; set; }
            [DataMember]
            public Double Last { get; set; }
            [DataMember]
            public UInt64 Volume { get; set; }
        }
    

    그 다음은 서비스의 계약을 정의하도록 하겠습니다. 개인적으로 config 클래스나 proxy 클래스는 좋아하지 않기 때문에 여기서도 사용하지 않을 겁니다.

    위에 서술한 시스템 요건을 기반으로 첫 번째 서버 계약을 정의하겠습니다.

        [ServiceContract(CallbackContract = typeof(IExportClient))]
        public interface IExportService
        {
            [OperationContract]
            void Subscribe();
    
            [OperationContract]
            void Unsubscribe();
    
            [OperationContract]
            String[] GetActiveSymbols();
        }
    

    보시다시피 서버 알림에 구독을 하거나 구독을 취소하는 표준 체계가 있죠. 아래는 각 오퍼레이션의 간단한 설명입니다.

    오퍼레이션명칭
    Subscribe()틱 내보내기 구독
    Unsubscribe()틱 내보내기 구독 해지
    GetActiveSymbols()내보내진 심볼 목록 반환


    또한 해당 인용문과 내보내진 심볼 목록에 대한 변경 사항이 클라이언트 콜백으로 전송되어야 합니다. 성능 향상을 위해 오퍼레이션은 '단방향 작업'으로 정의합니다.

        [ServiceContract]
        public interface IExportClient
        {
            [OperationContract(IsOneWay = true)]
            void SendTick(String symbol, MqlTick tick);
    
            [OperationContract(IsOneWay = true)]
            void ReportSymbolsChanged();
        }
    
    오퍼레이션명칭
    SendTick(String, MqlTick)틱 전송
    ReportSymbolsChanged()클라이언트에 심볼 목록 변경 사항 알림

    2. 서버 구현

    새로운 빌드를 생성해 서버 계약을 구현하는 서비스를 Qexport.Service.dll로 명명합니다.

    표준 바인딩에 비해 성능이 뛰어난 NetNamedPipesBinding을 사용할게요. 네트워크를 이용해 인용문을 브로드캐스트해야 하는 경우에는 NetTcpBinding이 사용되어야 합니다.

    다음은 서버 계약 구현에 대한 설명입니다.

    클래스 정의. 우선, ServiceBehavior 속성은 다음의 특성을 갖습니다.

    • InstanceContextMode = InstanceContextMode.Single-처리된 모든 요청에 하나의 서비스 인스턴스를 제공하여 솔루션의 성능을 향상시킵니다. 또한, 내보내진 심볼 목록을 제공하고 관리할 수 있습니다.
    • ConcurrencyMode = ConcurrencyMode.Multiple -클라이언트의 모든 요청에 병렬 연산을 적용합니다.
    • UseSynchronizationContext = false –행 발생 방지를 위해 GUI 쓰레드에 연결하지 않습니다. 이번 작업에서는 필요하지 않지만 윈도우 응용 프로그램을 이용하여 서비스를 호스팅하는 경우 필요합니다.
    • IncludeExceptionDetailInFaults = true –클라이언트에게 전달 시 FaultException 개체에 대한 예외 세부 정보를 포함합니다.

    ExportService는 다음의 두 가지 인터페이스를 갖습니다: IExportService, IDisposable. 첫 번째 인터페이스는 모든 서비스 함수를 구현하며, 두 번째 인터페이스는 .NET 리소스 릴리스의 표준 모델을 구현합니다.

        [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
            ConcurrencyMode = ConcurrencyMode.Multiple,
            UseSynchronizationContext = false,
            IncludeExceptionDetailInFaults = true)]
        public class ExportService : IExportService, IDisposable
        {
    

    서비스 변수를 정의하겠습니다.

            // full address of service in format net.pipe://localhost/server_name
            private readonly String _ServiceAddress;
    
            // service host
            private ServiceHost _ExportHost;
    
            // active clients callbacks collection
            private Collection<IExportClient> _Clients = new Collection<IExportClient>();
    
            // active symbols list
            private List<String> _ActiveSymbols = new List<string>();
            
            // object for locking
            private object lockClients = new object();
    

    서비스를 열고 닫는 Open() 과 Close() 메소드를 정의할게요.

            public void Open()
            {
                _ExportHost = new ServiceHost(this);
    
                // point with service
                _ExportHost.AddServiceEndpoint(typeof(IExportService),  // contract
                    new NetNamedPipeBinding(),                          // binding
                    new Uri(_ServiceAddress));                          // address
    
                // remove the restriction of 16 requests in queue
                ServiceThrottlingBehavior bhvThrot = new ServiceThrottlingBehavior();
                bhvThrot.MaxConcurrentCalls = Int32.MaxValue;
                _ExportHost.Description.Behaviors.Add(bhvThrot);
    
                _ExportHost.Open();
            }
    
            public void Close()
            {
                Dispose(true);
            }
           
            private void Dispose(bool disposing)
            {
                try
                {
                    // closing channel for each client
                    // ...
    
                    // closing host
                    _ExportHost.Close();
                }
                finally
                {
                    _ExportHost = null;
                }
    
                // ...
            }
    

    다음으로는 IExportService 메소드
    를 구현하겠습니다.

            public void Subscribe()
            {
                // get the callback channel
                IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>();
                lock (lockClients)
                    _Clients.Add(cl);
            }
    
            public void Unsubscribe()
            {
                // get the callback chanell
                IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>();
                lock (lockClients)
                    _Clients.Remove(cl);
            }
    
            public String[] GetActiveSymbols()
            {
                return _ActiveSymbols.ToArray();
            }
    

    이제 틱을 전송하고 내보내진 심볼을 등록하고 삭제하는 메소드를 추가할 차례입니다.

         public void RegisterSymbol(String symbol)
            {
                if (!_ActiveSymbols.Contains(symbol))
                    _ActiveSymbols.Add(symbol);
    
                  // sending notification to all clients about changes in the list of active symbols
                  //...
            }
    
            public void UnregisterSymbol(String symbol)
            {
                _ActiveSymbols.Remove(symbol);
    
                 // sending notification to all clients about the changes in the list of active symbols
                 //...
            }
    
            public void SendTick(String symbol, MqlTick tick)
            {
                lock (lockClients)
                    for (int i = 0; i < _Clients.Count; i++)
                        try
                        {
                            _Clients[i].SendTick(symbol, tick);
                        }
                        catch (CommunicationException)
                        {
                            // it seems that connection with client has lost - we just remove the client
                            _Clients.RemoveAt(i);
                            i--;
                        }
            }
    

    메인 서버 함수 목록도 요약해 볼게요. 필요한 것만요.

    메소드명칭
    Open()서버 실행
    Close()서버 중지
    RegisterSymbol(String)내보내진 심볼 목록에 심볼 추가
    UnregisterSymbol(String)내보내진 심볼 목록에서 심볼 삭제
    GetActiveSymbols()내보내진 심볼 개수 반환
    SendTick(String, MqlTick)클라이언트로 틱 전송

     

    3. 클라이언트 구현

    서버에 대해 알아보았으니 이제 클라이언트를 볼 차례입니다. Qexport.Client.dll. 빌드를 만듭니다. 클라이언트 계약이 구현될 곳입니다. 우선 행동을 정의하는 CallbackBehavior 속성을 알아보죠. 다음의 제어자를 갖습니다.

      • ConcurrencyMode = ConcurrencyMode.Multiple-모든 콜백 및 서버 응답에 병렬 연산을 적용합니다. 이 제어자는 매우 중요한데요. 서버가 ReportSymbolsChanged() 콜백 함수를 이용해 클라이언트에게 내보내진 심볼 목록에 대한 변경 사항을 알리려고 한다고 생각해 보세요. 그리고 클라이언트는 콜백 함수에서 GetActiveSymbols() 메소드를 호출해 새로운 심볼 목록을 받고 싶어하고요. 따라서 서버 응답을 기다리면서 콜백을 진행하는 클라이언트는 서버로부터 응답을 받을 수 없습니다. 결국 클라이언트는 타임아웃으로 연결을 끊게 되고요.
      • UseSynchronizationContext = false-행 발생 방지를 위해 GUI 쓰레드에 연결하지 않습니다. wcf 콜백은 자동으로 부모 스레드에 연결됩니다. 부모 스레드에 GUI가 있는 경우, 콜백이 자신을 호출한 메소드가 완료될 때까지 대기하는 반면 메소드는 콜백이 완료될 때까지 기다리므로 메소드가 완료되지 않는 상황이 발생할 수 있습니다. 분명 다르긴 하지만 조금 전에 다룬 케이스와 꽤 비슷하죠.

      서버 케이스의 경우 클라이언트는 다음의 두 가지 인터페이스를 구현합니다: IExportClient, IDisposable.

       [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,
              UseSynchronizationContext = false)]
          public class ExportClient : IExportClient, IDisposable
          {
      

      서비스 변수를 정의하겠습니다.

              // full service address
              private readonly String _ServiceAddress;
      
              // service object 
              private IExportService _ExportService;
      
              // Returns service instance
              public IExportService Service
              {
                  get
                  {
                      return _ExportService;
                  }
              }
      
              // Returns communication channel
              public IClientChannel Channel
              {
                  get
                  {
                      return (IClientChannel)_ExportService;
                  }
              }
      

      이제 콜백 메소드에 필요한 이벤트를 생성해 봅시다. 클라이언트 애플리케이션은 반드시 이벤트를 구독하고, 클라이언트 상태 변경에 대한 알림을 받을 수 있어야 합니다.

              // calls when tick received
              public event EventHandler<TickRecievedEventArgs> TickRecieved;
      
              // call when symbol list has changed
              public event EventHandler ActiveSymbolsChanged;
      

      또한 클라이언트의 Open() 및 Close() 메소드를 정의합니다.

              public void Open()
              {
                  // creating channel factory
                  var factory = new DuplexChannelFactory<IExportService>(
                      new InstanceContext(this),
                      new NetNamedPipeBinding());
      
                  // creating server channel
                  _ExportService = factory.CreateChannel(new EndpointAddress(_ServiceAddress));
      
                  IClientChannel channel = (IClientChannel)_ExportService;
                  channel.Open();
      
                  // connecting to feeds
                  _ExportService.Subscribe();
              }
      
              public void Close()
              {
                  Dispose(true);
              }
      
              private void Dispose(bool disposing)
              {
                  try
                  {
                      // unsubscribe feeds
                      _ExportService.Unsubscribe();
                      Channel.Close();
      
                  }
                  finally
                  {
                      _ExportService = null;
                  }
                  // ...
              }
      

      클라이언트가 열리거나 닫힐 때 피드에서 연결 및 연결 해제가 호출되므로 직접 호출할 필요가 없습니다.

      이제 클라이언트 계약을 써 볼까요? 계약이 구현되면 다음의 이벤트가 발생합니다.

              public void SendTick(string symbol, MqlTick tick)
              {
                  // firing event TickRecieved
              }
      
              public void ReportSymbolsChanged()
              {
                  // firing event ActiveSymbolsChanged        
              }
      

      마지막으로, 클라이언트의 주요 속성과 메소드는 다음과 같습니다.

      속성명칭
      Service서비스 커뮤니케이션 채널
      Channel서비스 계약 IExportService 발생 


      메소드명칭
      Open()서버 연결
      Close()서버 연결 해제

       

      이벤트명칭
      TickRecieved새로운 인용문 수신 후 발생
      ActiveSymbolsChanged실행 중인 심볼 목록에 변화 발생 시 발생

       

      4. 두 .NET 애플리케이션 간 전송 속도

      두 가지 .NET 애플리케이션 간의 전송 속도를 측정하는게 저는 꽤 재밌더라고요. 사실 초당 틱 개수인 처리량으로 측정되는 거죠. 서비스 성능 측정을 위해 몇 가지 콘솔 애플리케이션을 썼는데요. 첫 번째는 서버용이고 두 번째는 클라이언트용이죠. 서버의 Main() 함수에 다음의 코드를 작성했습니다.

                  ExportService host = new ExportService("mt5");
                  host.Open();
      
                  Console.WriteLine("Press any key to begin tick export");
                  Console.ReadKey();
      
                  int total = 0;
      
                  Stopwatch sw = new Stopwatch();
      
                  for (int c = 0; c < 10; c++)
                  {
                      int counter = 0;
                      sw.Reset();
                      sw.Start();
      
                      while (sw.ElapsedMilliseconds < 1000)
                      {
                          for (int i = 0; i < 100; i++)
                          {
                              MqlTick tick = new MqlTick { Time = 640000, Bid = 1.2345 };
                              host.SendTick("GBPUSD", tick);
                          }
                          counter++;
                      }
      
                      sw.Stop();
                      total += counter * 100;
      
                      Console.WriteLine("{0} ticks per second", counter * 100);
                  }
      
                  Console.WriteLine("Average {0:F2} ticks per second", total / 10);
                  
                  host.Close();
      

      보시다시피, 해당 코드의 처리량은 10건으로 측정됩니다. 다음은 애슬론 3000+에서 얻은 테스트 결과입니다.

      2600 ticks per second
      3400 ticks per second
      3300 ticks per second
      2500 ticks per second
      2500 ticks per second
      2500 ticks per second
      2400 ticks per second
      2500 ticks per second
      2500 ticks per second
      2500 ticks per second
      Average 2670,00 ticks per second
      

      초당 2500틱이네요. 이 정도면 100가지 심볼에 대한 인용문을 내보내기에 충분한 것 같아요(물론 그렇다고 가정하는 거죠. 아무도 그렇게 많은 차트를 열 것 같지는 않거든요). 게다가, 클라이언트의 수가 증가하면 각 클라이언트가 받을 수 있는 심볼의 최대 수는 감소합니다.

      5. '계층' 만들기

      이제 클라이언트 터미널에 어떻게 연결할지 생각해 봐야죠. MetaTrader5에서 첫 함수를 호출하면 어떻게 되는지 생각해 보세요. .NET 런타임 환경(CLR)이 로드되고 애플리케이션 도메인이 자동으로 생성되죠. 코드가 실행된 후에도 언로드되지 않는게 흥미로운데요.

      이때 CLR을 언로드하는 유일한 방법은 CLR을 종료(클라이언트 터미널 종료)하여 윈도우가 모든 프로세스 리소스를 지우도록 하는 겁니다. 다시 말해, 우리가 생성한 객체는 애플리케이션 도메인이 언로드되거나 가비지 수집기가 작동할 때까지 존재하게 됩니다.

      꽤 괜찮아 보인다고 생각할 수도 있지만 사실 가비지 수집기가 객체를 소멸시키는 것을 막더라도 MQL5 객체에는 접근할 수 없습니다. 다행히도 액세스 방법은 쉽게 만들 수 있어요. 중요한 건 말이죠. 각 애플리케이션 도메인별로 가비지 수집기 핸들 테이블(GC 핸들 테이블)이 있으며 애플리케이션은 이 테이블을 이용해 객체의 수명을 추적하고 수동으로 관리한다는 것입니다.

      애플리케이션은 System.Runtime.InteropServices.GCHandle.을 사용하여 테이블 요소를 제거하거나 추가합니다. 따라서 객체를 적절한 디스크립터로 래핑하기만 하면 GCHandle.Target. 속성에 대한 액세스를 얻을 수 있죠. 그러므로 GCHandle 개체를 참조할 수 있습니다. GCHandle 개체는 핸들 테이블에 속하며 절대 가비지 수집기에 의해 이동되거나 삭제될 수 없죠. 디스크립터의 참조 때문에 래핑된 객체는 재활용되지 않습니다.

      이제 이론을 실전에 적용해 보겠습니다. 우선 QExpertWrapper.dll이라는 이름으로 새로운 win32 dll을 생성하고 CLR을 지원하는 System.dll, QExport.dllQexport.Service.dll을 추가해 참조를 빌드합니다. 또한 마샬링을 수행하거나 핸들로 개체를 수신하는 등의 제어가 가능하도록 보조 클래스인 ServiceManaged를 생성합니다.

      ref class ServiceManaged
      {
              public:
                      static IntPtr CreateExportService(String^);
                      static void DestroyExportService(IntPtr);
                      static void RegisterSymbol(IntPtr, String^);
                      static void UnregisterSymbol(IntPtr, String^);
                      static void SendTick(IntPtr, String^, IntPtr);
      };
      

      해당 메소드의 구현에 대해 생각해 보죠. CreateExportService 메소드가 서비스를 만들고 GCHandle.Alloc을 이용해 GCHandle로 래핑한 후 참조를 반환하겠죠. 오류가 발생하면 MassageBox 함수가 나타날 겁니다. 사실 저는 디버깅 목적으로 사용하기 때문에 꼭 필요한 건지는 잘 모르겠습니다만, 그래도 여기 써 놓을게요.

      IntPtr ServiceManaged::CreateExportService(String^ serverName)
      {
              try
              {
                      ExportService^ service = gcnew ExportService(serverName);
                      service->Open();
              
                      GCHandle handle = GCHandle::Alloc(service);
                      return GCHandle::ToIntPtr(handle);
              }
              catch (Exception^ ex)
              {
                      MessageBox::Show(ex->Message, "CreateExportService");
              }
      }
      

      DestroyExportService 메소드가 포인터를 서비스의 GCHandle로 보내고 타겟 속성에서 서비스를 얻은 후 Close() 메소드를 호출합니다. Free() 메소드를 호출해 서비스 객체를 릴리스하는 것 또한 중요합니다. 그렇지 않으면 메모리에 남고 가비지 수집기에 의해 소멸되지 않거든요.

      void ServiceManaged::DestroyExportService(IntPtr hService)
      {
              try
              {
                      GCHandle handle = GCHandle::FromIntPtr(hService);
      
                      ExportService^ service = (ExportService^)handle.Target;
                      service->Close();
      
                      handle.Free();
              }
              catch (Exception^ ex)
              {
                      MessageBox::Show(ex->Message, "DestroyExportService");
              }
      }
      

      RegisterSymbol 메소드는 내보내진 심볼 목록에 심볼을 추가합니다.

      void ServiceManaged::RegisterSymbol(IntPtr hService, String^ symbol)
      {
              try
              {
                      GCHandle handle = GCHandle::FromIntPtr(hService);
                      ExportService^ service = (ExportService^)handle.Target;
      
                      service->RegisterSymbol(symbol);
              }
              catch (Exception^ ex)
              {
                      MessageBox::Show(ex->Message, "RegisterSymbol");
              }
      }
      

      UnregisterSymbol 메소드는 해당 목록에서 심볼을 삭제하죠.

      void ServiceManaged::UnregisterSymbol(IntPtr hService, String^ symbol)
      {
              try
              {
                      GCHandle handle = GCHandle::FromIntPtr(hService);
                      ExportService^ service = (ExportService^)handle.Target;
      
                      service->UnregisterSymbol(symbol);
              }
              catch (Exception^ ex)
              {
                      MessageBox::Show(ex->Message, "UnregisterSymbol");
              }
      }
      

      이제 SendTick 메소드에 대해 알아볼건데요. 마샬 클래스를 이용해 포인터가 MqlTick 구조로 변형되었습니다. 여기서 한 가지 더, 캐치 블록에는 코드가 없습니다. 에러 발생 시 일반 틱 큐가 느려지는 걸 방지하기 위함이죠.

      void ServiceManaged::SendTick(IntPtr hService, String^ symbol, IntPtr hTick)
      {
              try
              {
                      GCHandle handle = GCHandle::FromIntPtr(hService);
                      ExportService^ service = (ExportService^)handle.Target;
              
                      MqlTick tick = (MqlTick)Marshal::PtrToStructure(hTick, MqlTick::typeid);
      
                      service->SendTick(symbol, tick);
              }
              catch (...)
              {
              }
      }
      

      EX5 프로그램에서 호출되는 함수의 구현에 대해 생각해 보죠.

      #define _DLLAPI extern "C" __declspec(dllexport)
      
      // ---------------------------------------------------------------
      // Creates and opens service 
      // Returns its pointer
      // ---------------------------------------------------------------
      _DLLAPI long long __stdcall CreateExportService(const wchar_t* serverName)
      {
              IntPtr hService = ServiceManaged::CreateExportService(gcnew String(serverName));
              
              return (long long)hService.ToPointer(); 
      }
      
      // ----------------------------------------- ----------------------
      // Closes service
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall DestroyExportService(const long long hService)
      {
              ServiceManaged::DestroyExportService(IntPtr((HANDLE)hService));
      }
      
      // ---------------------------------------------------------------
      // Sends tick
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall SendTick(const long long hService, const wchar_t* symbol, const HANDLE hTick)
      {
              ServiceManaged::SendTick(IntPtr((HANDLE)hService), gcnew String(symbol), IntPtr((HANDLE)hTick));
      }
      
      // ---------------------------------------------------------------
      // Registers symbol to export
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall RegisterSymbol(const long long hService, const wchar_t* symbol)
      {
              ServiceManaged::RegisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
      }
      
      // ---------------------------------------------------------------
      // Removes symbol from list of exported symbols
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall UnregisterSymbol(const long long hService, const wchar_t* symbol)
      {
              ServiceManaged::UnregisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
      }
      

      코드는 준비되었으니 컴파일과 빌드만 하면 됩니다. 프로젝트 옵션에서 출력 디렉토리를 C:\Program Files\MetaTrader 5\MQL5\Libraries로 설정합니다. 해당 폴더에 라이브러리 세 개가 컴파일될 겁니다.

      MQL5 프로그램에서는 QExportWrapper.dll만 사용되며 나머지 두 라이브러리는 QExportWrapper가 사용합니다. 그렇기 때문에 Qexport.dll와 Qexport.Service.dll, 두 라이브러리를 MetaTrader 루트 디렉토리에 저장해야 하는데요. 편리한 방법은 아니죠.

      이에 대한 해결책은 구성 파일을 만들어 라이브러리의 경로를 지정하는 것입니다. MetaTrader 루트 디렉토리에 terminal.exe.config라는 이름의 파일을 생성한 후 다음의 문자열을 입력합니다.

      <?xml version="1.0" encoding="UTF-8" ?>
      <configuration>
         <runtime>
            <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
               <probing privatePath="mql5\libraries" />
            </assemblyBinding>
         </runtime>
      </configuration>
      

      다 됐네요. 이제 CLR은 우리가 지정한 폴더에서 라이브러리를 검색하게 됩니다.

       6. MQL5로 부분 서버 구현하기

      드디어 MQL5로 부분 서버를 만들 차례네요. QService.mqh라는 새 파일을 생성하고 QExpertWrapper.dll에서 불러온 함수를 정의합니다.

      #import "QExportWrapper.dll"
         long  CreateExportService(string);
         void DestroyExportService(long);
         void RegisterSymbol(long, string);
         void UnregisterSymbol(long, string);
         void SendTick(long, string, MqlTick&);
      #import
       
      

      모든 로직을 담기에 아주 이상적인 기능인 클래스를 MQL5에서 사용할 수 있다는 건 정말 좋은 일이죠. 코드 작성 시간은 줄고 코드에 대한 이해는 쉬워지니까요. 그러니까 라이브러리 메소드를 포함할 클래스를 만들어 봅시다.

      또한, 심볼마다 서비스가 생성되지 않도록 확인해 주는 서비스를 구성하고 사용해 보죠. 이때 아주 이상적인 방법 중 하나는 전역 변수를 활용하는 건데요. 이유는 다음과 같습니다.

      • 전역 변수는 클라이언트 터미널이 종료되면 사라지기 때문입니다. 서비스의 경우에도 마찬가지이죠.
      • 서비스를 이용하는 모든 Qservice 객체를 다룰 수 있죠. 마지막 객체가 닫힌 후에야 실제 서비스를 종료할 수 있습니다.

      Qservice 클래스를 만들어 보죠.

      class QService
      {
         private:
            // service pointer
            long hService;
            // service name
            string serverName;
            // name of the global variable of the service
            string gvName;
            // flag that indicates is service closed or not
            bool wasDestroyed;
            
            // enters the critical section
            void EnterCriticalSection();
            // leaves the critical section
            void LeaveCriticalSection();
            
         public:
         
            QService();
            ~QService();
            
            // opens service
            void Create(const string);
            // closes service
            void Close();
            // sends tick
            void SendTick(const string, MqlTick&);
      };
      
      //--------------------------------------------------------------------
      QService::QService()
      {
         wasDestroyed = false;
      }
      
      //--------------------------------------------------------------------
      QService::~QService()
      {
         // close if it hasn't been destroyed
         if (!wasDestroyed)
            Close();
      }
      
      //--------------------------------------------------------------------
      QService::Create(const string serviceName)
      {
         EnterCriticalSection();
         
         serverName = serviceName;
         
         bool exists = false;
         string name;
         
         // check for the active service with such name
         for (int i = 0; i < GlobalVariablesTotal(); i++)
         {
            name = GlobalVariableName(i);
            if (StringFind(name, "QService|" + serverName) == 0)
            {
               exists = true;
               break;
            }
         }
         
         if (!exists)   // if not exists
         {
            // starting service
            hService = CreateExportService(serverName);
            // adding a global variable
            gvName = "QService|" + serverName + ">" + (string)hService;
            GlobalVariableTemp(gvName);
            GlobalVariableSet(gvName, 1);
         }
         else          // the service is exists
         {
            gvName = name;
            // service handle
            hService = (int)StringSubstr(gvName, StringFind(gvName, ">") + 1);
            // notify the fact of using the service by this script
            // by increase of its counter
            GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) + 1);
         }
         
         // register the chart symbol
         RegisterSymbol(hService, Symbol());
         
         LeaveCriticalSection();
      }
      
      //--------------------------------------------------------------------
      QService::Close()
      {
         EnterCriticalSection();
         
         // notifying that this script doen't uses the service
         // by decreasing of its counter
         GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) - 1);
           
         // close service if there isn't any scripts that uses it
         if (NormalizeDouble(GlobalVariableGet(gvName), 0) < 1.0)
         {
            GlobalVariableDel(gvName);
            DestroyExportService(hService);
         }  
         else UnregisterSymbol(hService, Symbol()); // unregistering symbol
          
         wasDestroyed = true;
         
         LeaveCriticalSection();
      }
      
      //--------------------------------------------------------------------
      QService::SendTick(const string symbol, MqlTick& tick)
      {
         if (!wasDestroyed)
            SendTick(hService, symbol, tick);
      }
      
      //--------------------------------------------------------------------
      QService::EnterCriticalSection()
      {
         while (GlobalVariableCheck("QService_CriticalSection") > 0)
            Sleep(1);
         GlobalVariableTemp("QService_CriticalSection");
      }
      
      //--------------------------------------------------------------------
      QService::LeaveCriticalSection()
      {
         GlobalVariableDel("QService_CriticalSection");
      }

      해당 클래스에는 다음의 메소드가 포함됩니다.

      메소드명칭
      Create(const string)서비스 시작
      Close()서비스 종료
      SendTick(const string, MqlTick&)인용문 전송

       

      EnterCriticalSection()과 LeaveCriticalSection() 메소드를 이용하면 둘 사이에 코드 섹션을 실행할 수 있습니다.

      동시다발적인 Create() 함수 호출과 각 QService별 새로운 서비스 생성막아주는 거죠.

      이제 서비스와 관련된 클래스에 대한 설명이 끝났으니 인용문 브로드캐스팅을 위한 액스퍼트 어드바이저를 작성해 봅시다. 액스퍼트 어드바이저를 선택한 이유는 모든 틱을 처리할 수 있기 때문입니다.

      //+------------------------------------------------------------------+
      //|                                                    QExporter.mq5 |
      //|                                             Copyright GF1D, 2010 |
      //|                                             garf1eldhome@mail.ru |
      //+------------------------------------------------------------------+
      #property copyright "GF1D, 2010"
      #property link      "garf1eldhome@mail.ru"
      #property version   "1.00"
      
      #include "QService.mqh"
      //--- input parameters
      input string  ServerName = "mt5";
      
      QService* service;
      
      //+------------------------------------------------------------------+
      //| Expert initialization function                                   |
      //+------------------------------------------------------------------+
      int OnInit()
      {
         service = new QService();
         service.Create(ServerName);
         return(0);
      }
      
      //+------------------------------------------------------------------+
      //| Expert deinitialization function                                 |
      //+------------------------------------------------------------------+
      void OnDeinit(const int reason)
      {
         service.Close();
         delete service;
         service = NULL;
      }
      
      //+------------------------------------------------------------------+
      //| Expert tick function                                             |
      //+------------------------------------------------------------------+
      void OnTick()
      {
         MqlTick tick;
         SymbolInfoTick(Symbol(), tick);
         
         service.SendTick(Symbol(), tick);
      }
      //+------------------------------------------------------------------+
      

      7. EX5와 .NET 클라이언트 간 커뮤니케이션 성능 테스트하기

      클라이언트 터미널에서 바로 인용문이 수신되면 서비스의 성능이 분명 저하되겠죠. 그래서 한번 측정해 봤습니다. 마샬링 및 형 변환을 위한 CPU 시간이 감소되니까 성능도 분명 저하될 거라고 생각했어요.

      첫 번째 테스트에 이용한 스크립트와 동일한 스크립트를 작성했습니다. 다음과 같은 Start() 함수를 만들었죠.

         QService* serv = new QService();
         serv.Create("mt5");
      
         MqlTick tick;
         SymbolInfoTick("GBPUSD", tick);
       
         int total = 0;
         
         for(int c = 0; c < 10; c++)
         {
            int calls = 0;
            
            int ticks = GetTickCount();
      
            while(GetTickCount() - ticks < 1000)
            {
               for(int i = 0; i < 100; i++) serv.SendTick("GBPUSD", tick);
               calls++;
            }
            
            Print(calls * 100," calls per second");
            
            total += calls * 100;
         }
           
         Print("Average ", total / 10," calls per second");
      
         serv.Close();
         delete serv;
      

      다음의 결과를 얻었고요.

      1900  calls per second
      2400  calls per second
      2100  calls per second
      2300  calls per second
      2000  calls per second
      2100  calls per second
      2000  calls per second
      2100  calls per second
      2100  calls per second
      2100  calls per second
      Average  2110  calls per second
      

      초당 2500틱 vs 초당 1900틱 이 중 25%는 MT5의 서비스를 이용하는 데 사용되지만 그래도 이 정도면 충분합니다. 스레드풀과 동적 메소드인 System.Threading.ThreadPool.QueueUserWorkItem를 사용하면 성능이 향상된다니 흥미롭네요..

      이 메소드를 이용해 전송 속도를 초당 10000틱까지 끌어올릴 수 있었습니다. 하지만 가비지 수집기가 객체를 삭제할 시간이 없으므로 하드 테스트에서는 불안정하게 나타났습니다. MetaTrader가 할당한 메모리가 빠르게 증가했다가 결국은 충돌하고 맙니다. 하지만 하드 테스트는 현실과는 거리가 있으니 스레드풀 사용은 안전합니다.

       8. 실시간 테스트

      서비스를 이용하는 틱 테이블의 예제를 만들었는데요. 해당 프로젝트는 아카이브에 WindowsClient라는 이름으로 추가되어 있습니다. 아래는 예제 실행 결과입니다.

      그림 1. 인용문 테이블이 있는 WindowsClient 애플리케이션의 메인 윈도우

      결론

      이번에는 .NET 애플리케이션으로 인용문을 내보내는 메소드 중 하나를 설명했습니다. 필요한 모든 것이 구현되었으며 이제 여러분의 애플리케이션에서 클래스를 사용할 수 있죠. 조금 불편한 점은 필요한 차트마다 스크립트를 추가하는 건데요.

      MetaTrader를 이용하면 이 문제가 해결될 수 있을 것 같기는 해요. 그리고 모든 인용문이 필요한 게 아니라면 필요한 심볼에 대한 인용문을 브로드캐스트하는 스크립트를 이용하면 되겠죠. 아시다시피 시장 심도 브로드캐스팅이나 양면 액세스 또한 같은 방식으로 수행될 수 있습니다.

      아카이브 파일

      Bin.rar-솔루션 아카이브 직접 실행해 보길 원하는 사용자들을 위한 파일입니다. 컴퓨터에 .NET 프레임워크 3.5(3.0도 될지도 모르겠네요)가 설치되어 있어야 합니다.

      Src.rar-프로젝트 전체 소스 코드 MetaEditor와 비주얼 스튜디오 2008이 필요합니다.

      QExportDemoProfile.rar- 10개의 차트에 스크립트를 추가하는 그림 1의 MetaTrader 프로필


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

      파일 첨부됨 |
      bin.rar (33.23 KB)
      src.rar (137.41 KB)
      초보자를 위한 실용적인 MQL5 디지털 필터 구현 초보자를 위한 실용적인 MQL5 디지털 필터 구현
      자동 매매 시스템 관련 포럼에서 자주 언급되는 것 중 하나가 디지털 필터입니다. 그러니 MQL5에서 사용할 수 있는 디지털 필터 표준 코드를 꼭 제공해 드려야죠. 이 글에서는 '뉴비들을 위한 MQL5 커스텀 인디케이터'에 있는 간단한 SMA 인디케이터 코드를 조금 더 복잡하지만 보편적으로 사용할 수 있는 디지털 필터로 변환하는 법을 알아보겠습니다. 본문의 내용은 직전 글과 이어집니다. 프로그래밍 오류 수정법과 텍스트 변환 방법에 대한 설명 역시 포함되어 있습니다.
      MQL5로 방출형 인디케이터 그리기 MQL5로 방출형 인디케이터 그리기
      이 글에서는 새로운 시장 조사 접근법인 방출형 인디케이터에 대해 알아보겠습니다. 방출은 서로 다른 인디케이터의 교차점을 기반으로 계산됩니다. 각각의 틱 다음에 형형색색의 점이 나타나죠. 이 점들이 모여 성운, 구름, 궤도, 직선, 포물선 등의 형태를 갖는 클러스터를 형성합니다. 클러스터의 모양에 따라 시장 가격의 변화에 영향을 미치는, 눈에는 보이지 않는 원동력을 어느 정도 감지할 수 있죠.
      MQL5: MetaTrader5로 상품선물거래위원회(CFTC) 보고서 분석하기 MQL5: MetaTrader5로 상품선물거래위원회(CFTC) 보고서 분석하기
      이 글에서는 CTFC 보고서 분석에 필요한 도구를 개발해 보겠습니다. 우리가 해결할 문제는 다음과 같습니다. 중간 계산이나 변환을 거치지 않고 CFTC 보고서 내 데이터를 곧바로 활용할 수 있도록 해주는 인디케이터를 개발하는 것이죠. 그 외에도 여러 가지로 활용할 수 있습니다. 데이터 플로팅이라든지, 다른 인디케이터의 데이터로 활용하거나, 자동 분석 스크립트에서도 사용될 수 있고, 액스퍼트 어드바이저 매매 전략에서 사용될 수도 있죠.
      인디케이터 데이터 교환: 쉬워요! 인디케이터 데이터 교환: 쉬워요!
      차트에 추가된 인디케이터 데이터에 액세스가 가능한 동시에, 데이터 복사가 불필요하고, 필요한 경우 최소한의 수정만을 거쳐 기존의 코드를 사용할 수 있으며, MQL 코드가 선호되는 환경을 제공하고 싶습니다. 물론 DLL을 사용하긴 하겠지만 C++ 문자열을 이용할 겁니다. 이 글은 다른 MQL 프로그램에서 MetaTrader 터미널로 인디케이터 버퍼를 가져올 수 있도록 하는 편리한 개발 환경 구축 방법을 설명하고 있습니다.