
WCF 서비스를 통해 MetaTrader5에서 .NET 애플리케이션으로 인용문 내보내기
개요
MetaTrader4에서 DDE 서비스를 사용하던 프로그래머께서는 더이상 지원되지 않는다는 걸 아마 아실 거예요. 인용문을 내보내는 정해진 방법도 마땅히 없고요. 이에 대한 해결책으로 MQL5 개발자들은 여러분이 이를 구현하는 자체 dll을 사용할 것을 제안합니다. 어차피 구현해야 된다면 머리를 잘 굴려보자고요!왜 .NET인가?
저는 오랜 시간 .NET으로 프로그래밍을 했는데요. 이 플랫폼을 이용하면 인용문 내보내기가 훨씬 합리적이고, 재밌고, 간단하게 구현될 수 있겠더라고요. 안타깝게도 MQL5는 .NET 형식을 지원하지 않습니다. 개발자들도 다 이유가 있었겠죠. 그렇기 때문에 .NET 형식을 지원하는 win32 dll을 이용하도록 하겠습니다.
왜 WCF인가?
윈도우 커뮤니케이션 파운데이션(WCF)을 선택한 이유는요. 우선 확장과 적응이 쉬워서 입니다. 그리고 어려운 작업으로 시험해 보고 싶기도 했고요. 게다가, 마이크로소프트에 의하면 WCF가 .NET 리모팅에 비해 성능이 좋다더군요.
시스템 요건
우리 시스템에 어떤게 필요할지 생각해 보죠. 제 생각에는 크게 두 가지 요건으로 나뉠 것 같아요.
- 기본 구조인 MqlTick을 이용해 틱을 내보내는 편이 낫겠죠.
- 이미 내보내진 심볼 리스트를 아는 것도 좋을테고요.
시작합시다.
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.dll 및 Qexport.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



