Comment exporter des cotations de МetaTrader 5 vers des applications .NET à l'aide des services WCF
Introduction
Les programmeurs qui utilisent le service DDE dans MetaTrader 4 ont probablement entendu dire que dans la cinquième version, n'est plus prise en charge. Et il n'y a pas de solution standard pour exporter des cotations En solution à ce problème, les développeurs MQL5 suggèrent d'utiliser votre propre dll, qui l'implémente. Donc, si nous devons écrire l'implémentation, faisons-le intelligemment !Pourquoi .NET ?
Pour moi avec ma longue expérience de programmation en .NET, il serait plus raisonnable, intéressant et simple d'implémenter l'export de cotations en utilisant cette plateforme. Malheureusement, il n'y a pas de support natif de .NET dans MQL5 en cinquième version. Je suis sûr que les développeurs ont des raisons à cela. Par conséquent, nous utiliserons la dll win32 comme emballeur pour la prise en charge de .NET.
Pourquoi WCF ?
La Technologie Windows Communication Foundation (WCF) a été choisie par moi pour plusieurs raisons : d'une part, elle est facile à étendre et à adapter, d'autre part, je souhaitais la vérifier sous un travail dur. De plus, selon Microsoft, WCF affiche un peu plus de performances que .NET Remoting.
Exigences du Système
Pensons à ce que nous voulons de notre système. Je pense qu'il y a deux exigences principales:
- Bien sûr, nous devons exporter les ticks, en utilisant mieux la structure native MqlTick ;
- Il est préférable de connaître la liste des symboles actuellement exportés.
Commençons...
1. Classes générales et contrats
Tout d'abord, créons une nouvelle bibliothèque de classes et nommons-la QExport.dll.. Nous définissons la structure MqlTick comme un 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; } }
Ensuite, nous définirons les contrats de service. Je n'aime pas utiliser les classes de configuration et les classes proxy générées, vous ne rencontrerez donc pas de telles fonctionnalités ici.
Définissons le premier contrat de serveur en fonction des exigences décrites ci-dessus :
[ServiceContract(CallbackContract = typeof(IExportClient))] public interface IExportService { [OperationContract] void Subscribe(); [OperationContract] void Unsubscribe(); [OperationContract] String[] GetActiveSymbols(); }
Comme nous le constatons, il existe un schéma standard d'abonnement et de désabonnement des notifications du serveur. Les brefs détails des opérations sont décrits ci-dessous :
Opération | Description |
---|---|
S’abonner() | Abonnez-vous à l'exportation des tiques |
Désabonner() | Se désabonner de l'export des ticks |
GetActiveSymbols() | Renvoie la liste des symboles exportés |
Et les informations suivantes doivent être envoyées au rappel client : la cotation elle-même et la notification portant sur les modifications des lits de symboles exportés. Définissons les opérations requises comme des « opérations à sens unique » pour augmenter les performances :
[ServiceContract] public interface IExportClient { [OperationContract(IsOneWay = true)] void SendTick(String symbol, MqlTick tick); [OperationContract(IsOneWay = true)] void ReportSymbolsChanged(); }
Opération | Description |
---|---|
SendTick(String, MqlTick) | Envoie la tique |
ReportSymbolsChanged() | Informer le client des modifications apportées à la liste des symboles exportés |
2. Implémentation du serveur
Créons une nouvelle version avec le nom Qexport.Service.dll pour le service avec l'implémentation du contrat de serveur.
Choisissons le NetNamedPipesBinding pour une liaison, car il dispose de la plus grande performance par rapport aux fixations standard. Si l'on a besoin de diffuser des cotations, sur un réseau par exemple, le NetTcpBinding Devrait être utilisé.
Voici quelques détails de l’implémentation du contrat de serveur :
La définition de classe. Tout d'abord, il doit être marqué avec l'attribut ServiceBehavior avec les modificateurs suivants :
- InstanceContextMode = InstanceContextMode..Single - de fournir l'utilisation d'une instance de service pour toutes les demandes traitées, cela augmentera les performances de la solution. De plus, nous aurons la possibilité de servir et de gérer la liste des symboles exportés ;
- ConcurrencyMode = ConcurrencyMode..Multiple - indique le traitement parallèle de toutes les requêtes du client ;
- UseSynchronizationContext = faux – signifie que nous ne nous attachons pas au thread de l'interface graphique pour éviter les situations de blocage. Ce n'est pas nécessaire ici pour notre tâche, mais c'est nécessaire si nous souhaitons héberger le service à l'aide des applications Windows.
- IncludeExceptionDetailInFaults = vrai – pour inclure les détails de l'exception à l'objet FaultException lorsqu'il est transmis au client.
Le service d'exportation lui-même comporte deux interfaces : IExportService, IJetable. Le premier implémente toutes les fonctions de service, le second implémente le modèle standard de libération des ressources .NET.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false, IncludeExceptionDetailInFaults = true)] public class ExportService : IExportService, IDisposable {
Décrivons les variables du service :
// 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();
Définissons les méthodes Open() et Close(), qui ouvrent et clôturent notre service :
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; } // ... }
Ensuite, l'implémentation des méthodes 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(); }
Nous devons maintenant ajouter des méthodes pour envoyer des ticks et pour enregistrer et supprimer les symboles exportés.
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--; } }
Récapitulons la liste des principales fonctions du serveur (uniquement celles dont nous avons besoin) :
Méthodes | Description |
---|---|
Open() | Exécute le serveur |
Close() | Arrête le serveur |
RegisterSymbol(String) | Ajoute le symbole à la liste des symboles exportés |
UnregisterSymbol(String) | Supprime le symbole de la liste des symboles exportés |
GetActiveSymbols() | Renvoie le nombre de symboles exportés |
SendTick(String, MqlTick) | Envoie la coche aux clients |
3. Implémentation client
Nous avons pris en compte le serveur, je pense que c'est clair, il est donc temps de s’intéresser au client. Créons Qexport.Client.dll. Le contrat client y sera implémenté Tout d'abord, il doit être marqué avec l'attribut CallbackBehavior, qui définit son comportement. Il a les modificateurs suivants :
- ConcurrencyMode = ConcurrencyMode.Multiple - indique le traitement parallèle de tous les rappels et réponses du serveur. Ce modificateur est très important. Imaginez que ce serveur souhaite informer le client des modifications apportées à la liste des symboles exportés en appelant le rappel ReportSymbolsChanged(). Et le client (dans son rappel) souhaite recevoir la nouvelle liste des symboles exportés en appelant la méthode serveur GetActiveSymbols(). Il s'avère donc que le client ne peut pas recevoir de réponse du serveur car il procède au rappel en attendant la réponse du serveur. En conséquence, le client tombera suite au délai d'attente.
- UseSynchronizationContext = false - Indique que nous ne nous attachons pas à l'interface graphique pour éviter les situations de blocage. Par défaut, les rappels wcf sont attachés au thread parent. Si le thread parent dispose d’ une interface graphique, la situation est possible lorsque le rappel attend l’accomplissement de la méthode par laquelle il a été appelé, mais la méthode ne peut pas se terminer car elle attend la fin du rappel. C'est quelque chose de similaire au cas précédent, même si ce sont deux choses différentes.
Comme pour le cas du serveur, le client implémente également deux interfaces : IExportClient et IDisposable :
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)] public class ExportClient : IExportClient, IDisposable {
Décrivons les variables de service :
// 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; } }
Nous allons maintenant créer des événements pour nos méthodes de rappel. Il est nécessaire que l'application client puisse s'abonner aux événements et recevoir des notifications sur les changements d'état du client.
// calls when tick received public event EventHandler<TickRecievedEventArgs> TickRecieved; // call when symbol list has changed public event EventHandler ActiveSymbolsChanged;
Définissez également les méthodes Open() et Close() pour le 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; } // ... }
Notez que la connexion et la déconnexion des flux sont appelées lorsqu'un client est ouvert ou fermé, il n'est donc pas nécessaire de les appeler directement.
Et maintenant, écrivons le contrat client. Son implémentation conduit à la génération des événements suivants :
public void SendTick(string symbol, MqlTick tick) { // firing event TickRecieved } public void ReportSymbolsChanged() { // firing event ActiveSymbolsChanged }
Enfin, les principales propriétés et méthodes du client sont définies comme suit :
property | Description |
---|---|
Service | Canal de communication des services |
Canal | Instance de contrat de service IExportService |
Méthode | Description |
---|---|
Open() | Se connecte au serveur |
Close() | Se déconnecte du serveur |
Événement | Description |
---|---|
TiqueReçu | Généré après la réception de la nouvelle cotation |
ActiveSymbolsChanged | Généré après les modifications à la liste des symboles actifs |
4. Vitesse de transfert entre deux applications .NET
C'était intéressant pour moi de mesurer la vitesse de transfert entre deux applications .NET, en fait, c'est le débit, qui se mesure en ticks par seconde. J'ai écrit plusieurs applications console pour mesurer les performances du service : la première touche le serveur, la seconde le client. J'ai écrit le code suivant dans la fonction Main() du serveur :
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();
Comme nous le constatons, le code effectue dix mesures de débit. J'ai obtenu les résultats suivants du test sur mon 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 ticks par seconde - je pense qu'il suffit d'exporter des cotations pour 100 symboles (bien sûr, virtuellement, car il semble que personne ne souhaite ouvrir autant de graphiques et attacher des experts =)) De plus, avec un nombre croissant de clients, le nombre maximal de symboles exportés pour chaque client est réduit.
5. Créer une "strate"
Il est maintenant temps de réfléchir à la manière de le connecter au terminal client. Voyons ce que nous avons au premier appel de la fonction dans MetaTrader 5 : l'environnement d'exécution .NET (CLR) est chargé au processus et le domaine d'application est créé par défaut. Il est intéressant de noter qu'il n'est pas déchargé après l'exécution du code.
La seule manière de décharger le CLR du processus est de le terminer (fermer le terminal client), cela forcera Windows à effacer toutes les ressources du processus Ainsi, nous pouvons créer nos objets et ils existeront jusqu'à ce que le domaine d'application soit déchargé, ou détruit par le Garbage Collectionneur.
On peut dire que ça a l'air bien, mais même si nous empêchons la destruction de l'objet par le Garbage Collector, nous ne pouvons pas être en mesure d'accéder aux objets depuis MQL5. Heureusement, un tel accès peut être organisé facilement. L'astuce est la suivante : pour chaque domaine d'application, il existe une table de poignées Garbage Collector (GC handle table), qui est utilisée par l'application pour suivre la durée de vie de l'objet et permet de la gérer manuellement.
L'application ajoute et supprime des éléments de la table en utilisant le type System.Runtime.InteropServices.GCHandle. Tout ce dont nous avons besoin est d'emballer notre objet avec un tel descripteur et nous y avons accès à l’aide de la propriété GCHandle.Target. Ainsi, nous pouvons obtenir la référence à l'objet GCHandle, qui est dans la table des poignées et il est garanti qu'il ne sera pas déplacé ou supprimé par Garbage Collector. L'objet emballé évitera également le recyclage, du fait de la référence par descripteur.
Il est maintenant temps de tester la théorie dans la pratique. Pour ce faire, créons une nouvelle dll win32 dont le nom QExpertWrapper.dll et ajoutons le support CLR, System.dll, QExport.dll, Qexport.Service.dll à la référence de build. Nous créons également une classe auxiliaire ServiceManaged à des fins de gestion - pour effectuer le marshalling, recevoir des objets par des poignées, etc.
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); };
Examinons l’implémentation de ces méthodes. La méthode CreateExportService crée le service, l'encapsule dans GCHandle à l'aide de GCHandle..Alloc et renvoie sa référence. Si quelque chose ne va pas, il affiche une MessageBox avec une erreur. Je l'ai utilisé à des fins de débogage, donc je ne suis pas sûr que ce soit vraiment nécessaire, mais je l'ai laissé ici juste au cas où.
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"); } }
La méthode DestroyExportService obtient le pointeur vers le GCHandle du service, obtient le service de la propriété Target et appelle sa méthode Close(). Il est important de libérer l'objet de service en appelant sa méthode Free(). Autrement, il restera en mémoire, le Garbage Collector ne le supprime pas.
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"); } }
La méthode RegisterSymbol ajoute un symbole à la liste des symboles exportés :
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"); } }
La méthode UnregisterSymbolsupprime un symbole de la liste :
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"); } }
Et maintenant la méthode SendTick.. Comme nous le voyons, le pointeur est transformé en structure MqlTick à l'aide de la classe Marshal. Autre point : il n'y a pas de code dans catch block - c'est fait pour éviter les décalages de la file d'attente générale des ticks en cas d'erreur.
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 (...) { } }
Examinons l'implémentation de fonctions, qui seront appelées à partir de nos programmes 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)); }
Le code est prêt, nous devons maintenant le compiler et le construire. Indiquons le répertoire de sortie comme "C:\Program Files\MetaTrader 5\MQL5\Libraries" dans les options du projet. Après la compilation, trois bibliothèques apparaîtront dans le dossier indiqué.
Le programme mql5 n'en utilise qu'une, à savoir QExportWrapper.dll, deux autres bibliothèques sont utilisées par celui-ci. Pour cette raison, nous devons placer les bibliothèques Qexport.dll et Qexport.Service.dll .dans le dossier racine de MetaTrader. Ce n'est pas pratique.
La solution consiste à créer un fichier de configuration et à y spécifier le chemin des bibliothèques. Créons le fichier nommé terminal.exe.config dans le dossier racine de MetaTrader et écrivons-y les chaînes suivantes :
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="mql5\libraries" /> </assemblyBinding> </runtime> </configuration>
C'est prêt. Maintenant, CLR recherchera les bibliothèques dans le dossier que nous avons spécifié.
6. Implémentation de la partie du serveur en MQL5
Enfin, nous sommes arrivés à la programmation de la partie serveur en mql5. Créons un nouveau fichier QService..mqh et définissons les fonctions importées de 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
C'est génial que mql5 ait des classes car c'est une fonctionnalité idéale pour encapsuler toute la logique à l'intérieur, ce qui simplifie considérablement le travail et la compréhension du code. Concevons donc une classe qui sera une coquille pour les méthodes de la bibliothèque.
De plus, pour éviter la situation de création de service pour chaque symbole, organisons la vérification du service de travail avec un tel nom, et nous nous attèlerons dessus dans un tel cas. Les variables globales constituent une méthode idéale pour fournir ces informations, pour les raisons suivantes :
- les variables globales disparaissent après la clôture du terminal client. La même chose est avec le service;
- nous pouvons servir le nombre d'objets Qservice, qui utilise le service. Il permet de clôturer le service physique uniquement après la clôture du dernier objet.
Créons donc une 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 comporte les méthodes suivantes :
Méthode | Description |
---|---|
Créer (chaîne const) | Commence le service |
Close() | Clôture le service |
SendTick(chaîne const, MqlTick&) | Envoie une cotation |
Notez également que les méthodes privées EnterCriticalSection() et LeaveCriticalSection() vous permettent d'exécuter les sections critiques du code entre elles.
Cela nous soulagera des cas des appels simultanés de fonction Create() et création de nouveaux services pour chacun QService.
Donc, nous avons décrit la classe pour travailler avec le service, écrivons maintenant un Expert Advisor pour la diffusion des cotations. L'Expert Advisor a été choisi en raison de sa possibilité de traiter tous les ticks arrivés.
//+------------------------------------------------------------------+ //| 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 des performances de communication entre ex5 et le client .NET
Il est évident que la performance totale du service diminuera si les cotations arrivent directement depuis le terminal client, c'est pourquoi je me suis intéressé à le mesurer. J'étais sûr qu'il aurait dû diminuer en raison de la perte inévitable de temps CPU pour le triage et le transtypage.
Pour cela, j'ai écrit un script simple qui est le même que pour le premier test. La fonction Start() se présente comme suit :
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;
J'ai obtenu les résultats suivants :
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 ticks/sec contre 1900 ticks/sec. 25% est le prix qui devrait être payé pour l'utilisation des services de MT5, mais de toute façon c'est suffisant. Il est intéressant de noter que les performances peuvent être augmentées en utilisant le pool de threads et la méthode statique System.Threading..ThreadPool..QueueUserWorkItem.
En utilisant cette méthode, j'ai obtenu une vitesse de transfert jusqu'à 10000 ticks par seconde. Mais son travail dans un test difficile était instable en raison du fait que le Garbage Collector n'a pas le temps de supprimer des objets - en conséquence, la mémoire allouée par MetaTrader augmente rapidement et finalement il se bloque. Mais c'était un test difficile, loin d'être réel, il n'y a donc rien de dangereux dans l’utilisation du pool de threads.
8. Tests en temps réel
J'ai créé un exemple de table de ticks en utilisant le service. Le projet est attaché dans l'archive et nommé WindowsClient. Le résultat de ses travaux est présenté ci-dessous :
Fig 1. Fenêtre principale de l'application WindowsClient avec tableau de guillemets
Conclusion
Dans cet article, j'ai décrit l'une des méthodes d'exportation de devis vers des applications .NET. Tout le nécessaire a été implémenté et nous avons maintenant des classes prêtes qui peuvent être utilisées dans vos propres applications. La seule chose qu'il n'est pas pratique de joindre des scripts à chacun des graphiques nécessaires.
À l'heure actuelle, je pense que ce problème peut être résolu en utilisant les profils MetaTrader. De l'autre côté, si vous n'avez pas besoin de toutes les cotations, vous pouvez l'organiser avec un script qui diffuse des cotations pour les symboles nécessaires. Comme vous le comprenez, la diffusion en profondeur du marché ou même l'accès des deux côtés peuvent être organisés de la même manière.
Description des archives :
Bin.rar - archive avec une solution prête. Pour les utilisateurs qui souhaitent voir Comment ça marche ? Notez tout de même que .NET Framework 3.5 (peut-être il fonctionnera également avec la version 3.0) doit être installé sur votre ordinateur.
Src.rar - Le code source complet du programme Pour y travailler avec, vous aurez besoin de MetaEditor et Visual Studio 2008.
QExportDemoProfile.rar - Profil Metatrader, qui attache un script à 10 graphiques, comme illustré à la Fig. 1.
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/27
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation