diff --git a/MtApi5/MtApi5Client.cs b/MtApi5/MtApi5Client.cs index b75bd2a0..e68b4958 100755 --- a/MtApi5/MtApi5Client.cs +++ b/MtApi5/MtApi5Client.cs @@ -382,6 +382,14 @@ public int PositionsTotal() return SendCommand(ExecutorHandle, Mt5CommandType.PositionsTotal); } + /// + ///Asynchronous variant of PositionsTotal; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + public Task PositionsTotalAsync() + { + return SendCommandAsync(ExecutorHandle, Mt5CommandType.PositionsTotal); + } + /// ///Returns the symbol corresponding to the open position and automatically selects the position for further working with it using functions PositionGetDouble, PositionGetInteger, PositionGetString. /// @@ -460,6 +468,14 @@ public int OrdersTotal() return SendCommand(ExecutorHandle, Mt5CommandType.OrdersTotal); } + /// + ///Asynchronous variant of OrdersTotal; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + public Task OrdersTotalAsync() + { + return SendCommandAsync(ExecutorHandle, Mt5CommandType.OrdersTotal); + } + /// ///Returns the number of current orders. /// @@ -874,6 +890,16 @@ public double AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE propertyId) return SendCommand(ExecutorHandle, Mt5CommandType.AccountInfoDouble, cmdParams); } + /// + ///Asynchronous variant of AccountInfoDouble; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Identifier of the property. + public Task AccountInfoDoubleAsync(ENUM_ACCOUNT_INFO_DOUBLE propertyId) + { + Dictionary cmdParams = new() { { "PropertyId", propertyId } }; + return SendCommandAsync(ExecutorHandle, Mt5CommandType.AccountInfoDouble, cmdParams); + } + /// ///Returns the value of the corresponding account property. /// @@ -884,6 +910,16 @@ public long AccountInfoInteger(ENUM_ACCOUNT_INFO_INTEGER propertyId) return SendCommand(ExecutorHandle, Mt5CommandType.AccountInfoInteger, cmdParams); } + /// + ///Asynchronous variant of AccountInfoInteger; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Identifier of the property. + public Task AccountInfoIntegerAsync(ENUM_ACCOUNT_INFO_INTEGER propertyId) + { + Dictionary cmdParams = new() { { "PropertyId", propertyId } }; + return SendCommandAsync(ExecutorHandle, Mt5CommandType.AccountInfoInteger, cmdParams); + } + /// ///Returns the value of the corresponding account property. /// @@ -1013,7 +1049,22 @@ public int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, int startPos, ratesArray = response?.ToArray() ?? []; return response?.Count ?? 0; } - + + /// + ///Asynchronous variant of CopyRates; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + ///Period. + ///The start position for the first element to copy. + ///Data count to copy. + public async Task CopyRatesAsync(string symbolName, ENUM_TIMEFRAMES timeframe, int startPos, int count) + { + Dictionary cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Timeframe", (int)timeframe }, + { "StartPos", startPos }, { "Count", count } }; + var response = await SendCommandAsync>(ExecutorHandle, Mt5CommandType.CopyRates, cmdParams).ConfigureAwait(false); + return response?.ToArray() ?? []; + } + /// ///Gets history data of MqlRates structure of a specified symbol-period in specified quantity into the ratesArray array. The elements ordering of the copied data is from present to the past, i.e., starting position of 0 means the current bar. /// @@ -1031,6 +1082,21 @@ public int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime star return response?.Count ?? 0; } + /// + ///Asynchronous variant of CopyRates; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + ///Period. + ///The start time for the first element to copy. + ///Data count to copy. + public async Task CopyRatesAsync(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime startTime, int count) + { + Dictionary cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Timeframe", (int)timeframe }, + { "StartTime", Mt5TimeConverter.ConvertToMtTime(startTime) }, { "Count", count } }; + var response = await SendCommandAsync>(ExecutorHandle, Mt5CommandType.CopyRates1, cmdParams).ConfigureAwait(false); + return response?.ToArray() ?? []; + } + /// ///Gets history data of MqlRates structure of a specified symbol-period in specified quantity into the ratesArray array. The elements ordering of the copied data is from present to the past, i.e., starting position of 0 means the current bar. /// @@ -1049,6 +1115,22 @@ public int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime star return response?.Count ?? 0; } + /// + ///Asynchronous variant of CopyRates; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + ///Period. + ///The start time for the first element to copy. + ///Bar time, corresponding to the last element to copy. + public async Task CopyRatesAsync(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime startTime, DateTime stopTime) + { + Dictionary cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Timeframe", (int)timeframe }, + { "StartTime", Mt5TimeConverter.ConvertToMtTime(startTime) }, + { "StopTime", Mt5TimeConverter.ConvertToMtTime(stopTime) } }; + var response = await SendCommandAsync>(ExecutorHandle, Mt5CommandType.CopyRates2, cmdParams).ConfigureAwait(false); + return response?.ToArray() ?? []; + } + /// ///The function gets to time_array history data of bar opening time for the specified symbol-period pair in the specified quantity. It should be noted that elements ordering is from present to past, i.e., starting position of 0 means the current bar. /// @@ -1504,6 +1586,22 @@ public int CopySpread(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime sta return ticks; } + /// + ///Asynchronous variant of CopyTicks; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + ///The flag that determines the type of received ticks. + ///The date from which you want to request ticks. In milliseconds since 1970.01.01. If from=0, the last count ticks will be returned. + ///The number of ticks that you want to receive. If the 'from' and 'count' parameters are not specified, all available recent ticks (but not more than 2000) will be written to result. + /// + public async Task?> CopyTicksAsync(string symbolName, CopyTicksFlag flags = CopyTicksFlag.All, ulong from = 0, uint count = 0) + { + Dictionary cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Flags", flags }, + { "From", from }, { "Count", count } }; + var response = await SendCommandAsync>(ExecutorHandle, Mt5CommandType.CopyTicks, cmdParams).ConfigureAwait(false); + return response?.Select(t => new MqlTick(t)).ToList(); + } + /// ///The function returns the handle of a specified technical indicator created based on the array of parameters of MqlParam type. /// @@ -1583,6 +1681,17 @@ public double SymbolInfoDouble(string symbolName, ENUM_SYMBOL_INFO_DOUBLE propId return SendCommand(ExecutorHandle, Mt5CommandType.SymbolInfoDouble, cmdParams); } + /// + ///Asynchronous variant of SymbolInfoDouble; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + ///Identifier of a symbol property. + public Task SymbolInfoDoubleAsync(string symbolName, ENUM_SYMBOL_INFO_DOUBLE propId) + { + Dictionary cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "PropId", (int)propId } }; + return SendCommandAsync(ExecutorHandle, Mt5CommandType.SymbolInfoDouble, cmdParams); + } + /// ///Returns the corresponding property of a specified symbol. /// @@ -1594,6 +1703,17 @@ public long SymbolInfoInteger(string symbolName, ENUM_SYMBOL_INFO_INTEGER propId return SendCommand(ExecutorHandle, Mt5CommandType.SymbolInfoInteger, cmdParams); } + /// + ///Asynchronous variant of SymbolInfoInteger; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + ///Identifier of a symbol property. + public Task SymbolInfoIntegerAsync(string symbolName, ENUM_SYMBOL_INFO_INTEGER propId) + { + Dictionary cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "PropId", (int)propId } }; + return SendCommandAsync(ExecutorHandle, Mt5CommandType.SymbolInfoInteger, cmdParams); + } + /// ///Returns the corresponding property of a specified symbol. /// @@ -1645,6 +1765,17 @@ public bool SymbolInfoTick(string symbol, out MqlTick? tick) return null; } + /// + ///Asynchronous variant of SymbolInfoTick; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + ///Symbol name. + public async Task SymbolInfoTickAsync(string symbol) + { + Dictionary cmdParams = new() { { "Symbol", symbol ?? string.Empty } }; + var response = await SendCommandAsync>(ExecutorHandle, Mt5CommandType.SymbolInfoTick, cmdParams).ConfigureAwait(false); + return response != null && response.RetVal && response.Result != null ? new MqlTick(response.Result) : null; + } + /// ///Allows receiving time of beginning and end of the specified quoting sessions for a specified symbol and weekday. /// @@ -3158,6 +3289,15 @@ public DateTime TimeCurrent() return Mt5TimeConverter.ConvertFromMtTime(response); } + /// + ///Asynchronous variant of TimeCurrent; multiple in-flight commands are pipelined and typically served in a single expert wake-up. + /// + public async Task TimeCurrentAsync() + { + var response = await SendCommandAsync(ExecutorHandle, Mt5CommandType.TimeCurrent).ConfigureAwait(false); + return Mt5TimeConverter.ConvertFromMtTime(response); + } + /// ///Returns the calculated current time of the trade server. Unlike TimeCurrent(), the calculation of the time value is performed in the client terminal and depends on the time settings on your computer. /// @@ -3627,6 +3767,44 @@ private void Disconnect(bool failed) return (response.Value == null) ? default : response.Value; } + private async Task SendCommandAsync(int expertHandle, Mt5CommandType commandType, object? payload = null) + { + var client = Client; + if (client == null) + { + Log.Warn("SendCommandAsync: No connection"); + throw new Exception("No connection"); + } + + var payloadJson = payload == null ? string.Empty : JsonConvert.SerializeObject(payload); + Log.Debug($"SendCommandAsync: sending '{payloadJson}' ..."); + + var responseJson = await client.SendCommandAsync(expertHandle, (int)commandType, payloadJson, CommandTimeout).ConfigureAwait(false); + + Log.Debug($"SendCommandAsync: received response JSON [{responseJson}]"); + + if (string.IsNullOrEmpty(responseJson)) + { + Log.Warn("SendCommandAsync: 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("SendCommandAsync: Failed to deserialize response from JSON"); + throw new ExecutionException(ErrorCode.ErrCustom, "Response from MetaTrader is null"); + } + + if (response.ErrorCode != 0) + { + Log.Warn($"SendCommandAsync: ErrorCode = {response.ErrorCode}. {response.ErrorMessage}"); + throw new ExecutionException((ErrorCode)response.ErrorCode, response.ErrorMessage); + } + + return (response.Value == null) ? default : response.Value; + } + private void BacktestingReady() { SendCommand(ExecutorHandle, Mt5CommandType.BacktestingReady); diff --git a/MtClient/MtRpcClient.cs b/MtClient/MtRpcClient.cs index a85aee34..3fae97e3 100755 --- a/MtClient/MtRpcClient.cs +++ b/MtClient/MtRpcClient.cs @@ -87,6 +87,28 @@ public async void Disconnect() return response; } + public async Task SendCommandAsync(int expertHandle, int commandType, string payload, int timeout = 10000) // 10 sec + { + CommandTask commandTask = new(); + int commandId; + lock (tasks_) + { + commandId = nextCommandId++; + tasks_[commandId] = commandTask; + } + + MtCommand command = new(expertHandle, commandType, commandId, payload); + Send(command); + + var response = await commandTask.WaitResponseAsync(timeout).ConfigureAwait(false); + lock (tasks_) + { + tasks_.Remove(commandId); + } + + return response; + } + public HashSet? RequestExpertsList() { CommandTask requestTask = new(); @@ -309,6 +331,9 @@ private void ProcessExpertList(HashSet experts) internal class CommandTask { private readonly EventWaitHandle responseWaiter_ = new AutoResetEvent(false); + // RunContinuationsAsynchronously: the response arrives on the receive thread, so awaiter + // continuations must not run inline on it and block processing of further messages. + private readonly TaskCompletionSource responseSource_ = new(TaskCreationOptions.RunContinuationsAsynchronously); private T? response_; private readonly object locker_ = new(); @@ -321,6 +346,20 @@ internal class CommandTask } } + public async Task WaitResponseAsync(int time) + { + using var timeoutCancellation = new CancellationTokenSource(); + var completed = await Task.WhenAny(responseSource_.Task, Task.Delay(time, timeoutCancellation.Token)).ConfigureAwait(false); + if (completed == responseSource_.Task) + timeoutCancellation.Cancel(); + + // Mirrors WaitResponse: on timeout the current response value (default if none arrived) is returned. + lock (locker_) + { + return response_; + } + } + public void SetResponse(T result) { lock (locker_) @@ -328,6 +367,7 @@ public void SetResponse(T result) response_ = result; } responseWaiter_.Set(); + responseSource_.TrySetResult(result); } } diff --git a/TestClients/MtApi5TestClient/MainWindow.xaml b/TestClients/MtApi5TestClient/MainWindow.xaml index c30fe43e..1515a08f 100755 --- a/TestClients/MtApi5TestClient/MainWindow.xaml +++ b/TestClients/MtApi5TestClient/MainWindow.xaml @@ -599,6 +599,7 @@