diff --git a/MetaTraderApi_2022.sln b/MetaTraderApi_2022.sln index e6c715ba..de480078 100755 --- a/MetaTraderApi_2022.sln +++ b/MetaTraderApi_2022.sln @@ -11,6 +11,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MT5Connector", "MT5Connecto EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MtApi5", "MtApi5\MtApi5.csproj", "{AC8B5010-DA75-477E-9CA5-547C649E12D8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestClients", "TestClients", "{0C166554-D20F-4996-AA33-436997058FA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MtApi5TestClient", "TestClients\MtApi5TestClient\MtApi5TestClient.csproj", "{38B9C657-BC2F-44F0-8824-54B31F2A64F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,10 +73,25 @@ Global {AC8B5010-DA75-477E-9CA5-547C649E12D8}.Release|x64.Build.0 = Release|Any CPU {AC8B5010-DA75-477E-9CA5-547C649E12D8}.Release|x86.ActiveCfg = Release|Any CPU {AC8B5010-DA75-477E-9CA5-547C649E12D8}.Release|x86.Build.0 = Release|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Debug|x64.Build.0 = Debug|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Debug|x86.ActiveCfg = Debug|x86 + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Debug|x86.Build.0 = Debug|x86 + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Release|Any CPU.Build.0 = Release|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Release|x64.ActiveCfg = Release|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Release|x64.Build.0 = Release|Any CPU + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Release|x86.ActiveCfg = Release|x86 + {38B9C657-BC2F-44F0-8824-54B31F2A64F5}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {38B9C657-BC2F-44F0-8824-54B31F2A64F5} = {0C166554-D20F-4996-AA33-436997058FA1} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F3C954E-FEF0-4161-B557-9F8D62D7A39F} EndGlobalSection diff --git a/MtApi5/IMtLogger.cs b/MtApi5/IMtLogger.cs new file mode 100755 index 00000000..7b49dcba --- /dev/null +++ b/MtApi5/IMtLogger.cs @@ -0,0 +1,11 @@ +namespace MtApi5 +{ + public interface IMtLogger + { + void Debug(object message); + void Error(object message); + void Fatal(object message); + void Info(object message); + void Warn(object message); + } +} diff --git a/MtApi5/LogConfigurator.cs b/MtApi5/LogConfigurator.cs deleted file mode 100755 index 125c3375..00000000 --- a/MtApi5/LogConfigurator.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Diagnostics; -//using log4net; -//using log4net.Appender; -//using log4net.Core; -//using log4net.Layout; -//using log4net.Repository.Hierarchy; - -namespace MtApi5 -{ - public class MtLog - { - #region ctor - internal MtLog(Type type) - { -// _log = LogManager.GetLogger(type); - } - #endregion - - #region Public - - public void Debug(object message) - { -// _log.Debug(message); - } - - public void Error(object message) - { -// _log.Error(message); - } - - public void Fatal(object message) - { -// _log.Fatal(message); - } - - public void Info(object message) - { -// _log.Info(message); - } - - public void Warn(object message) - { -// _log.Warn(message); - } - #endregion - - #region Private - - //private readonly ILog _log; - - #endregion - } - - public enum LogLevel - { - Off, - Debug, - Info - } - - public class LogConfigurator - { - private const string LogFileNameExtension = "log"; - - public static void Setup(string profileName) - { -#if (DEBUG) - const LogLevel logLevel = LogLevel.Debug; -#else - const LogLevel logLevel = LogLevel.Info; -#endif - Setup(profileName, logLevel); - } - - public static void Setup(string profileName, LogLevel logLevel) - { - if (string.IsNullOrEmpty(profileName)) - throw new ArgumentNullException(); - - //var hierarchy = (Hierarchy) LogManager.GetRepository(); - - ////check if logger is already configurated to avoid creation many empty logs files - //if (hierarchy.Configured) - // return; - - //var patternLayout = new PatternLayout - //{ - // ConversionPattern = "%date [%thread] %-5level %logger - %message%newline" - //}; - //patternLayout.ActivateOptions(); - - ////var filename = $"{DateTime.Now:yyyy-dd-M--HH-mm-ss}-{Process.GetCurrentProcess().Id}.{LogFileNameExtension}"; // This filename format keeps generating tons of log files with same content but different per-second/thread filenames until the harddisk have 0KB space left - //var filename = $"{DateTime.Now:yyyy-MM-dd}.{LogFileNameExtension}"; - - //var roller = new RollingFileAppender - //{ - // AppendToFile = true, - // File = $@"{System.IO.Path.GetTempPath()}{profileName}\Logs\{filename}", - // Layout = patternLayout, - // PreserveLogFileNameExtension = true, - // MaxSizeRollBackups = 5, - // MaximumFileSize = "1GB", - // RollingStyle = RollingFileAppender.RollingMode.Size, - // StaticLogFileName = false - //}; - //roller.ActivateOptions(); - //hierarchy.Root.AddAppender(roller); - //hierarchy.Root.Level = ConvertLogLevel(logLevel); - //hierarchy.Configured = true; - } - - public static MtLog GetLogger(Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - return new MtLog(type); - } - - //private static Level ConvertLogLevel(LogLevel logLevel) - //{ - // switch (logLevel) - // { - // case LogLevel.Debug: return Level.Debug; - // case LogLevel.Info: return Level.Info; - // case LogLevel.Off: return Level.Off; - // default: - // throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); - // } - //} - } -} diff --git a/MtApi5/MqlBookInfo.cs b/MtApi5/MqlBookInfo.cs index 39d1de8a..c704ae58 100755 --- a/MtApi5/MqlBookInfo.cs +++ b/MtApi5/MqlBookInfo.cs @@ -1,5 +1,4 @@ -// ReSharper disable InconsistentNaming -namespace MtApi5 +namespace MtApi5 { public class MqlBookInfo { diff --git a/MtApi5/Mt5CommandType.cs b/MtApi5/Mt5CommandType.cs index c50d5521..62e1e92d 100755 --- a/MtApi5/Mt5CommandType.cs +++ b/MtApi5/Mt5CommandType.cs @@ -7,6 +7,7 @@ namespace MtApi5 //trade operations //OrderSend = 1, + GetQuote = 1, OrderCalcMargin = 2, OrderCalcProfit = 3, //OrderSendAsync = 5, diff --git a/MtApi5/Mt5Quote.cs b/MtApi5/Mt5Quote.cs index ded45fcb..cae0e17c 100755 --- a/MtApi5/Mt5Quote.cs +++ b/MtApi5/Mt5Quote.cs @@ -4,15 +4,19 @@ namespace MtApi5 { public class Mt5Quote { - public string Instrument { get; } - public double Bid { get; } - public double Ask { get; } + public string? Instrument { get; set; } + public double Bid { get; set; } + public double Ask { get; set; } public int ExpertHandle { get; set; } public DateTime Time { get; set; } public double Last { get; set; } public ulong Volume { get; set; } -// public long TimeMsc { get; set; } -// public uint Flags { get; set; } + // public long TimeMsc { get; set; } + // public uint Flags { get; set; } + + internal Mt5Quote() + { + } internal Mt5Quote(string instrument, double bid, double ask) { diff --git a/MtApi5/MtApi5Client.cs b/MtApi5/MtApi5Client.cs index 96ad82c1..c656b90b 100755 --- a/MtApi5/MtApi5Client.cs +++ b/MtApi5/MtApi5Client.cs @@ -1,5 +1,4 @@ -// ReSharper disable InconsistentNaming -using System.Collections; +using System.Collections; using MtApi5.Requests; using Newtonsoft.Json; using MtApi5.Events; @@ -28,33 +27,32 @@ namespace MtApi5 #region Private Fields - private static readonly MtLog Log = LogConfigurator.GetLogger(typeof(MtApi5Client)); - private MtRpcClient? _client; - private readonly object _locker = new object(); + private readonly object _locker = new(); private volatile bool _isBacktestingMode; private Mt5ConnectionState _connectionState = Mt5ConnectionState.Disconnected; private int _executorHandle; - private readonly Dictionary> _mtEventHandlers = - new Dictionary>(); + private readonly Dictionary> _mtEventHandlers = []; + private HashSet _experts = []; + private Dictionary _quotes = []; + private readonly EventWaitHandle _quotesWaiter = new AutoResetEvent(false); + private int _commandId = 0; + private readonly Dictionary _tasks = []; #endregion #region Public Methods - public MtApi5Client() - { -#if (DEBUG) - const LogLevel logLevel = LogLevel.Debug; -#else - const LogLevel logLevel = LogLevel.Info; -#endif - LogConfigurator.Setup(LogProfileName, logLevel); + private IMtLogger? Log { get; } + public MtApi5Client(IMtLogger? log = null) + { _mtEventHandlers[Mt5EventTypes.OnBookEvent] = ReceivedOnBookEvent; _mtEventHandlers[Mt5EventTypes.OnTick] = ReceivedOnTickEvent; _mtEventHandlers[Mt5EventTypes.OnTradeTransaction] = ReceivedOnTradeTransactionEvent; _mtEventHandlers[Mt5EventTypes.OnLastTimeBar] = ReceivedOnLastTimeBarEvent; _mtEventHandlers[Mt5EventTypes.OnLockTicks] = ReceivedOnLockTicksEvent; + + Log = log; } /// @@ -64,7 +62,7 @@ namespace MtApi5 ///Port of host connection (default 8222) public void BeginConnect(string host, int port) { - Log.Info($"BeginConnect: host = {host}, port = {port}"); + Log?.Info($"BeginConnect: host = {host}, port = {port}"); Task.Factory.StartNew(() => Connect(host, port)); } @@ -74,7 +72,7 @@ namespace MtApi5 ///Port of host connection (default 8222) public void BeginConnect(int port) { - Log.Info($"BeginConnect: port = localhost:{port}"); + Log?.Info($"BeginConnect: port = localhost:{port}"); Task.Factory.StartNew(() => Connect("localhost", port)); } @@ -83,20 +81,23 @@ namespace MtApi5 /// public void BeginDisconnect() { - Log.Info("BeginDisconnect called."); + Log?.Info("BeginDisconnect called."); Task.Factory.StartNew(() => Disconnect(false)); } /// - ///Load quotes connected into MetaTrader API. + ///Load quotes connected into MetaTrader API (deprecated) /// - public IEnumerable? GetQuotes() + public IEnumerable GetQuotes() { - //TODO: !!! - //var client = Client; - //var quotes = client?.GetQuotes(); - //return quotes?.Select(q => new Mt5Quote(q)); - return null; + // this function is deprecated. + // should be used event OnQuoteList + + _quotesWaiter.WaitOne(10000); // wait 10 sec for loading all quotes from MetaTrader + lock (_locker) + { + return _quotes.Values.ToList(); + } } /// @@ -121,11 +122,11 @@ namespace MtApi5 /// public bool OrderSend(MqlTradeRequest request, out MqlTradeResult? result) { - Log.Debug($"OrderSend: request = {request}"); + Log?.Debug($"OrderSend: request = {request}"); if (request == null) { - Log.Warn("OrderSend: request is not defined!"); + Log?.Warn("OrderSend: request is not defined!"); result = null; return false; } @@ -153,11 +154,11 @@ namespace MtApi5 /// public bool OrderSendAsync(MqlTradeRequest request, out MqlTradeResult? result) { - Log.Debug($"OrderSend: request = {request}"); + Log?.Debug($"OrderSend: request = {request}"); if (request == null) { - Log.Warn("OrderSend: request is not defined!"); + Log?.Warn("OrderSend: request is not defined!"); result = null; return false; } @@ -234,11 +235,11 @@ namespace MtApi5 /// public bool OrderCheck(MqlTradeRequest request, out MqlTradeCheckResult? result) { - Log.Debug($"OrderCheck: request = {request}"); + Log?.Debug($"OrderCheck: request = {request}"); if (request == null) { - Log.Warn("OrderCheck: request is not defined!"); + Log?.Warn("OrderCheck: request is not defined!"); result = null; return false; } @@ -606,7 +607,7 @@ namespace MtApi5 /// output result public bool PositionClose(ulong ticket, ulong deviation, out MqlTradeResult? result) { - Log.Debug($"PositionClose: ticket = {ticket}, deviation = {deviation}"); + Log?.Debug($"PositionClose: ticket = {ticket}, deviation = {deviation}"); var response = SendRequest(new PositionCloseRequest { @@ -660,7 +661,7 @@ namespace MtApi5 /// true - successful check of the basic structures, otherwise - false. public bool PositionOpen(string symbol, ENUM_ORDER_TYPE orderType, double volume, double price, double sl, double tp, string comment, out MqlTradeResult? result) { - Log.Debug($"PositionOpen: symbol = {symbol}, orderType = {orderType}, volume = {volume}, price = {price}, sl = {sl}, tp = {tp}, comment = {comment}"); + Log?.Debug($"PositionOpen: symbol = {symbol}, orderType = {orderType}, volume = {volume}, price = {price}, sl = {sl}, tp = {tp}, comment = {comment}"); var response = SendRequest(new PositionOpenRequest { @@ -734,7 +735,7 @@ namespace MtApi5 /// true - successful check of the structures, otherwise - false. public bool Buy(out MqlTradeResult? result, double volume, string? symbol = null, double price = 0.0, double sl = 0.0, double tp = 0.0, string? comment = null) { - Log.Debug($"Buy: volume = {volume}, symbol = {symbol}, sl = {sl}, tp = {tp}, comment = {comment}"); + Log?.Debug($"Buy: volume = {volume}, symbol = {symbol}, sl = {sl}, tp = {tp}, comment = {comment}"); var response = SendRequest(new BuyRequest { @@ -763,7 +764,7 @@ namespace MtApi5 /// true - successful check of the structures, otherwise - false. public bool Sell(out MqlTradeResult? result, double volume, string? symbol = null, double price = 0.0, double sl = 0.0, double tp = 0.0, string? comment = null) { - Log.Debug($"Sell: volume = {volume}, symbol = {symbol}, sl = {sl}, tp = {tp}, comment = {comment}"); + Log?.Debug($"Sell: volume = {volume}, symbol = {symbol}, sl = {sl}, tp = {tp}, comment = {comment}"); var response = SendRequest(new SellRequest { @@ -2213,7 +2214,7 @@ namespace MtApi5 #region Common Functions /// - ///It enters a message in the Expert Advisor log. + ///It enters a message in the Expert Advisor Log?. /// ///Message public bool Print(string message) @@ -2988,7 +2989,7 @@ namespace MtApi5 ///input-parameters of a custom indicator. If there is no parameters specified, then default values will be used. public int iCustom(string symbol, ENUM_TIMEFRAMES period, string name, double[] parameters) { - Log.Debug("iCustom: called."); + Log?.Debug("iCustom: called."); var response = SendRequest(new ICustomRequest { Symbol = symbol, @@ -2997,7 +2998,7 @@ namespace MtApi5 Params = new ArrayList(parameters), ParamsType = ICustomRequest.ParametersType.Double }); - Log.Debug($"iCustom: response = {response}."); + Log?.Debug($"iCustom: response = {response}."); return response; } @@ -3010,7 +3011,7 @@ namespace MtApi5 ///input-parameters of a custom indicator. If there is no parameters specified, then default values will be used. public int iCustom(string symbol, ENUM_TIMEFRAMES period, string name, int[] parameters) { - Log.Debug("iCustom: called."); + Log?.Debug("iCustom: called."); var response = SendRequest(new ICustomRequest { Symbol = symbol, @@ -3019,7 +3020,7 @@ namespace MtApi5 Params = new ArrayList(parameters), ParamsType = ICustomRequest.ParametersType.Int }); - Log.Debug($"iCustom: response = {response}."); + Log?.Debug($"iCustom: response = {response}."); return response; } @@ -3032,7 +3033,7 @@ namespace MtApi5 ///input-parameters of a custom indicator. If there is no parameters specified, then default values will be used. public int iCustom(string symbol, ENUM_TIMEFRAMES period, string name, string[] parameters) { - Log.Debug("iCustom: called."); + Log?.Debug("iCustom: called."); var response = SendRequest(new ICustomRequest { Symbol = symbol, @@ -3041,7 +3042,7 @@ namespace MtApi5 Params = new ArrayList(parameters), ParamsType = ICustomRequest.ParametersType.Int }); - Log.Debug($"iCustom: response = {response}."); + Log?.Debug($"iCustom: response = {response}."); return response; } @@ -3054,7 +3055,7 @@ namespace MtApi5 ///input-parameters of a custom indicator. If there is no parameters specified, then default values will be used. public int iCustom(string symbol, ENUM_TIMEFRAMES period, string name, bool[] parameters) { - Log.Debug("iCustom: called."); + Log?.Debug("iCustom: called."); var response = SendRequest(new ICustomRequest { Symbol = symbol, @@ -3063,7 +3064,7 @@ namespace MtApi5 Params = new ArrayList(parameters), ParamsType = ICustomRequest.ParametersType.Int }); - Log.Debug($"iCustom: response = {response}."); + Log?.Debug($"iCustom: response = {response}."); return response; } @@ -3076,9 +3077,9 @@ namespace MtApi5 /// public DateTime TimeCurrent() { - Log.Debug("TimeCurrent: called."); + Log?.Debug("TimeCurrent: called."); var response = SendCommand(Mt5CommandType.TimeCurrent, null); - Log.Debug($"TimeCurrent: response = {response}."); + Log?.Debug($"TimeCurrent: response = {response}."); return Mt5TimeConverter.ConvertFromMtTime(response); } @@ -3087,9 +3088,9 @@ namespace MtApi5 /// public DateTime TimeTradeServer() { - Log.Debug("TimeTradeServer: called."); + Log?.Debug("TimeTradeServer: called."); var response = SendCommand(Mt5CommandType.TimeTradeServer, null); - Log.Debug($"TimeTradeServer: response = {response}."); + Log?.Debug($"TimeTradeServer: response = {response}."); return Mt5TimeConverter.ConvertFromMtTime(response); } @@ -3098,9 +3099,9 @@ namespace MtApi5 /// public DateTime TimeLocal() { - Log.Debug("TimeLocal: called."); + Log?.Debug("TimeLocal: called."); var response = SendCommand(Mt5CommandType.TimeLocal, null); - Log.Debug($"TimeLocal: response = {response}."); + Log?.Debug($"TimeLocal: response = {response}."); return Mt5TimeConverter.ConvertFromMtTime(response); } @@ -3109,9 +3110,9 @@ namespace MtApi5 /// public DateTime TimeGMT() { - Log.Debug("TimeGMT: called."); + Log?.Debug("TimeGMT: called."); var response = SendCommand(Mt5CommandType.TimeGMT, null); - Log.Debug($"TimeGMT: response = {response}."); + Log?.Debug($"TimeGMT: response = {response}."); return Mt5TimeConverter.ConvertFromMtTime(response); } @@ -3315,6 +3316,7 @@ namespace MtApi5 public event EventHandler? OnBookEvent; public event EventHandler? OnLastTimeBar; public event EventHandler? OnLockTicks; + public event EventHandler>? QuoteList; #endregion #region Private Methods @@ -3346,18 +3348,19 @@ namespace MtApi5 ConnectionStateChanged?.Invoke(this, new Mt5ConnectionEventArgs(Mt5ConnectionState.Connecting, message)); var client = new MtRpcClient(host, port); + client.MessageReceived += _client_OnMessageReceived; + client.ConnectionFailed += _client_OnConnectionFailed; + client.Disconnected += _client_Disconnected; + var state = Mt5ConnectionState.Failed; try { await client.Connect(); - client.MessageReceived += _client_OnMessageReceived; - client.ConnectionFailed += _client_OnConnectionFailed; - state = Mt5ConnectionState.Connected; } catch (Exception e) { - Log.Warn($"Failed connection to {host}:{port}. {e.Message}"); + Log?.Warn($"Failed connection to {host}:{port}. {e.Message}"); } lock (_locker) @@ -3365,28 +3368,30 @@ namespace MtApi5 if (state == Mt5ConnectionState.Connected) { _client = client; - Log.Info($"Connected to {host}:{port}"); + Log?.Info($"Connected to {host}:{port}"); } _connectionState = state; } + if (state == Mt5ConnectionState.Connected) + OnConnected(); + ConnectionStateChanged?.Invoke(this, new Mt5ConnectionEventArgs(state, message)); - if (state == Mt5ConnectionState.Connected) - { - OnConnected(); - } + Log?.Info($"Connected finished"); } private void _client_OnMessageReceived(object? o, MtMessage msg) { + Log?.Debug($"Message received: {msg}"); + Task.Run(() => { switch (msg.MsgType) { case MessageType.ExpertList: - //ProcessExpertList(msg as MtExpertListMsg); + ProcessExpertList(msg as MtExpertListMsg); break; case MessageType.ExpertAdded: //ProcessExpertAdded(msg as MtExpertAddedMsg); @@ -3398,18 +3403,79 @@ namespace MtApi5 //ProcessEvent(msg as MtEvent); break; case MessageType.Response: - //ProcessResponse(msg as MtResponse); + ProcessResponse(msg as MtResponse); break; } }); } - private void _client_OnConnectionFailed(object? sender, EventArgs e) + private void ProcessExpertList(MtExpertListMsg? msg) { + if (msg == null) + return; + + HashSet experts = msg.Experts; + if (experts == null || experts.Count == 0) + { + Console.WriteLine($"ProcessExpertList: expert list invalid"); + return; + } + + lock(_locker) + { + _experts = experts; + } + + Dictionary quotes = []; + foreach (var handle in experts) + { + Console.WriteLine($"ProcessExpertList: {handle}"); + var quote = GetQuote(handle); + if (quote != null) + quotes[handle] = quote; + } + lock (_locker) { - Disconnect(true); + _quotes= quotes; } + _quotesWaiter.Set(); + + QuoteList?.Invoke(this, _quotes.Values.ToList()); + } + + private void ProcessResponse(MtResponse? msg) + { + if (msg == null) + return; + + var handle = msg.ExpertHandle; + var commandId = msg.CommandId; + var payload = msg.Payload; + + Log?.Debug($"ProcessResponse: {handle}, {commandId}, [{payload}]"); + + lock (_locker) + { + if (_tasks.TryGetValue(commandId, out CommandTask? value)) + value.SetResponse(payload); + } + } + + private Mt5Quote? GetQuote(int expertHandle) + { + var response = SendCommand(expertHandle, Mt5CommandType.GetQuote); + return response; + } + + private void _client_OnConnectionFailed(object? sender, EventArgs e) + { + Disconnect(true); + } + + private void _client_Disconnected(object? sender, EventArgs e) + { + Disconnect(false); } //private void _client_MtEventReceived(MtEvent e) @@ -3493,15 +3559,92 @@ namespace MtApi5 _connectionState = state; client = _client; _client = null; + + _quotes.Clear(); + _experts.Clear(); + _tasks.Clear(); } - client?.Disconnect(); //TODO: use dispose + client?.Disconnect(); - Log.Info(message); + Log?.Info(message); ConnectionStateChanged?.Invoke(this, new Mt5ConnectionEventArgs(state, message)); } + internal class CommandTask + { + private readonly EventWaitHandle responseWaiter_ = new AutoResetEvent(false); + private string? response_; + private readonly object locker_ = new(); + + public string? WaitResponse(int time) + { + responseWaiter_.WaitOne(time); + lock (locker_) + { + return response_; + } + } + + public void SetResponse(string result) + { + lock (locker_) + { + response_ = result; + } + responseWaiter_.Set(); + } + } + + private T? SendCommand(int expertHandle, Mt5CommandType commandType, object? payload = null) + { + var client = Client; + if (client == null) + { + Log?.Warn("SendCommand: No connection"); + throw new Exception("No connection"); + } + + var payloadJson = JsonConvert.SerializeObject(payload); + + MtCommand command = new(expertHandle, (int)commandType, _commandId++, payloadJson); + _tasks[command.CommandId] = new CommandTask(); + + Log?.Debug($"SendCommand: {command.CommandId}"); + client.Send(command); + + var responseJson = _tasks[command.CommandId].WaitResponse(10000); // 10 sec + if (_tasks.Remove(command.CommandId) == false) + { + Log?.Warn($"SendCommand: task {command.CommandId} is not found in collection"); + return default; + } + + Log?.Debug($"SendCommand: received response JSON [{responseJson}]"); + + if (string.IsNullOrEmpty(responseJson)) + { + Log?.Warn("SendCommand: Response JSON from MetaTrader is null or empty"); + throw new ExecutionException(ErrorCode.ErrCustom, "Response from MetaTrader is null"); + } + + var response = JsonConvert.DeserializeObject>(responseJson); + if (response == null) + { + Log?.Warn("SendCommand: Failed to deserialize response from JSON"); + throw new ExecutionException(ErrorCode.ErrCustom, "Response from MetaTrader is null"); + } + + if (response.ErrorCode != 0) + { + Log?.Warn($"SendCommand: ErrorCode = {response.ErrorCode}. {response}"); + throw new ExecutionException((ErrorCode)response.ErrorCode, response.ToString()); + } + + return (response.Value == null) ? default : response.Value; + } + private T SendCommand(Mt5CommandType commandType, ArrayList? commandParameters, Dictionary? namedParams = null, int? executor = null) { T r; @@ -3510,7 +3653,7 @@ namespace MtApi5 //var client = Client; //if (client == null) //{ - // Log.Warn("SendCommand: No connection"); + // Log?.Warn("SendCommand: No connection"); // throw new Exception("No connection"); //} @@ -3520,19 +3663,19 @@ namespace MtApi5 //} //catch (CommunicationException ex) //{ - // Log.Warn($"SendCommand: {ex.Message}"); + // Log?.Warn($"SendCommand: {ex.Message}"); // throw new Exception(ex.Message, ex); //} //if (response == null) //{ - // Log.Warn("SendCommand: Response from MetaTrader is null"); + // Log?.Warn("SendCommand: Response from MetaTrader is null"); // throw new ExecutionException(ErrorCode.ErrCustom, "Response from MetaTrader is null"); //} //if (response.ErrorCode != 0) //{ - // Log.Warn($"SendCommand: ErrorCode = {response.ErrorCode}. {response}"); + // Log?.Warn($"SendCommand: ErrorCode = {response.ErrorCode}. {response}"); // throw new ExecutionException((ErrorCode)response.ErrorCode, response.ToString()); //} @@ -3557,14 +3700,14 @@ namespace MtApi5 if (res == null) { - Log.Warn("SendRequest: Response from MetaTrader is null"); + Log?.Warn("SendRequest: Response from MetaTrader is null"); throw new ExecutionException(ErrorCode.ErrCustom, "Response from MetaTrader is null"); } var response = JsonConvert.DeserializeObject>(res) ?? throw new Exception("Failed to deserialize response"); if (response.ErrorCode != 0) { - Log.Warn($"SendRequest: ErrorCode = {response.ErrorCode}. {response}"); + Log?.Warn($"SendRequest: ErrorCode = {response.ErrorCode}. {response}"); throw new ExecutionException((ErrorCode)response.ErrorCode, response.ErrorMessage); } @@ -3607,12 +3750,18 @@ namespace MtApi5 private void OnConnected() { + Log?.Debug("OnConnected: begin"); + + Client?.Send(new MtClientReady()); + _isBacktestingMode = IsTesting(); if (_isBacktestingMode) { BacktestingReady(); } + + Log?.Debug("OnConnected: finished"); } private void BacktestingReady() diff --git a/MtApi5/MtConverters.cs b/MtApi5/MtConverters.cs index d6cbcdad..d47162d5 100755 --- a/MtApi5/MtConverters.cs +++ b/MtApi5/MtConverters.cs @@ -4,14 +4,10 @@ namespace MtApi5 { internal static class MtConverters { - private static readonly MtLog Log = LogConfigurator.GetLogger(typeof(MtConverters)); - #region Values Converters public static bool ParseResult(this string inputString, char separator, out double result) { - Log.Debug($"ParseResult: inputString = {inputString}, separator = {separator}"); - var retVal = false; result = 0; @@ -26,25 +22,18 @@ namespace MtApi5 result = double.Parse(values[1]); } - catch (Exception ex) + catch (Exception) { retVal = false; - Log.Error($"ParseResult: {ex.Message}"); } } } - else - { - Log.Warn("ParseResult: input srting is null or empty!"); - } return retVal; } public static bool ParseResult(this string inputString, char separator, out DateTime from, out DateTime to) { - Log.Debug($"ParseResult: inputString = {inputString}, separator = {separator}"); - var retVal = false; from = new DateTime(); @@ -65,17 +54,12 @@ namespace MtApi5 var iTo= int.Parse(values[2]); to = Mt5TimeConverter.ConvertFromMtTime(iTo); } - catch (Exception ex) + catch (Exception) { retVal = false; - Log.Error($"ParseResult: {ex.Message}"); } } } - else - { - Log.Warn("ParseResult: input srting is null or empty!"); - } return retVal; } diff --git a/MtApi5/Requests/Response.cs b/MtApi5/Requests/Response.cs index cc338339..668a0890 100644 --- a/MtApi5/Requests/Response.cs +++ b/MtApi5/Requests/Response.cs @@ -5,6 +5,6 @@ public int ErrorCode { get; set; } public string? ErrorMessage { get; set; } - public required T Value { get; set; } + public T? Value { get; set; } } } \ No newline at end of file