Come Esportare Quotazioni da МetaTrader 5 ad Applicazioni .NET Utilizzando i Servizi WCF
Introduzione
I programmatori che utilizzano il servizio DDE in MetaTrader 4 probabilmente hanno sentito che nella quinta versione non è più supportato. E non esiste una soluzione standard per l'esportazione delle quotazioni. Come soluzione a questo problema, gli sviluppatori MQL5 suggeriscono di utilizzare la propria dll, che la implementa. Quindi, se dobbiamo scrivere l'implementazione, facciamolo in modo intelligente!Perché .NET?
Per me con la mia lunga esperienza di programmazione in .NET, sarebbe più sensato, interessante e semplice implementare l'esportazione delle quotazioni utilizzando questa piattaforma. Sfortunatamente, non c'è alcun supporto nativo di .NET in MQL5 nella quinta versione. Sono sicuro che gli sviluppatori hanno alcune ragioni per questo. Pertanto, utilizzeremo la dll win32 come wrapper per il supporto .NET.
Perché WCF?
La Windows Communication Foundation Technology (WCF) è stata scelta da me per diversi motivi: da un lato, è facile da estendere e adattare, dall'altro volevo verificarla con un duro lavoro. Inoltre, secondo Microsoft, WCF ha prestazioni leggermente superiori rispetto a .NET Remoting.
Requisiti di Sistema
Pensiamo a cosa vogliamo dal nostro sistema. Penso che ci siano due requisiti principali:
- Ovviamente bisogna esportare i tick, meglio utilizzando la struttura nativa MqlTick;
- È preferibile conoscere l'elenco dei simboli attualmente esportati.
Iniziamo...
1. Classi generali e contratti
Prima di tutto, creiamo una nuova libreria di classi e la chiamiamo QExport.dll. Definiamo la struttura MqlTick come 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; } }
Poi definiremo i contratti del servizio. Non mi piace usare le classi di configurazione e le classi proxy generate, quindi non incontrerai tali funzionalità qui.
Definiamo il primo contratto server in base ai requisiti descritti sopra:
[ServiceContract(CallbackContract = typeof(IExportClient))] public interface IExportService { [OperationContract] void Subscribe(); [OperationContract] void Unsubscribe(); [OperationContract] String[] GetActiveSymbols(); }
Come notiamo, esiste uno schema standard di iscrizione e annullamento dell'iscrizione alle notifiche del server. Si riportano di seguito i brevi dettagli delle operazioni:
Operazione | Descrizione |
---|---|
Abbonati() | Iscriviti all'esportazione di tick |
Annulla abbonamento() | Annulla l'iscrizione all'esportazione di tick |
GetActiveSymbols() | Restituisce l'elenco dei simboli esportati |
E le seguenti informazioni dovrebbero essere inviate al callback del cliente: la citazione stessa e la notifica sui cambiamenti dei lits dei simboli esportati. Definiamo le operazioni necessarie come "Operazioni One Way" per aumentare le prestazioni:
[ServiceContract] public interface IExportClient { [OperationContract(IsOneWay = true)] void SendTick(String symbol, MqlTick tick); [OperationContract(IsOneWay = true)] void ReportSymbolsChanged(); }
Operazione | Descrizione |
---|---|
SendTick(String, MqlTick) | Invia tick |
ReportSymbolsChanged() | Notifica al cliente le modifiche nell'elenco dei simboli esportati |
2. Implementazione del server
Creiamo una nuova build con nome Qexport.Service.dll per il servizio con l'implementazione del contratto del server.
Scegliamo il NetNamedPipesBinding per un'associazione, perché ha le prestazioni più elevate rispetto agli attacchi standard. Se abbiamo bisogno di trasmettere quotazioni, ad esempio su una rete, bisognerebbe usare il NetTcpBinding.
Di seguito sono riportati alcuni dettagli dell'implementazione del contratto del server:
La definizione di classe Prima di tutto, dovrebbe essere contrassegnato con l'attributo ServiceBehavior con i seguenti modificatori:
- InstanceContextMode = InstanceContextMode.Single - fornire l'utilizzo di un'istanza del servizio per tutte le richieste elaborate, aumenterà le prestazioni della soluzione. Inoltre, avremo la possibilità di servire e gestire l'elenco dei simboli esportati;
- ConcurrencyMode = ConcurrencyMode.Multiple - indica l'elaborazione parallela per tutte le richieste del client;
- UseSynchronizationContext = false – significa che non ci colleghiamo al thread della GUI per prevenire situazioni di blocco. Non è necessario qui per il nostro compito, ma è necessario se vogliamo ospitare il servizio utilizzando le applicazioni Windows.
- IncludeExceptionDetailInFaults = true – per includere i dettagli dell'eccezione all'oggetto FaultException quando passato al cliente.
Lo stesso ExportService contiene due interfacce: IExportService, IDisposable. Il primo implementa tutte le funzioni di servizio, il secondo implementa il modello standard di rilascio delle risorse .NET.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false, IncludeExceptionDetailInFaults = true)] public class ExportService : IExportService, IDisposable {
Descriviamo le variabili del servizio:
// 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();
Definiamo i metodi Open() e Close(), che aprono e chiudono il nostro servizio:
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; } // ... }
Successivamente, l'implementazione dei metodi 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(); }
Ora abbiamo bisogno di aggiungere metodi per inviare tick e per registrare ed eliminare i simboli esportati.
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--; } }
Riassumiamo l'elenco delle principali funzioni del server (solo quelle di cui abbiamo bisogno):
Metodi | Descrizione |
---|---|
Open() | Esegue il server |
Close() | Arresta il server |
RegisterSymbol(String) | Aggiunge il simbolo all'elenco dei simboli esportati |
UnregisterSymbol (String) | Elimina il simbolo dall'elenco dei simboli esportati |
GetActiveSymbols() | Restituisce il numero di simboli esportati |
SendTick(String, MqlTick) | Invia tick ai clienti |
3. Implementazione del cliente
Abbiamo considerato il server, penso che sia chiaro, quindi è il momento di considerare il client. Creiamo il Qexport.Client.dll. Il contratto con il cliente sarà implementato lì. Innanzitutto, dovrebbe essere contrassegnato con l'attributo CallbackBehavior, che ne definisce il comportamento. Ha i seguenti modificatori:
- ConcurrencyMode = ConcurrencyMode.Multiple - indica l'elaborazione parallela per tutti i callback e le risposte del server. Questo modificatore è molto importante. Immagina che il server voglia notificare al client le modifiche nell'elenco dei simboli esportati chiamando il callback ReportSymbolsChanged(). E il client (nella sua callback) vuole ricevere la nuova lista dei simboli esportati chiamando il metodo del server GetActiveSymbols(). Quindi risulta che il client non può ricevere risposta dal server perché sta procedendo alla richiamata con l'attesa della risposta del server. Di conseguenza il client cadrà a causa del timeout.
- UseSynchronizationContext = false - specifica che non ci colleghiamo alla GUI per prevenire situazioni di blocco. Di default, i callback wcf sono allegati al thread padre. Se il thread padre ha una GUI, la situazione è possibile quando la richiamata attende il completamento del metodo da cui è stata chiamata, ma il metodo non può finire perché attende la fine della richiamata. È qualcosa di simile al caso precedente, anche se si tratta di due cose diverse.
Per quanto riguarda il caso del server, anche il client implementa due interfacce: IExportClient and IDisposable:
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)] public class ExportClient : IExportClient, IDisposable {
Descriviamo le variabili del servizio:
// 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; } }
Ora creeremo eventi per i nostri metodi di callback. È necessario che l'applicazione client sia in grado di iscriversi agli eventi e ricevere notifiche sui cambiamenti dello stato del client.
// calls when tick received public event EventHandler<TickRecievedEventArgs> TickRecieved; // call when symbol list has changed public event EventHandler ActiveSymbolsChanged;
Definire anche i metodi Open() e Close() per il client:
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; } // ... }
Nota che la connessione e la disconnessione dai feed vengono chiamate quando un client viene aperto o chiuso, quindi non è necessario chiamarli direttamente.
E ora, scriviamo il contratto del cliente. La sua implementazione porta alla generazione dei seguenti eventi:
public void SendTick(string symbol, MqlTick tick) { // firing event TickRecieved } public void ReportSymbolsChanged() { // firing event ActiveSymbolsChanged }
Infine, le principali proprietà e modalità del client sono definite come segue:
Property | Descrizione |
---|---|
Servizio | Canale di comunicazione di servizio |
Canale | Istanza del contratto di servizio IExportService |
Metodo | Descrizione |
---|---|
Open() | Si connette al server |
Close() | Si disconnette dal server |
Evento | Descrizione |
---|---|
TickRecieved | Generato dopo la ricezione del nuovo preventivo |
ActiveSymbolsChanged | Generato dopo le modifiche nell'elenco dei simboli attivi |
4. Velocità di trasferimento tra due applicazioni .NET
È stato interessante per me misurare la velocità di trasferimento tra due applicazioni .NET, infatti è il throughput, che viene misurato in tick al secondo. Ho scritto diverse applicazioni console per misurare le prestazioni del servizio: la prima è per il server, la seconda è per il client. Ho scritto il seguente codice nella funzione Main() del server:
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();
Come si vede, il codice esegue dieci misurazioni del throughput. Ho ottenuto i seguenti risultati del test sul mio Athlon 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 tick al secondo - Penso che sia sufficiente esportare quotazioni per 100 simboli (ovviamente, virtualmente, perché sembra che nessuno voglia aprire così tanti grafici e allegare esperti =)) Inoltre, con l'aumento del numero di clienti, il numero massimo di i simboli esportati per ogni cliente sono ridotti.
5. Creare uno "strato"
Ora è il momento di pensare a come collegarlo al terminale client. Vediamo cosa abbiamo alla prima chiamata della funzione in MetaTrader 5: l'ambiente di runtime .NET (CLR) viene caricato nel processo e il dominio dell'applicazione viene creato per impostazione predefinita. È interessante che non venga scaricato dopo l'esecuzione del codice.
L'unico modo per scaricare CLR dal processo è terminarlo (chiudi il terminale client), che costringerà Windows a cancellare tutto le risorse del processo. Quindi, possiamo creare i nostri oggetti e esisteranno fino a che il dominio dell'applicazione non viene aggiunto o finché non viene distrutto da Garbage Collector.
Puoi dire che sembra buono, ma anche se impediamo la distruzione dell'oggetto da parte di Garbage Collector, non possiamo essere in grado di accedere agli oggetti da MQL5. Fortunatamente, tale accesso può essere organizzato facilmente. Il trucco è il seguente: per ogni dominio applicativo esiste una tabella di handle di Garbage Collector (tabella di handle GC), che viene utilizzata dall'applicazione per tenere traccia della durata dell'oggetto e consente di gestirlo manualmente.
L'applicazione aggiunge ed elimina elementi dalla tabella utilizzando il tipo System.Runtime.InteropServices.GCHandle. Tutto ciò di cui abbiamo bisogno è avvolgere il nostro oggetto con un tale descrittore e abbiamo accesso ad esso attraverso la proprietà GCHandle.Target. Quindi possiamo ottenere il riferimento all'oggetto GCHandle, che è nella tabella degli handle ed è garantito che non verrà spostato o eliminato da Garbage Collector. L'oggetto avvolto eviterà anche il riciclaggio a causa del riferimento per descrittore.
Ora è il momento di testare la teoria nella pratica. Per farlo, creiamo una nuova dll win32 con nome QExpertWrapper.dll e aggiungiamo il supporto CLR, System.dll, QExport.dll, Qexport.Service.dll al riferimento di build. Inoltre, creiamo una classe ausiliaria ServiceManaged per scopi di gestione: per eseguire il marshalling, ricevere oggetti tramite handles, ecc.
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); };
Consideriamo l'implementazione di questi metodi. Il metodo CreateExportService crea il servizio, lo avvolge in GCHandle utilizzando GCHandle.Alloc e ne restituisce il riferimento. Se qualcosa va storto, mostra un MessageBox con un errore. L'ho usato per il debug, quindi non sono sicuro che sia davvero necessario, ma l'ho lasciato qui per ogni evenienza.
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"); } }
Il metodoDestroyExportService ottiene il puntatore al GCHandle del servizio, ottiene il servizio dalla proprietà Target e chiama il suo metodo Close(). È importante rilasciare l'oggetto di servizio chiamando il suo metodo Free(). Altrimenti rimarrà in memoria, il Garbage Collector non lo rimuoverà.
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"); } }
Il metodo RegisterSymbol aggiunge un simbolo all'elenco dei simboli esportati:
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"); } }
Il metodo UnregisterSymbolelimina un simbolo dall'elenco:
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"); } }
E ora il metodo SendTick. Come si vede, il puntatore viene trasformato nella struttura MqlTick utilizzando la classe Marshal. Un altro punto: non c'è alcun codice nel blocco catch - è fatto per evitare i ritardi della coda di tick generale in caso di errore.
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 (...) { } }
Consideriamo l'implementazione delle funzioni, che verranno chiamate dai nostri programmi 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)); }
Il codice è pronto, ora dobbiamo compilarlo e costruirlo. Specifichiamo la directory di output come "C:\Program Files\MetaTrader 5\MQL5\Libraries" nelle opzioni del progetto. Dopo la compilazione appariranno tre librerie nella cartella specificata.
Il programma mql5 ne utilizza solo uno, ovvero QExportWrapper.dll, altre due librerie vengono utilizzate da esso. Per questo motivo abbiamo bisogno di mettere le librerie Qexport.dll e Qexport.Service.dll nella cartella principale di MetaTrader. Non è conveniente.
La soluzione è creare un file di configurazione e specificare il percorso per le librerie lì. Creiamo il file con nome terminal.exe.confignella cartella principale di MetaTrader e scriviamoci le seguenti stringhe:
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="mql5\libraries" /> </assemblyBinding> </runtime> </configuration>
È pronto. Ora CLR cercherà le librerie nella cartella che abbiamo specificato.
6. Implementazione della parte server in MQL5
Infine, siamo giunti alla programmazione della parte server in mql5. Creiamo un nuovo file QService.mqh e definiamo le funzioni importate di 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
È fantastico che mql5 abbia classi perché è una funzionalità ideale per incapsulare tutte le logiche all'interno, che semplifica notevolmente il lavoro e la comprensione del codice. Pertanto progettiamo una classe che sarà una shell per i metodi della libreria.
Inoltre, per evitare la situazione con la creazione di un servizio per ogni simbolo, organizziamo il controllo del servizio funzionante con tale nome, e lo lavoreremo in tal caso. Un metodo ideale per fornire queste informazioni sono le variabili globali, per i seguenti motivi:
- le variabili globali scompaiono dopo la chiusura del terminale client. Lo stesso è con il servizio;
- possiamo servire il numero di oggetti Qservice, che utilizza il servizio. Consente di chiudere il servizio fisico solo dopo la chiusura dell'ultimo oggetto.
Quindi, creiamo una classe 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"); }
La classe contiene i seguenti metodi:
Metodo | Descrizione |
---|---|
Crea (stringa costante) | Avvia il servizio |
Close() | Chiude il servizio |
SendTick(const string, MqlTick&) | Invia preventivo |
Si noti inoltre che i metodi privati EnterCriticalSection() e LeaveCriticalSection() consentono di eseguire le sezioni di codice critiche tra di essi.
Ci solleverà dai casi del simultaneo chiamate della funzione Create() e creazione di nuovi servizi per ciascuno QService.
Quindi, abbiamo descritto la classe per lavorare con il servizio, ora scriviamo un Expert Advisor per la trasmissione delle quotazioni. L'Expert Advisor è stato scelto per la sua possibilità di processare tutti i tick arrivati.
//+------------------------------------------------------------------+ //| 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. Test delle prestazioni di comunicazione tra ex5 e client .NET
È evidente che le prestazioni complessive del servizio diminuiranno se i preventivi arriveranno direttamente dal terminale del cliente, quindi mi interessa misurarlo. Ero sicuro che avrebbe dovuto diminuire a causa dell'inevitabile perdita di tempo della CPU per il marshalling e il typecast.
A questo scopo ho scritto un semplice script che è lo stesso del primo test. La funzione Start() ha il seguente aspetto:
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;
Ho i seguenti risultati:
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 tick/sec contro 1900 tick/sec. Il 25% è il prezzo che dovrebbe essere pagato per l'utilizzo dei servizi da MT5, ma comunque è sufficiente. È interessante notare che le prestazioni possono essere aumentate utilizzando il pool di thread e il metodo statico System.Threading.ThreadPool.QueueUserWorkItem.
Usando questo metodo, ho ottenuto la velocità di trasferimento fino a 10000 tick al secondo. Ma il suo lavoro in un duro test è stato instabile a causa del fatto che il Garbage Collector non ha tempo per eliminare gli oggetti - di conseguenza la memoria, allocata da MetaTrader, cresce rapidamente e alla fine si blocca. Ma è stato un duro test, lontano dal reale, quindi non c'è niente di pericoloso nell'usare il pool di thread.
8. Test in tempo reale
Ho creato un esempio di tabella tick utilizzando il servizio. Il progetto è allegato nell'archivio e denominato WindowsClient. Il risultato del suo lavoro è presentato di seguito:
Fig. 1. Finestra principale dell'applicazione WindowsClient con la tabella delle quotazioni
Conclusione
In questo articolo ho descritto uno dei metodi per esportare le quotazioni in applicazioni .NET. Tutto il necessario è stato implementato e ora abbiamo classi pronte che possono essere utilizzate nelle tue applicazioni. L'unica cosa che non è conveniente allegare script a ciascuno dei grafici necessari.
Al momento penso che questo problema possa essere risolto utilizzando i profili MetaTrader. Dall'altro lato, se non hai bisogno di tutte le virgolette, puoi organizzarlo con uno script che trasmette le virgolette per i simboli necessari. Come capisci, la trasmissione di profondità di mercato o anche l'accesso bilaterale possono essere organizzati allo stesso modo.
Descrizione degli archivi:
Bin.rar - archivio con una soluzione pronta. Per gli utenti che vogliono vedere come funziona. Nota inoltre che .NET Framework 3.5 (forse funzionerà anche con la versione 3.0) dovrebbe essere installato sul tuo computer.
Src.rar - codice sorgente completo del progetto. Per lavorare con esso avrai bisogno di MetaEditor e Visual Studio 2008.
QExportDemoProfile.rar- Profilo Metatrader, che collega lo script a 10 grafici, come mostrato in Fig. 1.
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/27
- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso