МetaTrader 5. Экспорт котировок в .NET приложение, используя WCF сервисы
Введение
Программисты, использующие механизм трансляции котировок по DDE в MetaTrader 4, наверное, уже слышали, что в пятой версии этот механизм отправился на утилизацию. Вместе с тем, никакой стандартной возможности по экспорту котировок предоставлено не было. Как решение проблемы, разработчики предлагают написание своих dll, реализующих этот механизм. Что ж, если писать реализацию самим, то давайте это сделаем красиво!
Почему .NET?
Как человеку, программирующему на .Net уже достаточное количество времени, мне было рациональнее, интереснее и проще реализовать экспорт котировок именно с использованием этой платформы. К сожалению, с выходом "пятерки" поддержки .Net также не появилось. Уверен, у разработчиков на это были свои причины и соображения. Поэтому по старинке будем использовать как прослойку win32 dll с поддержкой .NET.
Почему WCF?
Технология Windows Communication Foundation была выбрана мною по нескольким причинам: с одной стороны, это ее легкая расширяемость и адаптируемость; с другой стороны, все же хочется проверить ее в действии под приличной нагрузкой. К тому же, по словам Microsoft, WCF дает немного большую производительность по сравнению с .Net Remoting.
Определение требований к системе
Итак, давайте подумаем, что же мы хотим получить от нашего механизма. На мой взгляд, тут выплывают два основных требования:
- Безусловно, это сам экспорт тиков, желательно используя структуру MqlTick.
- Хорошо бы знать, какие инструменты экспортируются в данный момент.
1. Проектирование общих классов и контрактов
Для начала создадим новую библиотеку классов и назовем ее QExport.dll. В ней опишем структуру MqlTick, попутно обозначив её как контракт данных:
[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; } }
Затем приступим к определению контрактов сервиса. Сразу оговорюсь, что я не сторонник конфигурационных файлов и сгенерированных прокси-классов, поэтому подобной «прелести» здесь не будет.
Определим серверный контракт исходя из описанных выше требований:
[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 во избежание ситуаций зависания. Хотя для нашей задачи здесь он не нужен, но если хостить сервис из-под win-приложения, то понадобится;
- IncludeExceptionDetailInFaults = true – информация об исключении включается в объект FaultException при передаче клиенту.
Сам ExportService включает два интерфейса: IExportService, IDisposable. Первый реализует все функции по работе с сервисом, а второй реализует стандартную модель освобождения ресурсов .Net:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false, IncludeExceptionDetailInFaults = true)] public class ExportService : IExportService, IDisposable {
Опишем переменные сервиса:
// хранится полный адрес сервиса в формате net.pipe://localhost/имя_сервера private readonly String _ServiceAddress; // хост сервиса private ServiceHost _ExportHost; // коллекция коллбэков активных клиентов private Collection<IExportClient> _Clients = new Collection<IExportClient>(); // список активных символов private List<String> _ActiveSymbols = new List<string>(); // объект для лока private object lockClients = new object();Определим методы Open() и Close(), которые будут открывать и закрывать наш сервис соответственно:
public void Open() { _ExportHost = new ServiceHost(this); // точка с сервисом _ExportHost.AddServiceEndpoint(typeof(IExportService), // контракт new NetNamedPipeBinding(), // привязка new Uri(_ServiceAddress)); // адрес // производим регулировку нагрузки на сервер // снимаем ограничение на 16 запросов в очереди 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 { // закрываем канал с каждым клиентом // ... // закрываем хост _ExportHost.Close(); } finally { _ExportHost = null; } // ... }
Далее, реализуем все методы интерфейса IExportService:
public void Subscribe() { // получаем канал обратной связи IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>(); lock (lockClients) _Clients.Add(cl); } public void Unsubscribe() { // получаем канал обратной связи 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); // отсылаем всем клиентам уведомление, что список символов изменился //... } public void UnregisterSymbol(String symbol) { _ActiveSymbols.Remove(symbol); // отсылаем всем клиентам уведомление, что список символов изменился //... } 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) { // по видимому, связь оборвалась с клиентом - просто удалим его _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 {
Опишем переменные сервиса:
// хранится полный адрес сервиса private readonly String _ServiceAddress; // объект сервиса, к которому подключены private IExportService _ExportService; // Возвращает экземпляр сервиса public IExportService Service { get { return _ExportService; } } // Возвращает коммуникационный канал public IClientChannel Channel { get { return (IClientChannel)_ExportService; } }
Теперь создадим события, которые будут срабатывать в наших коллбэк-методах. Это нужно для того, чтобы клиентское приложение могло на них подписаться и получать уведомления при изменении состояния клиента:
// вызывается при получении тика public event EventHandler<TickRecievedEventArgs> TickRecieved; // вызывается при изменении списка активных инструментов public event EventHandler ActiveSymbolsChanged;
У клиента также определим методы Open() и Close():
public void Open() { // создаем фабрику каналов var factory = new DuplexChannelFactory<IExportService>( new InstanceContext(this), new NetNamedPipeBinding()); // создаем серверный канал _ExportService = factory.CreateChannel(new EndpointAddress(_ServiceAddress)); IClientChannel channel = (IClientChannel)_ExportService; channel.Open(); // подключаемся к фидам _ExportService.Subscribe(); } public void Close() { Dispose(true); } private void Dispose(bool disposing) { try { // отключаемся от фидов _ExportService.Unsubscribe(); Channel.Close(); } finally { _ExportService = null; } // ... }
Обратите внимание, что подключение к фидам и отключение от них вызываются при открытии/закрытии клиента, поэтому явным образом их вызывать не следует!
А теперь, собственно, реализуем сам клиентский контракт. Его реализация сводится к генерации соответствующих событий:
public void SendTick(string symbol, MqlTick tick) { // файрим событие TickRecieved } public void ReportSymbolsChanged() { // файрим событие ActiveSymbolsChanged }
В результате, мы можем описать основные свойства и методы клиента:
Свойства | Описание |
---|---|
Service | Коммуникационный канал сервиса |
Channel | Экземпляр сервисного контракта IExportService, с которым работает клиент |
Методы | Описание |
---|---|
Open() | Подключается к серверу |
Close() | Отключается от сервера |
События | Описание |
---|---|
TickRecieved | Генерируется при поступлении новой котировки |
ActiveSymbolsChanged | Генерируется при изменении списка активных инструментов |
4. Полевое испытание между двумя .NET приложениями
Мне стало интересно, какая же скорость передачи между двумя .net приложениями, вернее даже не скорость, а пропускная способность (тиков в секунду). Я написал пару консольных приложений для определения производительности сервиса: одно – для сервера, второе – для клиента. В функции Main сервера написал следующий код:
ExportService host = new ExportService("mt5"); host.Open(); Console.WriteLine("Нажмите любую клавишу для начала выгрузки тиков"); 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} тиков в секунду", counter * 100); } Console.WriteLine("В среднем {0:F2} тиков в секунду", total / 10); host.Close();
Мы видим, что код производит десять замеров пропускной способности решения. На моем Athlon 3000+ тест выдал следующие результаты:
2600 тиков в секунду 3400 тиков в секунду 3300 тиков в секунду 2500 тиков в секунду 2500 тиков в секунду 2500 тиков в секунду 2400 тиков в секунду 2500 тиков в секунду 2500 тиков в секунду 2500 тиков в секунду В среднем 2670,00 тиков в секунду
2500 тиков в секунду - думаю, этого хватит, чтобы свободно экспортировать котировки из 100 инструментов (теоретически, конечно, потому что никто не захочет открывать столько графиков и набрасывать на все скрипты =)). Причем при увеличении числа клиентов максимальное число экспортируемых инструментов на каждого клиента уменьшается, то есть, по сути, имеет обратную зависимость.
5. Организуем "прослойку"
Теперь пришло время подумать, как
связать это все с терминалом. Давайте посмотрим, что же получается при первом
вызове управляемой функции из MetaTrader 5: в процесс загружается исполняющая среда .Net (CLR), и в ней создается домен приложения
по умолчанию. Самое интересное, что все это не выгружается после выполнения
кода.
Единственный метод выгрузить CLR из процесса – завершить процесс (закрыть терминал), что
вынудит Windows очистить все ресурсы, используемые в процессе. Получается, мы
можем создавать наши объекты, и они будут храниться до тех пор, пока не
выгрузится сам домен приложения или его не съест сборщик мусора.
"Это все
хорошо", - скажете вы, но даже если сделать так, чтобы сборщик мусора не смог удалить
объекты, из Mql5 все равно не будет доступа к ним. К счастью, такой доступ
можно легко организовать. Весь фокус в том, что для каждого домена приложений
создается таблица описателей GC (GC handle table), с помощью которой приложение отслеживает время жизни
объекта или позволяет управлять им вручную.
Приложение добавляет и удаляет элементы из таблицы с помощью типа System.Runtime.InteropServices.GCHandle. Достаточно обернуть таким описателем наш объект, и мы имеем доступ к нему через свойство GCHandle.Target, при этом, мы можем получить ссылку на объект GCHandle, который находится в таблице описателей и гарантированно не будет перемещен или удален сборщиком мусора. Обернутый объект также избежит утилизации, так как на него явно будет ссылаться описатель.
Пришло время проверить теорию на практике. Для этого в Visual Studio создадим новую win32 dll с именем QExpertWrapper.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 с помощью GCHandle.Alloc, и возвращает на него ссылку. Если что-то пойдет не так, появится окошко с ошибкой. Окошко использовалось во время отладки, поэтому не уверен, нужно ли оно здесь. На всякий случай оставил.
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 с сервисом, получает сервис из свойства Target, и вызывает у него метод 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 с помощью класса Marshal. Еще один момент: отсутствует код в блоке catch - это сделано для того, чтобы не тормозить общий поток тиков при ошибке.
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) // --------------------------------------------------------------- // Создает и открывает сервис // и возвращает указатель на него // --------------------------------------------------------------- _DLLAPI long long __stdcall CreateExportService(const wchar_t* serverName) { IntPtr hService = ServiceManaged::CreateExportService(gcnew String(serverName)); return (long long)hService.ToPointer(); } // ----------------------------------------- ---------------------- // Закрывает сервис // --------------------------------------------------------------- _DLLAPI void __stdcall DestroyExportService(const long long hService) { ServiceManaged::DestroyExportService(IntPtr((HANDLE)hService)); } // --------------------------------------------------------------- // Передает тик // --------------------------------------------------------------- _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)); } // --------------------------------------------------------------- // Регистрирует экспортируемый символ // --------------------------------------------------------------- _DLLAPI void __stdcall RegisterSymbol(const long long hService, const wchar_t* symbol) { ServiceManaged::RegisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol)); } // --------------------------------------------------------------- // Убирает экспортируемый символ // --------------------------------------------------------------- _DLLAPI void __stdcall UnregisterSymbol(const long long hService, const wchar_t* symbol) { ServiceManaged::UnregisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol)); }
Ну вот, кажется, весь код написан,
осталось только скомпилировать и скомпоновать. В настройках проекта в качестве output directory укажем C:\Program Files\MetaTrader 5\MQL5\Libraries. После билда в указанной папке появятся 3
библиотеки.
Но так как из mql используется только одна из них, а именно QExportWrapper.dll, две
остальные подгружаются ею. По этой причине сборки Qexport.dll и Qexport.Service.dll придется поместить в корневую папку
с 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: // указатель на сервис long hService; // имя сервиса string serverName; // имя глобальной переменной сервиса string gvName; // указывает, был ли сервис закрыт явно bool wasDestroyed; // входит в критическую секцию void EnterCriticalSection(); // выходит из критической секции void LeaveCriticalSection(); public: QService(); ~QService(); // открывает сервис void Create(const string); // закрывает сервис void Close(); // посылает тик void SendTick(const string, MqlTick&); }; //-------------------------------------------------------------------- QService::QService() { wasDestroyed = false; } //-------------------------------------------------------------------- QService::~QService() { // если не был удален явно, то удалим в деструкторе if (!wasDestroyed) Close(); } //-------------------------------------------------------------------- QService::Create(const string serviceName) { EnterCriticalSection(); serverName = serviceName; bool exists = false; string name; // проверяем, есть ли у нас уже запущенный сервис с таким именем for (int i = 0; i < GlobalVariablesTotal(); i++) { name = GlobalVariableName(i); if (StringFind(name, "QService|" + serverName) == 0) { exists = true; break; } } if (!exists) // еще не запущен { // запускаем сервис hService = CreateExportService(serverName); // добавляем глобальную переменную gvName = "QService|" + serverName + ">" + (string)hService; GlobalVariableTemp(gvName); GlobalVariableSet(gvName, 1); } else // уже запущен { gvName = name; // получаем хендл сервиса hService = (int)StringSubstr(gvName, StringFind(gvName, ">") + 1); // уведомляем, что еще один скрипт использует сервер GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) + 1); } // оповещаем, какой символ подключен RegisterSymbol(hService, Symbol()); LeaveCriticalSection(); } //-------------------------------------------------------------------- QService::Close() { EnterCriticalSection(); // уведомляем, что еще один скрипт уже не использует сервер GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) - 1); // если больше нет скриптов, использующих сервер, то закроем его if (NormalizeDouble(GlobalVariableGet(gvName), 0) < 1.0) { GlobalVariableDel(gvName); DestroyExportService(hService); } else UnregisterSymbol(hService, 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
Очевидно, меня заинтересовало,
насколько же снизится пропускная способность сервиса, если котировки уже будут
поступать непосредственно с терминала. А в том, что она снизится, я был уверен:
неизбежные потери процессорного времени на маршаллинг и приведение типов.
Для этой цели я написал простенький скрипт, который аналогичен приведенному в первом тесте. Функция 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," вызовов в секунду"); total += calls * 100; } Print("В среднем ", total / 10," вызовов в секунду"); serv.Close(); delete serv;
И вот какие результаты я получил:
1900 вызовов в секунду 2400 вызовов в секунду 2100 вызовов в секунду 2300 вызовов в секунду 2000 вызовов в секунду 2100 вызовов в секунду 2000 вызовов в секунду 2100 вызовов в секунду 2100 вызовов в секунду 2100 вызовов в секунду В среднем 2110 вызовов в секунду
2500 тиков/с против 1900 тиков/с. 25% – вот цена
использования сервисов из MT5, чего, впрочем, хватит с головой. Сразу скажу, что
это не предел - если хочется большей производительности, то можно
воспользоваться пулом потоков, а именно статическим методом System.Threading.ThreadPool.QueueUserWorkItem.
С ним у меня пропускная способность спокойно выходила за 100000 тиков в секунду. Правда длилось это недолго – сборщик мусора не успевает удалять объекты и память, занимаемая МТ, стремительно растет, после чего все крешится. Но в реальных условиях при обычной нагрузке в использовании пула потоков нет ничего опасного =)
8. Тестирование в
реальных условиях
В заключение я создал пример реализации таблицы котировок, используя сервис. Проект находится в солюшене с другими и называется WindowsClient. Результат работы представлен на рисунке ниже.
Рис 1. Главное окно программы WinowsClient с таблицей котировок
Заключение
В данной статье я описал лишь один из способов экспорта котировок в .NET. Было реализовано все, что требовалось, и теперь мы имеем готовые классы, которые можем использовать в своих приложениях. Единственное, что не совсем удобно - это набрасывать скрипты на каждый требуемый график.
Пока я вижу один выход - использовать профили MetaTrader. C другой стороны, если получение всех котировок не нужно, то можно организовать всё в виде скрипта, который в цикле транслирует котировки с нужных символов. Как вы понимаете, по такому принципу можно организовать трансляцию стаканов или даже двусторонний доступ.
В архивах:
Bin.rar - папка с уже готовым решением. Для тех, кто просто хочет посмотреть, что за фрукт. Но учтите, для этого понадобится наличие .NET Framework 3.5 (возможно, на 3.0 тоже запустится).
Src.rar - полный исходный код. Для его просмотра нужен MetaEditor и Visual Studio 2008.
QExportDemoProfile.rar - профиль Metatrader, который набрасывает скрипты на 10 графиков, изображенных на рис 1.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Очень интересно..Спасибо..Но может быть Сегодня есть более простые возможности*??
Статья хороша тем, что рассказывает про WCF тем, кто незнаком с технологией. Кстати, я проверял быстродействие WCF через интернет на дистанции Питер - Одесса, получалась приличная скорость около 14000 двусторонних транзакций/сек пакетами по 1 кБ. Мне лично технология нравиться своей объектно-ориентированностью, то есть передается не поток байтов (хотя можно и так), а экземпляры классов, то есть на приемном конце не надо этот поток декодировать.
Ну а по теме котировок - проще использовать memory mapping. В приложении .NET запускаем отдельную задачу с мьютексом, MQL4 грузит в память данные и сбрасывает мьютекс, после чего приложение читает данные. Так проще и быстрее, и ДЛЛ не нужна. Надо только в MQL4 добавить системные ДЛЛ для поддержки мэппинга и мьютексов, тут недавно статья была по этой теме.
1) Если у вас терминал x64, то скорее всего будет необходимо перекомпилировать с++ проект в x64. На всякий случай делается это так: Visual Studio -> Properties (вашего с++ проекта) -> Configuration Manager -> и там через выпадающий список либо через <New...> меняете на x64. Компилируем. Готово. Если не компилируется - проверьте References проекта, пройдитесь по ошибкам компилятора.
2) У меня были проблемы c советником, он даже не инициализировался. Я заметил это только при отключении советника от графика, когда вылетела ошибка init failed (забавно..). Советник впадал в бесконечный Sleep еще на стадии инициализации и все дело было в этих строчках (в QService.mqh):
QService::EnterCriticalSection()
{
while (GlobalVariableCheck("QService_CriticalSection") > 0)
Sleep(1);
GlobalVariableTemp("QService_CriticalSection");
}
После дебага и тщетных попыток понять этот "Sleep(1)", заменил код на:
QService::EnterCriticalSection()
{
if(!GlobalVariableCheck("QService_CriticalSection"))
GlobalVariableTemp("QService_CriticalSection");
}
P.S: Спасибо автору статьи! Все круто расписано!
Очень интересно..Спасибо..Но может быть Сегодня есть более простые возможности*??
Конечно есть - использование подключения через встроенную в терминал возможность. Это PIPE-канал.
https://www.mql5.com/ru/docs/files/fileopen