docker files fixes from liaqat
This commit is contained in:
51
src/Managing.Application/Workflows/FlowFactory.cs
Normal file
51
src/Managing.Application/Workflows/FlowFactory.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Workflows.Flows.Feeds;
|
||||
using Managing.Application.Workflows.Flows.Trading;
|
||||
using Managing.Domain.Workflows;
|
||||
using Managing.Domain.Workflows.Synthetics;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Workflows;
|
||||
|
||||
public class FlowFactory : IFlowFactory
|
||||
{
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public FlowFactory(IExchangeService exchangeService, ICacheService cacheService, ITradingService tradingService, IAccountService accountService)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_cacheService = cacheService;
|
||||
_tradingService = tradingService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
public IFlow BuildFlow(SyntheticFlow request)
|
||||
{
|
||||
IFlow flow = request.Type switch
|
||||
{
|
||||
FlowType.FeedTicker => new FeedTicker(_exchangeService),
|
||||
FlowType.RsiDivergence => new RsiDiv(),
|
||||
FlowType.OpenPosition => new OpenPosition(_exchangeService, _cacheService, _accountService, _tradingService),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
flow.Children = new List<IFlow>();
|
||||
flow.Parameters = new List<FlowParameter>();
|
||||
|
||||
foreach (var parameter in request.Parameters)
|
||||
{
|
||||
if (!flow.Parameters.Any(p => p.Name == parameter.Name)) {
|
||||
flow.Parameters.Add(new FlowParameter
|
||||
{
|
||||
Name = parameter.Name,
|
||||
Value = parameter.Value
|
||||
});
|
||||
}
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
64
src/Managing.Application/Workflows/Flows/Feeds/FeedTicker.cs
Normal file
64
src/Managing.Application/Workflows/Flows/Feeds/FeedTicker.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Workflows;
|
||||
using Newtonsoft.Json;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Workflows.Flows.Feeds;
|
||||
|
||||
public class FeedTicker : FlowBase
|
||||
{
|
||||
public override List<IFlow> Children { get; set; }
|
||||
public override List<FlowParameter> Parameters { get; set; }
|
||||
public override Guid ParentId { get; }
|
||||
public override Guid Id { get; }
|
||||
public override string Output { get; set; }
|
||||
public override string Name => "Feed Ticker";
|
||||
public override FlowType Type => FlowType.FeedTicker;
|
||||
public override string Description => "This flow will take a feed and output the candles";
|
||||
public FeedTickerParameters FeedTickerParameters { get; set; }
|
||||
public override List<FlowOutput> AcceptedInputs => new();
|
||||
public override List<FlowOutput> OutputTypes => new() { FlowOutput.Candles };
|
||||
private readonly IExchangeService _exchangeService;
|
||||
|
||||
public FeedTicker(IExchangeService exchangeService)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
}
|
||||
|
||||
public async override Task Execute(string input)
|
||||
{
|
||||
MapParameters();
|
||||
var candles = await _exchangeService.GetCandlesInflux(FeedTickerParameters.Exchange, FeedTickerParameters.Ticker, DateTime.Now.AddDays(-11), FeedTickerParameters.Timeframe);
|
||||
|
||||
Output = JsonConvert.SerializeObject(candles.ToHashSet());
|
||||
|
||||
if(Children != null && Children.Count > 0)
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
await child.Execute(Output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void MapParameters()
|
||||
{
|
||||
FeedTickerParameters = new FeedTickerParameters();
|
||||
foreach (var param in Parameters)
|
||||
{
|
||||
if (param.Name == nameof(FeedTickerParameters.Ticker))
|
||||
FeedTickerParameters.Ticker = (Ticker)Enum.Parse(typeof(Ticker), param.Value);
|
||||
if (param.Name == nameof(FeedTickerParameters.Exchange))
|
||||
FeedTickerParameters.Exchange = (TradingExchanges)Enum.Parse(typeof(TradingExchanges), param.Value);
|
||||
if (param.Name == nameof(FeedTickerParameters.Timeframe))
|
||||
FeedTickerParameters.Timeframe = (Timeframe)Enum.Parse(typeof(Timeframe), param.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FeedTickerParameters
|
||||
{
|
||||
public TradingExchanges Exchange { get; set; }
|
||||
public Ticker Ticker { get; set; }
|
||||
public Timeframe Timeframe { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Workflows;
|
||||
using Newtonsoft.Json;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Workflows.Flows.Feeds;
|
||||
|
||||
public class RsiDiv : FlowBase
|
||||
{
|
||||
public override List<IFlow> Children { get; set; }
|
||||
public override List<FlowParameter> Parameters { get; set; }
|
||||
public override Guid ParentId { get; }
|
||||
public override Guid Id { get; }
|
||||
public override string Output { get; set; }
|
||||
public override string Name => "Rsi Divergence";
|
||||
public override string Description => "This flow will take a feed of candle an run the RSI Divergence";
|
||||
public override FlowType Type => FlowType.RsiDivergence;
|
||||
public override List<FlowOutput> AcceptedInputs => new() { FlowOutput.Candles };
|
||||
public override List<FlowOutput> OutputTypes => new() { FlowOutput.Signal };
|
||||
public RsiDivParameters RsiDivParameters { get; set; }
|
||||
|
||||
public async override Task Execute(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(input)}' cannot be null or empty.", nameof(input));
|
||||
}
|
||||
|
||||
MapParameters();
|
||||
var candles = JsonConvert.DeserializeObject<HashSet<Candle>>(input);
|
||||
|
||||
var strategy = new RSIDivergenceStrategy(Name, RsiDivParameters.Timeframe, RsiDivParameters.Period);
|
||||
strategy.UpdateCandles(candles);
|
||||
strategy.Run();
|
||||
|
||||
Output = JsonConvert.SerializeObject(strategy.Signals);
|
||||
|
||||
if(Children != null && Children.Count > 0)
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
await child.Execute(Output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void MapParameters()
|
||||
{
|
||||
RsiDivParameters = new RsiDivParameters();
|
||||
foreach (var param in Parameters)
|
||||
{
|
||||
if (param.Name == nameof(RsiDivParameters.Period))
|
||||
RsiDivParameters.Period = int.Parse(param.Value);
|
||||
if (param.Name == nameof(RsiDivParameters.Timeframe))
|
||||
RsiDivParameters.Timeframe = (Timeframe)Enum.Parse(typeof(Timeframe), param.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RsiDivParameters
|
||||
{
|
||||
public int Period { get; set; }
|
||||
public Timeframe Timeframe { get; set; }
|
||||
}
|
||||
230
src/Managing.Application/Workflows/Flows/Trading/OpenPosition.cs
Normal file
230
src/Managing.Application/Workflows/Flows/Trading/OpenPosition.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Accounts;
|
||||
using Managing.Application.Shared;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Application.Trading;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Domain.Workflows;
|
||||
using Newtonsoft.Json;
|
||||
using static Managing.Common.Enums;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
|
||||
namespace Managing.Application.Workflows.Flows.Trading;
|
||||
|
||||
public class OpenPosition : FlowBase
|
||||
{
|
||||
public override List<IFlow> Children { get; set; }
|
||||
public override List<FlowParameter> Parameters { get; set; }
|
||||
public override Guid ParentId { get; }
|
||||
public override Guid Id { get; }
|
||||
public override string Output { get; set; }
|
||||
public override string Name => "Open Position";
|
||||
public override FlowType Type => FlowType.OpenPosition;
|
||||
public override string Description => "This flow will open a position for a given signal";
|
||||
public OpenPositionParameters OpenPositionParameters { get; set; }
|
||||
public override List<FlowOutput> AcceptedInputs => new() { FlowOutput.Signal, FlowOutput.MoneyManagement };
|
||||
public override List<FlowOutput> OutputTypes => new() { FlowOutput.Position };
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly ISettingsRepository _settingsRepository;
|
||||
private readonly IMessengerService _messengerService;
|
||||
|
||||
private readonly string POSITIONS_KEY = "positions";
|
||||
private readonly string ACCOUNT_KEY = "account";
|
||||
private readonly string CANDLES_KEY = "candles";
|
||||
private readonly string SIGNALS_KEY = "signals";
|
||||
private readonly string FEE_KEY = "fee";
|
||||
private readonly string WALLET_BALANCES = "wallet-balance";
|
||||
private decimal Fee { get; set; }
|
||||
|
||||
|
||||
public OpenPosition(
|
||||
IExchangeService exchangeService,
|
||||
ICacheService cacheService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_cacheService = cacheService;
|
||||
_accountService = accountService;
|
||||
_tradingService = tradingService;
|
||||
}
|
||||
|
||||
public async override Task Execute(string input)
|
||||
{
|
||||
MapParameters();
|
||||
var signal = JsonConvert.DeserializeObject<Signal>(input);
|
||||
var Candles = JsonConvert.DeserializeObject<HashSet<Candle>>(_cacheService.GetValue(CANDLES_KEY));
|
||||
var Account = JsonConvert.DeserializeObject<Account>(_cacheService.GetValue(ACCOUNT_KEY));
|
||||
var Positions = JsonConvert.DeserializeObject<List<Position>>(_cacheService.GetValue(POSITIONS_KEY));
|
||||
var Signals = JsonConvert.DeserializeObject<HashSet<Signal>>(_cacheService.GetValue(POSITIONS_KEY));
|
||||
|
||||
Fee = _cacheService.GetOrSave(FEE_KEY, () =>
|
||||
{
|
||||
return _tradingService.GetFee(Account, OpenPositionParameters.IsForBacktest);
|
||||
}, TimeSpan.FromDays(1));
|
||||
|
||||
await ExecuteOpenPosition(signal, Positions, Signals, Candles, Account);
|
||||
|
||||
_cacheService.SaveValue(POSITIONS_KEY, JsonConvert.SerializeObject(Positions));
|
||||
_cacheService.SaveValue(SIGNALS_KEY, JsonConvert.SerializeObject(Signals));
|
||||
|
||||
if (Children != null && Children.Count > 0)
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
await child.Execute(Output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteOpenPosition(Signal signal, List<Position> positions, HashSet<Signal> signals, HashSet<Candle> candles, Account account)
|
||||
{
|
||||
// Check if a position is already open
|
||||
var openedPosition = positions.FirstOrDefault(p => p.Status == PositionStatus.Filled
|
||||
&& p.SignalIdentifier != signal.Identifier);
|
||||
|
||||
var lastPrice = OpenPositionParameters.IsForBacktest ? candles.Last().Close : _exchangeService.GetPrice(account, signal.Ticker, DateTime.UtcNow);
|
||||
|
||||
// If position open
|
||||
if (openedPosition != null)
|
||||
{
|
||||
var previousSignal = signals.First(s => s.Identifier == openedPosition.SignalIdentifier);
|
||||
|
||||
// Check if signal is the opposite side => flip the position
|
||||
if (openedPosition.OriginDirection == signal.Direction)
|
||||
{
|
||||
// An operation is already open for the same direction
|
||||
//await LogInformation($"Signal {signal.Identifier} try to open a position but {previousSignal.Identifier} is already open for the same direction");
|
||||
signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
}
|
||||
else
|
||||
{
|
||||
// An operation is already open for the opposite direction
|
||||
// ==> Flip the position
|
||||
if (OpenPositionParameters.FlipPosition)
|
||||
{
|
||||
//await LogInformation("Try to flip the position because of an opposite direction signal");
|
||||
//await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
|
||||
positions.FirstOrDefault(s => s.Identifier == previousSignal.Identifier).Status = PositionStatus.Flipped;
|
||||
await ExecuteOpenPosition(signal, positions, signals, candles, account);
|
||||
|
||||
//await LogInformation($"Position {previousSignal.Identifier} flipped by {signal.Identifier} at {lastPrice}$");
|
||||
}
|
||||
else
|
||||
{
|
||||
//await LogWarning($"A position is already open for signal {previousSignal.Identifier}. Position flipping is currently not enable, the position will not be flipped.");
|
||||
signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CanOpenPosition(signal, positions, signals, candles))
|
||||
{
|
||||
//await LogInformation("Tried to open position but last position was a loss. Wait for an opposition direction side or wait x candles to open a new position");
|
||||
signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
return;
|
||||
}
|
||||
//await LogInformation($"Open position - Date: {signal.Date:T} - SignalIdentifier : {signal.Identifier} - Strategie : {signal.StrategyType}");
|
||||
|
||||
try
|
||||
{
|
||||
var moneyManagement = await _settingsRepository.GetMoneyManagement(OpenPositionParameters.MoneyManagementName);
|
||||
var WalletBalances = JsonConvert.DeserializeObject<Dictionary<DateTime, decimal>>(_cacheService.GetValue(WALLET_BALANCES));
|
||||
|
||||
var command = new OpenPositionRequest(
|
||||
OpenPositionParameters.AccountName,
|
||||
moneyManagement,
|
||||
signal.Direction,
|
||||
signal.Ticker,
|
||||
PositionInitiator.Bot,
|
||||
signal.Date,
|
||||
OpenPositionParameters.IsForBacktest,
|
||||
lastPrice,
|
||||
balance: WalletBalances.LastOrDefault().Value,
|
||||
fee: Fee);
|
||||
|
||||
var position = await new OpenPositionCommandHandler(_exchangeService, _accountService, _tradingService)
|
||||
.Handle(command);
|
||||
|
||||
if (position != null)
|
||||
{
|
||||
if (position.Open.Status != TradeStatus.Cancelled)
|
||||
{
|
||||
position.SignalIdentifier = signal.Identifier;
|
||||
positions.Add(position);
|
||||
signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.PositionOpen;
|
||||
|
||||
if (!OpenPositionParameters.IsForBacktest)
|
||||
{
|
||||
await _messengerService.SendPosition(position);
|
||||
}
|
||||
Output = JsonConvert.SerializeObject(position);
|
||||
//Logger.LogInformation($"Position requested");
|
||||
}
|
||||
else
|
||||
{
|
||||
positions.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = PositionStatus.Rejected;
|
||||
signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
signals.FirstOrDefault(s => s.Identifier == signal.Identifier).Status = SignalStatus.Expired;
|
||||
//await LogWarning($"Cannot open trade : {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private bool CanOpenPosition(Signal signal, List<Position> positions, HashSet<Signal> signals, HashSet<Candle> candles)
|
||||
{
|
||||
if (positions.Count == 0)
|
||||
return true;
|
||||
|
||||
var lastPosition = positions.LastOrDefault(p => p.IsFinished()
|
||||
&& p.SignalIdentifier != signal.Identifier
|
||||
&& p.ProfitAndLoss.Realized < 0
|
||||
&& p.OriginDirection == signal.Direction);
|
||||
|
||||
if (lastPosition == null)
|
||||
return true;
|
||||
|
||||
var tenCandleAgo = candles.TakeLast(10).First();
|
||||
var positionSignal = signals.FirstOrDefault(s => s.Identifier == lastPosition.SignalIdentifier);
|
||||
|
||||
return positionSignal.Date < tenCandleAgo.Date;
|
||||
}
|
||||
|
||||
|
||||
public override void MapParameters()
|
||||
{
|
||||
OpenPositionParameters = new OpenPositionParameters();
|
||||
foreach (var param in Parameters)
|
||||
{
|
||||
if (param.Name == nameof(OpenPositionParameters.AccountName))
|
||||
OpenPositionParameters.AccountName = param.Value;
|
||||
if (param.Name == nameof(OpenPositionParameters.MoneyManagementName))
|
||||
OpenPositionParameters.MoneyManagementName = param.Value;
|
||||
if (param.Name == nameof(OpenPositionParameters.FlipPosition))
|
||||
OpenPositionParameters.FlipPosition = bool.Parse(param.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenPositionParameters
|
||||
{
|
||||
public string MoneyManagementName { get; set; }
|
||||
public string AccountName { get; set; }
|
||||
public bool IsForBacktest { get; set; }
|
||||
public bool FlipPosition { get; set; }
|
||||
}
|
||||
126
src/Managing.Application/Workflows/WorkflowService.cs
Normal file
126
src/Managing.Application/Workflows/WorkflowService.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Workflows.Flows.Feeds;
|
||||
using Managing.Application.Workflows.Flows.Trading;
|
||||
using Managing.Domain.Workflows;
|
||||
using Managing.Domain.Workflows.Synthetics;
|
||||
|
||||
namespace Managing.Application.Workflows;
|
||||
|
||||
public class WorkflowService : IWorkflowService
|
||||
{
|
||||
private readonly IWorkflowRepository _workflowRepository;
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly IFlowFactory _flowFactory;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public WorkflowService(
|
||||
IWorkflowRepository workflowRepository,
|
||||
IExchangeService exchangeService,
|
||||
IFlowFactory flowFactory,
|
||||
ICacheService cacheService,
|
||||
ITradingService tradingService,
|
||||
IAccountService accountService)
|
||||
{
|
||||
_workflowRepository = workflowRepository;
|
||||
_exchangeService = exchangeService;
|
||||
_flowFactory = flowFactory;
|
||||
_cacheService = cacheService;
|
||||
_tradingService = tradingService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
public async Task<Workflow> InsertOrUpdateWorkflow(SyntheticWorkflow workflowRequest)
|
||||
{
|
||||
var previousWorkflow = await _workflowRepository.GetWorkflow(workflowRequest.Name);
|
||||
|
||||
try
|
||||
{
|
||||
if (previousWorkflow != null)
|
||||
{
|
||||
await _workflowRepository.UpdateWorkflow(workflowRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _workflowRepository.InsertWorkflow(workflowRequest);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return Map(workflowRequest);
|
||||
}
|
||||
|
||||
private Workflow Map(SyntheticWorkflow workflowRequest)
|
||||
{
|
||||
var workflow = new Workflow
|
||||
{
|
||||
Name = workflowRequest.Name,
|
||||
Usage = workflowRequest.Usage,
|
||||
Description = workflowRequest.Description,
|
||||
Flows = new List<IFlow>()
|
||||
};
|
||||
|
||||
// Add first flow that dont have any parent
|
||||
var firstFlow = workflowRequest.Flows.SingleOrDefault(f => string.IsNullOrEmpty(f.ParentId));
|
||||
|
||||
if (firstFlow != null)
|
||||
{
|
||||
var flow = Build(workflowRequest.Flows, firstFlow);
|
||||
workflow.Flows.Add(flow);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO : Throw exception
|
||||
throw new Exception("No first flow found");
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private IFlow Build(List<SyntheticFlow> flows, SyntheticFlow firstFlow)
|
||||
{
|
||||
var flow = _flowFactory.BuildFlow(firstFlow);
|
||||
|
||||
foreach (var flowRequest in flows.Where(f => f.ParentId == firstFlow.Id))
|
||||
{
|
||||
flow.Children.Add(Build(flows, flowRequest));
|
||||
}
|
||||
|
||||
return flow;
|
||||
}
|
||||
|
||||
public IEnumerable<SyntheticWorkflow> GetWorkflows()
|
||||
{
|
||||
return _workflowRepository.GetWorkflows();
|
||||
}
|
||||
|
||||
public bool DeleteWorkflow(string name)
|
||||
{
|
||||
return _workflowRepository.DeleteWorkflow(name);
|
||||
}
|
||||
|
||||
public async Task<Workflow> GetWorkflow(string name)
|
||||
{
|
||||
return Map(await _workflowRepository.GetWorkflow(name));
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IFlow>> GetAvailableFlows()
|
||||
{
|
||||
var availableFlows = new List<IFlow>
|
||||
{
|
||||
new FeedTicker(_exchangeService),
|
||||
new RsiDiv(),
|
||||
new OpenPosition(_exchangeService, _cacheService, _accountService, _tradingService)
|
||||
};
|
||||
|
||||
return Task.FromResult(availableFlows.AsEnumerable());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user