Adlandırılmış Kanalları kullanarak MetaTrader 5 terminalleri arasında iletişim kurmak için DLL içermeyen bir çözüm
Giriş
MetaTrader 5 terminalleri arasındaki olası iletişim yollarını bir müddet merak ettim. Amacım, tick göstergesini kullanmak ve terminallerden birinde farklı fiyat teklifi sağlayıcılardan gelen tickleri görüntülemekti.
Doğal çözüm, sabit sürücüde ayrı dosyalar kullanmaktı. Bir terminal dosyaya veri yazar ve diğeri onu okurdu. Bu yöntem, tek tek mesaj göndermek için uygun olsa da, akışlı fiyat teklifleri için en etkili yöntem gibi görünmüyor.
Daha sonra, WCF hizmetlerini kullanarak tekliflerin .NET uygulamalarına nasıl aktarılacağına dair Alexander'ın güzel bir makalesine rastladım ve bitirmek üzereyken karşıma Sergeev tarafından yazılmış başka bir makale çıktı.
Her iki makale de ihtiyaçlarımı hemen hemen karşılıyordu ancak biri Sunucu, diğeri İstemci olarak işlev gören farklı terminaller tarafından kullanılabilecek DLL içermeyen bir çözüm aradım. İnternette arama yaparken Adlandırılmış Kanalların iletişim için kullanılabileceğini öneren bir not buldum ve kanalları kullanarak İşlemler Arası İletişim için MSDN spesifikasyonunu iyice okudum
Adlandırılmış Kanalların aynı bilgisayar üzerinden veya intranet üzerinden farklı bilgisayarlar üzerinden iletişimi desteklediğini keşfettim ve bu yaklaşımı tercih etmeye karar verdim.
Bu makalede, Adlandırılmış Kanallar iletişimi tanıtılmakta ve CNamedPipes sınıfını tasarlama süreci açıklanmaktadır. Ayrıca, MetaTrader 5 terminalleri ve genel sistem verimi arasındaki tick göstergesi akışının test edilmesini de içermektedir.
1. Adlandırılmış Kanalları Kullanarak İşlemler Arası İletişim
Tipik bir kanal düşündüğümüzde, medyayı iletmek için kullanılan bir tür silindir hayal ederiz. Bu aynı zamanda bir işletim sistemindeki işlemler arası iletişim araçlarından biri için kullanılan bir terimdir. Bizim durumumuzda, veri alışverişi yapan MetaTrader 5 terminalleri gibi iki süreci birbirine bağlayan bir kanal hayal edebilirsiniz.
Kanallar anonim veya adlandırılmış olabilir. Aralarındaki iki temel fark vardır: İlki, anonim kanalların bir ağ üzerinden kullanılamaması ve ikincisi iki sürecin ilişkilendirilmesi gerektiğidir. Yani, bir süreç üst ve diğeri alt olmalıdır. Adlandırılmış kanallar bu sınırlamaya sahip değildir.
Kanalları kullanarak iletişim kurmak için, bir sunucu işlemi bilinen bir ada sahip bir kanal kurmalıdır. Kanal adı bir dizedir ve \\servername\pipe\pipename biçiminde olmalıdır. Kanallar aynı bilgisayarda kullanılıyorsa sunucu adı atlanabilir ve bunun yerine bir nokta konulabilir: \\.\pipe\pipename.
Bir kanala bağlanmaya çalışan istemcinin adını bilmesi gerekir. Terminalleri ayırt etmek için \\.\pipe\mt[account_number] ad kuralını kullanıyorum, ancak adlandırma kuralı isteğe bağlı olarak değiştirilebilir.
2. CNamedPipes sınıfını uygulama
Adlandırılmış bir kanal oluşturma ve ona bağlanmanın düşük seviyeli mekanizmasına ilişkin kısa bir açıklamayla başlayacağım. Windows işletim sistemlerinde kanalları işleyen tüm işlevler kernel32.dll kitaplığı aracılığıyla kullanılabilir. Sunucu tarafında adlandırılmış bir kanal örneği oluşturma işlevi şu şekildedir: CreateNamedPipe().
Kanal oluşturulduktan sonra, sunucu bir istemcinin bağlanmasını beklemek için ConnectNamedPipe() işlevini çağırır. Bağlantı başarılı olursa ConnectNamedPipe() işlevi sıfır olmayan bir tamsayı döndürür. Yine de, istemcinin CreateNamedPipe() işlevi çağrıldıktan sonra ve ConnectNamedPipe() işlevi çağrılmadan önce başarıyla bağlanması mümkündür. Bu durumda, ConnectNamedPipe() işlevi sıfır döndürür ve GetLastError() işlevi 535 (0X217) hatasını döndürür : ERROR_PIPE_CONNECTED.
Bir kanala yazma ve kanaldan okuma, dosya erişimiyle aynı işlevlerle sağlanır:
BOOL WINAPI ReadFile( __in HANDLE hFile, __out LPVOID lpBuffer, __in DWORD nNumberOfBytesToRead, __out_opt LPDWORD lpNumberOfBytesRead, __inout_opt LPOVERLAPPED lpOverlapped );
BOOL WINAPI WriteFile( __in HANDLE hFile, __in LPCVOID lpBuffer, __in DWORD nNumberOfBytesToWrite, __out_opt LPDWORD lpNumberOfBytesWritten, __inout_opt LPOVERLAPPED lpOverlapped );
Adlandırılmış kanallar hakkında bilgi edindikten sonra, alttaki düşük düzeyli talimatları gizlemek için CNamedPipes sınıfını tasarladım.
Artık CNamedPipes.mqh dosyasını terminalin uygun (/include) klasörüne koymak ve kaynak koduna dahil etmek ve bir CNamedPipe nesnesi bildirmek yeterlidir.
Tasarladığım sınıf, adlandırılmış kanalları işlemek için birkaç temel yöntem sunar:
Create(), Connect(), Disconnect(), Open(), Close(), WriteUnicode(), ReadUnicode(), WriteANSI(), ReadANSI(), WriteTick(), ReadTick()
Ek gereksinimlere göre sınıf daha da genişletilebilir.
Create() yöntemi, belirli bir ada sahip bir kanal oluşturmaya çalışır. Terminaller arasındaki bağlantıyı basitleştirmek için 'hesap' giriş parametresi, kanalı kullanacak bir istemcinin hesap numarasıdır.
Hesap adı girilmezse yöntem, mevcut terminalin hesap numarası ile bir kanal açmaya çalışır. Create() işlevi, kanal başarıyla oluşturulduysa true değerini döndürür.
//+------------------------------------------------------------------+ /// Create() : try to create a new instance of Named Pipe /// \param account - source terminal account number /// \return true - if created, false otherwise //+------------------------------------------------------------------+ bool CNamedPipe::Create(int account=0) { if(account==0) pipeNumber=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeNumber=IntegerToString(account); string fullPipeName=pipeNamePrefix+pipeNumber; hPipe=CreateNamedPipeW(fullPipeName, (int)GENERIC_READ|GENERIC_WRITE|(ENUM_PIPE_ACCESS)PIPE_ACCESS_DUPLEX, (ENUM_PIPE_MODE)PIPE_TYPE_RW_BYTE,PIPE_UNLIMITED_INSTANCES, BufferSize*sizeof(ushort),BufferSize*sizeof(ushort),0,NULL); if(hPipe==INVALID_HANDLE_VALUE) return false; else return true; }
Connect() yöntemi, bir istemcinin bir kanala bağlanmasını bekler. İstemci bir kanala başarıyla bağlanırsa true değerini döndürür.
//+------------------------------------------------------------------+ /// Connect() : wait for a client to connect to a pipe /// \return true - if connected, false otherwise. //+------------------------------------------------------------------+ bool CNamedPipe::Connect(void) { if(ConnectNamedPipe(hPipe,NULL)==false) return(kernel32::GetLastError()==ERROR_PIPE_CONNECTED); else return true; }
Disconnect() yöntemi, sunucunun bağlantısını bir kanaldan keser.
//+------------------------------------------------------------------+ /// Disconnect(): disconnect from a pipe /// \return true - if disconnected, false otherwise //+------------------------------------------------------------------+ bool CNamedPipe::Disconnect(void) { return DisconnectNamedPipe(hPipe); }
Open() yöntemi bir istemci tarafından kullanılmalıdır; önceden oluşturulmuş bir kanalı açmaya çalışır. Kanal açma işlemi başarılı olursa true değerini döndürür. Oluşturulan kanala herhangi bir nedenle 5 saniyelik zaman aşımı içinde bağlanamazsa veya kanal açılma işlemi başarısız olursa false değerini döndürür.
//+------------------------------------------------------------------+ /// Open() : try to open previously created pipe /// \param account - source terminal account number /// \return true - if successfull, false otherwise. //+------------------------------------------------------------------+ bool CNamedPipe::Open(int account=0) { if(account==0) pipeName=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeName=IntegerToString(account); string fullPipeName=pipeNamePrefix+pipeName; if(hPipe==INVALID_HANDLE_VALUE) { if(WaitNamedPipeW(fullPipeName,5000)==0) { Print("Pipe "+fullPipeName+" not available..."); return false; } hPipe=CreateFileW(fullPipeName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL); if(hPipe==INVALID_HANDLE_VALUE) { Print("Pipe open failed"); return false; } } return true; }
Close() yöntemi, kanal tanıtıcısını kapatır.
//+------------------------------------------------------------------+ /// Close() : close pipe handle /// \return 0 if successfull, non-zero otherwise //+------------------------------------------------------------------+ int CNamedPipe::Close(void) { return CloseHandle(hPipe); }
Sonraki altı yöntem, kanallar üzerinden okumak ve yazmak için kullanılır. İlk iki çift, dizeleri Unicode ve ANSI biçimlerinde işler, her ikisi de terminaller arasında komut veya mesaj göndermek için kullanılabilir.
MQL5'teki dize değişkeni, Unicode içeren bir nesne olarak depolanır; bu nedenle doğal yol Unicode yöntemlerini sağlamaktı, ancak MQL5 UnicodeToANSI yöntemlerini sağladığı için ANSI dize iletişimini de uyguladım. Son iki yöntem, MqlTick nesnesinin adlandırılmış bir kanal aracılığıyla gönderilmesini ve alınmasını işler.
WriteUnicode() yöntemi, Unicode karakterlerden oluşan mesajı yazar. Her karakter iki bayttan oluştuğu için, bir kanala bir dizi ushort olarak gönderir.
//+------------------------------------------------------------------+ /// WriteUnicode() : write Unicode string to a pipe /// \param message - string to send /// \return number of bytes written to a pipe //+------------------------------------------------------------------+ int CNamedPipe::WriteUnicode(string message) { int ushortsToWrite, bytesWritten; ushort UNICODEarray[]; ushortsToWrite = StringToShortArray(message, UNICODEarray); WriteFile(hPipe,ushortsToWrite,sizeof(int),bytesWritten,0); WriteFile(hPipe,UNICODEarray,ushortsToWrite*sizeof(ushort),bytesWritten,0); return bytesWritten; }
ReadUnicode() yöntemi, ushort dizisini alır ve bir dize nesnesini döndürür.
//+------------------------------------------------------------------+ /// ReadUnicode(): read unicode string from a pipe /// \return unicode string (MQL5 string) //+------------------------------------------------------------------+ string CNamedPipe::ReadUnicode(void) { string ret; ushort UNICODEarray[STR_SIZE*sizeof(uint)]; int bytesRead, ushortsToRead; ReadFile(hPipe,ushortsToRead,sizeof(int),bytesRead,0); ReadFile(hPipe,UNICODEarray,ushortsToRead*sizeof(ushort),bytesRead,0); if(bytesRead!=0) ret = ShortArrayToString(UNICODEarray); return ret; }
WriteANSI() yöntemi, ANSI uchar dizisini bir kanala yazar.
//+------------------------------------------------------------------+ /// WriteANSI() : write ANSI string to a pipe /// \param message - string to send /// \return number of bytes written to a pipe //+------------------------------------------------------------------+ int CNamedPipe::WriteANSI(string message) { int bytesToWrite, bytesWritten; uchar ANSIarray[]; bytesToWrite = StringToCharArray(message, ANSIarray); WriteFile(hPipe,bytesToWrite,sizeof(int),bytesWritten,0); WriteFile(hPipe,ANSIarray,bytesToWrite,bytesWritten,0); return bytesWritten; }
ReadANSI() yöntemi, bir kanaldan uchar dizisini okur ve bir dize nesnesini döndürür.
//+------------------------------------------------------------------+ /// ReadANSI(): read ANSI string from a pipe /// \return unicode string (MQL5 string) //+------------------------------------------------------------------+ string CNamedPipe::ReadANSI(void) { string ret; uchar ANSIarray[STR_SIZE]; int bytesRead, bytesToRead; ReadFile(hPipe,bytesToRead,sizeof(int),bytesRead,0); ReadFile(hPipe,ANSIarray,bytesToRead,bytesRead,0); if(bytesRead!=0) ret = CharArrayToString(ANSIarray); return ret; }
WriteTick() yöntemi, bir kanala tek bir MqlTick nesnesi yazar.
//+------------------------------------------------------------------+ /// WriteTick() : write MqlTick to a pipe /// \param MqlTick to send /// \return true if tick was written correctly, false otherwise //+------------------------------------------------------------------+ int CNamedPipe::WriteTick(MqlTick &outgoing) { int bytesWritten; WriteFile(hPipe,outgoing,MQLTICK_SIZE,bytesWritten,0); return bytesWritten; }
ReadTick() yöntemi, bir kanaldan tek bir MqlTick nesnesini okur. Bir kanal boşsa 0 döndürür, boş değilse bir dizi MqlTick nesnesi baytını döndürmelidir.
//+------------------------------------------------------------------+ /// ReadTick() : read MqlTick from a pipe /// \return true if tick was read correctly, false otherwise //+------------------------------------------------------------------+ int CNamedPipe::ReadTick(MqlTick &incoming) { int bytesRead; ReadFile(hPipe,incoming,MQLTICK_SIZE,bytesRead,NULL); return bytesRead; } //+------------------------------------------------------------------+
Adlandırılmış kanalları işlemenin temel yöntemleri bilindiğine göre, iki MQL programıyla başlayabiliriz: Fiyat teklifi almak için basit bir script dosyası ve fiyat teklifi göndermek için bir gösterge.
3. Fiyat Teklifi Almak için Sunucu Script Dosyası
Örnek sunucu, adlandırılmış kanalı başlatır ve bir istemcinin bağlanmasını bekler. İstemci bağlantısını kestikten sonra, o istemci tarafından toplamda kaç tick alındığını gösterir ve yeni bir istemcinin bağlanmasını bekler. İstemci bağlantısı kesilirse ve sunucu genel bir 'gvar0' değişkeni bulursa, çıkar. 'gvar0' değişkeni yoksa, bir grafiğe sağ tıklanarak ve Expert Listesi seçeneği seçilerek sunucu manuel olarak durdurulabilir.
//+------------------------------------------------------------------+ //| NamedPipeServer.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #include <CNamedPipes.mqh> CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { bool tickReceived; int i=0; if(pipe.Create()==true) while (GlobalVariableCheck("gvar0")==false) { Print("Waiting for client to connect."); if (pipe.Connect()==true) Print("Pipe connected"); while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==false) { if(GetError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.Name()); pipe.Disconnect(); break; } } else i++; Print(IntegerToString(i) + "ticks received."); } while(tickReceived==true); if (i>0) { Print(IntegerToString(i) + "ticks received."); i=0; }; if(GlobalVariableCheck("gvar0")==true || (GetError()==ERROR_BROKEN_PIPE)) break; } } pipe.Close(); }
4. Fiyat Teklifi Göndermek İçin Basit Gösterge
Fiyat teklifi gönderme göstergesi, OnInit() yöntemi içinde bir kanal açar ve OnCalculate() yöntemi her tetiklendiğinde tek bir MqlTick gönderir://+------------------------------------------------------------------+ //| SendTickPipeIndicator.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property indicator_chart_window #include <CNamedPipes.mqh> CNamedPipe pipe; int ctx; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { while (!pipe.Open(AccountInfoInteger(ACCOUNT_LOGIN))) { Print("Pipe not created, retrying in 5 seconds..."); if (GlobalVariableCheck("gvar1")==true) break; } ctx = 0; return(0); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { ctx++; MqlTick outgoing; SymbolInfoTick(Symbol(), outgoing); pipe.WriteTick(outgoing); Print(IntegerToString(ctx)+" tick send to server by SendTickPipeClick."); return(rates_total); } //+------------------------------------------------------------------+
5. Tek İstemci Terminalinde Birden Çok Sağlayıcıdan Tick Göstergelerini Çalıştırma
Gelen fiyat tekliflerini ayrı tick göstergelerinde görüntülemek istediğim için durum daha da karmaşık bir hal aldı. Bunu, EventChartCustom() yöntemini tetikleyerek, gelen tickleri tick göstergesine yayınlayan kanal sunucusunu uygulayarak başardım.
Alış ve satış fiyat teklifleri, noktalı virgülle bölünmüş tek bir dize olarak gönderilir, ör. '1.20223;120225'. Uygun gösterge, OnChartEvent() içindeki özel bir olayı işler ve bir tick grafiği görüntüler.
//+------------------------------------------------------------------+ //| NamedPipeServerBroadcaster.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property script_show_inputs #include <CNamedPipes.mqh> input int account = 0; CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { bool tickReceived; int i=0; if(pipe.Create(account)==true) while(GlobalVariableCheck("gvar0")==false) { if(pipe.Connect()==true) Print("Pipe connected"); i=0; while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else { i++; Print(IntegerToString(i)+" ticks received BY server."); string bidask=DoubleToString(pipe.incoming.bid)+";"+DoubleToString(pipe.incoming.ask); long currChart=ChartFirst(); int chart=0; while(chart<100) { EventChartCustom(currChart,6666,0,(double)account,bidask); currChart=ChartNext(currChart); if(currChart==0) break; // Reached the end of the charts list chart++; } if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; } } while(tickReceived==true); if(i>0) { Print(IntegerToString(i)+"ticks received."); i=0; }; if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; Sleep(100); } } pipe.Close(); }
Tickleri görüntülemek için MQLmagazine içine yerleştirilmiş tick göstergesini seçtim fakat OnCalculate() yönteminin yerine OnChartEvent() içinde işleme uyguladım ve koşullu talimatlar ekledim. Yalnızca dparam parametresi kanal numarasına eşitse ve olay kimliği CHARTEVENT_CUSTOM+6666'ya eşitse, işleme için bir fiyat teklifi kabul edilir:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (dparam==(double)incomingPipe) if(id>CHARTEVENT_CUSTOM) { if(id==CHARTEVENT_CUSTOM+6666) { // Process incoming tick } } else { // Handle the user event } }
Aşağıdaki ekran görüntüsünde üç tick göstergesi vardır.
Bunlardan ikisi kanallardan alınan tickleri göstermekte olup ticklerin kaybolmadığını kontrol etmek için, kanalları kullanmayan üçüncü bir gösterge çalıştırıldı.
Şek. 1 Adlandırılmış bir kanal aracılığıyla alınan fiyat teklifleri
Göstergeleri nasıl çalıştırdığıma ilişkin yorumları içeren ekran görüntüsünü ekte bulabilirsiniz:
Şek. 2 Gösterge kurulumunu açıklayan ekran kaydı
6. Sistem Verimliliğini Test Etme
Kanallar paylaşılan belleği kullandığı için iletişim çok hızlıdır. İki MetaTrader 5 terminali arasında art arda 100 000 ve 1 000 000 tick gönderme testi yaptım. Gönderen script dosyası WriteTick() işlevini kullanır ve GetTickCount() kullanarak zaman aralığını ölçer:
Print("Sending..."); uint start = GetTickCount(); for (int i=0;i<100000;i++) pipe.WriteTick(outgoing); uint stop = GetTickCount(); Print("Sending took" + IntegerToString(stop-start) + " [ms]"); pipe.Close();
Sunucu, gelen fiyat tekliflerini okur. Zaman aralığı, ilk gelen fiyat teklifinden istemci bağlantısı kesilene kadar ölçülür:
//+------------------------------------------------------------------+ //| SpeedTestPipeServer.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property script_show_inputs #include <CNamedPipes.mqh> input int account=0; bool tickReceived; uint start,stop; CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { int i=0; if(pipe.Create(account)==true) if(pipe.Connect()==true) Print("Pipe connected"); do { tickReceived=pipe.ReadTick(); if(i==0) start=GetTickCount(); if(tickReceived==false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else i++; } while(tickReceived==true); stop=GetTickCount(); if(i>0) { Print(IntegerToString(i)+" ticks received."); i=0; }; pipe.Close(); Print("Server: receiving took "+IntegerToString(stop-start)+" [ms]"); } //+------------------------------------------------------------------+
10 örnek çalışmasının sonuçları şu şekildedir:
Çalışma | Fiyat Teklifleri | Gönderme süresi [ms] | Alma süresi [ms] |
---|---|---|---|
1 | 100000 | 624 | 624 |
2 | 100000 | 702 | 702 |
3 | 100000 | 687 | 687 |
4 | 100000 | 592 | 608 |
5 | 100000 | 624 | 624 |
6 | 1000000 | 5616 | 5616 |
7 | 1000000 | 5788 | 5788 |
8 | 1000000 | 5928 | 5913 |
9 | 1000000 | 5772 | 5756 |
10 | 1000000 | 5710 | 5710 |
Tablo 1 Verimlilik hızı ölçümleri
1 000 000 fiyat teklifi göndermenin ortalama hızı, 2,0GHz T4200 CPU ve 3GB RAM ile Windows Vista çalıştıran bir dizüstü bilgisayarda yaklaşık 170 000 tick/saniye idi.
Sonuç
Adlandırılmış Kanalları kullanarak MetaTrader 5 terminalleri arasında bir iletişim yöntemi sundum. Yöntemin, terminaller arasında gerçek zamanlı fiyat teklifleri göndermek için yeterli olduğu ortaya çıktı.
CNamedPipes sınıfı, örneğin iki bağımsız hesapta riskten korunmayı mümkün kılmak için ek gereksinimlere göre daha da genişletilebilir. CNamedPipe sınıfı kaynak kodunu chm biçiminde belgelerle ve makaleyi yazmak için uyguladığım diğer kaynak kodlarını ekte bulabilirsiniz.
MetaQuotes Ltd tarafından İngilizceden çevrilmiştir.
Orijinal makale: https://www.mql5.com/en/articles/115
- Ücretsiz ticaret uygulamaları
- İşlem kopyalama için 8.000'den fazla sinyal
- Finansal piyasaları keşfetmek için ekonomik haberler
Gizlilik ve Veri Koruma Politikasını ve MQL5.com Kullanım Şartlarını kabul edersiniz