Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 179 additions & 1 deletion MtApi5/MtApi5Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,14 @@ public int PositionsTotal()
return SendCommand<int>(ExecutorHandle, Mt5CommandType.PositionsTotal);
}

///<summary>
///Asynchronous variant of PositionsTotal; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
public Task<int> PositionsTotalAsync()
{
return SendCommandAsync<int>(ExecutorHandle, Mt5CommandType.PositionsTotal);
}

///<summary>
///Returns the symbol corresponding to the open position and automatically selects the position for further working with it using functions PositionGetDouble, PositionGetInteger, PositionGetString.
///</summary>
Expand Down Expand Up @@ -460,6 +468,14 @@ public int OrdersTotal()
return SendCommand<int>(ExecutorHandle, Mt5CommandType.OrdersTotal);
}

///<summary>
///Asynchronous variant of OrdersTotal; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
public Task<int> OrdersTotalAsync()
{
return SendCommandAsync<int>(ExecutorHandle, Mt5CommandType.OrdersTotal);
}

///<summary>
///Returns the number of current orders.
///</summary>
Expand Down Expand Up @@ -874,6 +890,16 @@ public double AccountInfoDouble(ENUM_ACCOUNT_INFO_DOUBLE propertyId)
return SendCommand<double>(ExecutorHandle, Mt5CommandType.AccountInfoDouble, cmdParams);
}

///<summary>
///Asynchronous variant of AccountInfoDouble; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="propertyId">Identifier of the property.</param>
public Task<double> AccountInfoDoubleAsync(ENUM_ACCOUNT_INFO_DOUBLE propertyId)
{
Dictionary<string, object> cmdParams = new() { { "PropertyId", propertyId } };
return SendCommandAsync<double>(ExecutorHandle, Mt5CommandType.AccountInfoDouble, cmdParams);
}

///<summary>
///Returns the value of the corresponding account property.
///</summary>
Expand All @@ -884,6 +910,16 @@ public long AccountInfoInteger(ENUM_ACCOUNT_INFO_INTEGER propertyId)
return SendCommand<long>(ExecutorHandle, Mt5CommandType.AccountInfoInteger, cmdParams);
}

///<summary>
///Asynchronous variant of AccountInfoInteger; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="propertyId">Identifier of the property.</param>
public Task<long> AccountInfoIntegerAsync(ENUM_ACCOUNT_INFO_INTEGER propertyId)
{
Dictionary<string, object> cmdParams = new() { { "PropertyId", propertyId } };
return SendCommandAsync<long>(ExecutorHandle, Mt5CommandType.AccountInfoInteger, cmdParams);
}

///<summary>
///Returns the value of the corresponding account property.
///</summary>
Expand Down Expand Up @@ -1013,7 +1049,22 @@ public int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, int startPos,
ratesArray = response?.ToArray() ?? [];
return response?.Count ?? 0;
}


///<summary>
///Asynchronous variant of CopyRates; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbolName">Symbol name.</param>
///<param name="timeframe">Period.</param>
///<param name="startPos">The start position for the first element to copy.</param>
///<param name="count">Data count to copy.</param>
public async Task<MqlRates[]> CopyRatesAsync(string symbolName, ENUM_TIMEFRAMES timeframe, int startPos, int count)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Timeframe", (int)timeframe },
{ "StartPos", startPos }, { "Count", count } };
var response = await SendCommandAsync<List<MqlRates>>(ExecutorHandle, Mt5CommandType.CopyRates, cmdParams).ConfigureAwait(false);
return response?.ToArray() ?? [];
}

///<summary>
///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.
///</summary>
Expand All @@ -1031,6 +1082,21 @@ public int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime star
return response?.Count ?? 0;
}

///<summary>
///Asynchronous variant of CopyRates; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbolName">Symbol name.</param>
///<param name="timeframe">Period.</param>
///<param name="startTime">The start time for the first element to copy.</param>
///<param name="count">Data count to copy.</param>
public async Task<MqlRates[]> CopyRatesAsync(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime startTime, int count)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Timeframe", (int)timeframe },
{ "StartTime", Mt5TimeConverter.ConvertToMtTime(startTime) }, { "Count", count } };
var response = await SendCommandAsync<List<MqlRates>>(ExecutorHandle, Mt5CommandType.CopyRates1, cmdParams).ConfigureAwait(false);
return response?.ToArray() ?? [];
}

///<summary>
///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.
///</summary>
Expand All @@ -1049,6 +1115,22 @@ public int CopyRates(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime star
return response?.Count ?? 0;
}

///<summary>
///Asynchronous variant of CopyRates; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbolName">Symbol name.</param>
///<param name="timeframe">Period.</param>
///<param name="startTime">The start time for the first element to copy.</param>
///<param name="stopTime">Bar time, corresponding to the last element to copy.</param>
public async Task<MqlRates[]> CopyRatesAsync(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime startTime, DateTime stopTime)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Timeframe", (int)timeframe },
{ "StartTime", Mt5TimeConverter.ConvertToMtTime(startTime) },
{ "StopTime", Mt5TimeConverter.ConvertToMtTime(stopTime) } };
var response = await SendCommandAsync<List<MqlRates>>(ExecutorHandle, Mt5CommandType.CopyRates2, cmdParams).ConfigureAwait(false);
return response?.ToArray() ?? [];
}

///<summary>
///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.
///</summary>
Expand Down Expand Up @@ -1504,6 +1586,22 @@ public int CopySpread(string symbolName, ENUM_TIMEFRAMES timeframe, DateTime sta
return ticks;
}

///<summary>
///Asynchronous variant of CopyTicks; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbolName">Symbol name.</param>
///<param name="flags">The flag that determines the type of received ticks.</param>
///<param name="from">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.</param>
///<param name="count">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.</param>
///<see href="https://www.mql5.com/en/docs/series/copyticks"/>
public async Task<List<MqlTick>?> CopyTicksAsync(string symbolName, CopyTicksFlag flags = CopyTicksFlag.All, ulong from = 0, uint count = 0)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "Flags", flags },
{ "From", from }, { "Count", count } };
var response = await SendCommandAsync<List<MtTick>>(ExecutorHandle, Mt5CommandType.CopyTicks, cmdParams).ConfigureAwait(false);
return response?.Select(t => new MqlTick(t)).ToList();
}

///<summary>
///The function returns the handle of a specified technical indicator created based on the array of parameters of MqlParam type.
///</summary>
Expand Down Expand Up @@ -1583,6 +1681,17 @@ public double SymbolInfoDouble(string symbolName, ENUM_SYMBOL_INFO_DOUBLE propId
return SendCommand<double>(ExecutorHandle, Mt5CommandType.SymbolInfoDouble, cmdParams);
}

///<summary>
///Asynchronous variant of SymbolInfoDouble; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbolName">Symbol name.</param>
///<param name="propId">Identifier of a symbol property.</param>
public Task<double> SymbolInfoDoubleAsync(string symbolName, ENUM_SYMBOL_INFO_DOUBLE propId)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "PropId", (int)propId } };
return SendCommandAsync<double>(ExecutorHandle, Mt5CommandType.SymbolInfoDouble, cmdParams);
}

///<summary>
///Returns the corresponding property of a specified symbol.
///</summary>
Expand All @@ -1594,6 +1703,17 @@ public long SymbolInfoInteger(string symbolName, ENUM_SYMBOL_INFO_INTEGER propId
return SendCommand<long>(ExecutorHandle, Mt5CommandType.SymbolInfoInteger, cmdParams);
}

///<summary>
///Asynchronous variant of SymbolInfoInteger; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbolName">Symbol name.</param>
///<param name="propId">Identifier of a symbol property.</param>
public Task<long> SymbolInfoIntegerAsync(string symbolName, ENUM_SYMBOL_INFO_INTEGER propId)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbolName ?? string.Empty }, { "PropId", (int)propId } };
return SendCommandAsync<long>(ExecutorHandle, Mt5CommandType.SymbolInfoInteger, cmdParams);
}

///<summary>
///Returns the corresponding property of a specified symbol.
///</summary>
Expand Down Expand Up @@ -1645,6 +1765,17 @@ public bool SymbolInfoTick(string symbol, out MqlTick? tick)
return null;
}

///<summary>
///Asynchronous variant of SymbolInfoTick; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
///<param name="symbol">Symbol name.</param>
public async Task<MqlTick?> SymbolInfoTickAsync(string symbol)
{
Dictionary<string, object> cmdParams = new() { { "Symbol", symbol ?? string.Empty } };
var response = await SendCommandAsync<FuncResult<MtTick>>(ExecutorHandle, Mt5CommandType.SymbolInfoTick, cmdParams).ConfigureAwait(false);
return response != null && response.RetVal && response.Result != null ? new MqlTick(response.Result) : null;
}

///<summary>
///Allows receiving time of beginning and end of the specified quoting sessions for a specified symbol and weekday.
///</summary>
Expand Down Expand Up @@ -3158,6 +3289,15 @@ public DateTime TimeCurrent()
return Mt5TimeConverter.ConvertFromMtTime(response);
}

///<summary>
///Asynchronous variant of TimeCurrent; multiple in-flight commands are pipelined and typically served in a single expert wake-up.
///</summary>
public async Task<DateTime> TimeCurrentAsync()
{
var response = await SendCommandAsync<long>(ExecutorHandle, Mt5CommandType.TimeCurrent).ConfigureAwait(false);
return Mt5TimeConverter.ConvertFromMtTime(response);
}

///<summary>
///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.
///</summary>
Expand Down Expand Up @@ -3627,6 +3767,44 @@ private void Disconnect(bool failed)
return (response.Value == null) ? default : response.Value;
}

private async Task<T?> SendCommandAsync<T>(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<Response<T>>(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<object>(ExecutorHandle, Mt5CommandType.BacktestingReady);
Expand Down
40 changes: 40 additions & 0 deletions MtClient/MtRpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,28 @@ public async void Disconnect()
return response;
}

public async Task<string?> SendCommandAsync(int expertHandle, int commandType, string payload, int timeout = 10000) // 10 sec
{
CommandTask<string> 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<int>? RequestExpertsList()
{
CommandTask<object> requestTask = new();
Expand Down Expand Up @@ -309,6 +331,9 @@ private void ProcessExpertList(HashSet<int> experts)
internal class CommandTask<T>
{
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<T?> responseSource_ = new(TaskCreationOptions.RunContinuationsAsynchronously);
private T? response_;
private readonly object locker_ = new();

Expand All @@ -321,13 +346,28 @@ internal class CommandTask<T>
}
}

public async Task<T?> 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_)
{
response_ = result;
}
responseWaiter_.Set();
responseSource_.TrySetResult(result);
}
}

Expand Down
1 change: 1 addition & 0 deletions TestClients/MtApi5TestClient/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@
<Button Command="{Binding TimeTradeServerCommand}" Content="TimeTradeServer" Margin="2"/>
<Button Command="{Binding TimeLocalCommand}" Content="TimeLocal" Margin="2"/>
<Button Command="{Binding TimeGMTCommand}" Content="TimeGMT" Margin="2"/>
<Button Command="{Binding AsyncPipelineDemoCommand}" Content="AsyncPipelineDemo" Margin="2"/>
</WrapPanel>
</TabItem>

Expand Down
Loading