docker files fixes from liaqat

This commit is contained in:
alirehmani
2024-05-03 16:39:25 +05:00
commit 464a8730e8
587 changed files with 44288 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Workflows;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
{
public interface IBotFactory
{
ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly);
ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly);
ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly);
ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly);
IBot CreateSimpleBot(string botName, Workflow workflow);
}
}

View File

@@ -0,0 +1,12 @@
namespace Managing.Application.Abstractions
{
public interface ICacheService
{
string SaveValue(string name, string value);
string GetValue(string key);
void RemoveValue(string key);
T GetOrSave<T>(string name, Func<T> action, TimeSpan slidingExpiration);
T GetValue<T>(string key);
void SaveValue<T>(string name, T value, TimeSpan slidingExpiration);
}
}

View File

@@ -0,0 +1,7 @@
namespace Managing.Application.Abstractions
{
public interface ICommandHandler<T, M>
{
Task<M> Handle(T request);
}
}

View File

@@ -0,0 +1,9 @@
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions;
public interface IFlowFactory
{
IFlow BuildFlow(SyntheticFlow request);
}

View File

@@ -0,0 +1,13 @@
using Managing.Domain.MoneyManagements;
namespace Managing.Application.Abstractions
{
public interface IMoneyManagementService
{
Task<MoneyManagement> CreateOrUpdateMoneyManagement(MoneyManagement request);
Task<MoneyManagement> GetMoneyMangement(string name);
IEnumerable<MoneyManagement> GetMoneyMangements();
bool DeleteMoneyManagement(string name);
bool DeleteMoneyManagements();
}
}

View File

@@ -0,0 +1,29 @@
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
{
public interface IScenarioService
{
IEnumerable<Scenario> GetScenarios();
Scenario CreateScenario(string name, List<string> strategies);
IEnumerable<Strategy> GetStrategies();
bool DeleteStrategy(string name);
bool DeleteScenario(string name);
Scenario GetScenario(string name);
Strategy CreateStrategy(StrategyType type,
Timeframe timeframe,
string name,
int? period = null,
int? fastPeriods = null,
int? slowPeriods = null,
int? signalPeriods = null,
double? multiplier = null,
int? stochPeriods = null,
int? smoothPeriods = null,
int? cyclePeriods = null);
bool DeleteStrategies();
bool DeleteScenarios();
}
}

View File

@@ -0,0 +1,7 @@
namespace Managing.Application.Abstractions;
public interface ISettingsService
{
bool SetupSettings();
Task<bool> ResetSettings();
}

View File

@@ -0,0 +1,48 @@
namespace Managing.Application.Abstractions
{
public interface ITaskCache
{
/// <summary>
/// Return from the cache the value for the given key. If value is already present in cache,
/// that value will be returned. Otherwise value is first generated with the given method.
///
/// Return value can be a completed or running task-object. If the task-object is completed,
/// it has run succesfully to completion. Most often when a running task is returned,
/// it is the task returned by the function the caller has given as a parameter, but the
/// returned task might also have a different origin (from another call to this same method).
/// If the cache contains a task that will end up throwing an exception in the future, the same
/// task instance is returned to all the callers of this method. This means that any given
/// caller of this method should anticipate the type of exceptions that could be thrown from
/// the updateFunc used by any of the caller of this method.
///
/// To prevent the problem described above, as a convention, all the call sites of his method
/// (if more than one) should use the same updateFunc-parameter and also be prepared for the
/// exceptions that the the updateFunc could throw.
/// </summary>
/// <typeparam name="T">Type of the value.</typeparam>
/// <param name="key">Key that matches the wanted return value.</param>
/// <param name="valueFactory">Function that is run only if a value for the given key is not already present in the cache.</param>
/// <returns>Returned task-object can be completed or running. Note that the task might result in exception.</returns>
Task<T> AddOrGetExisting<T>(string key, Func<Task<T>> valueFactory);
/// <summary>
/// Invalidate the value for the given key, if value exists.
/// </summary>
/// <param name="key"></param>
void Invalidate(string key);
/// <summary>
/// Does the cache alrealy contain a value for the key.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
bool Contains(string key);
/// <summary>
/// Empties the cache from all entries.
/// </summary>
void Clear();
List<T> GetCache<T>();
T Get<T>(string name);
}
}

View File

@@ -0,0 +1,33 @@
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
{
public interface ITradingBot : IBot
{
HashSet<Signal> Signals { get; set; }
List<Position> Positions { get; set; }
HashSet<Candle> Candles { get; set; }
Timeframe Timeframe { get; set; }
HashSet<IStrategy> Strategies { get; set; }
Ticker Ticker { get; }
string Scenario { get; }
string AccountName { get; }
bool IsForWatchingOnly { get; set; }
MoneyManagement MoneyManagement { get; set; }
BotType BotType { get; set; }
Dictionary<DateTime, decimal> WalletBalances { get; set; }
Task Run();
Task ToggleIsForWatchOnly();
int GetWinRate();
decimal GetProfitAndLoss();
decimal GetTotalFees();
void LoadStrategies(IEnumerable<IStrategy> strategies);
}
}

View File

@@ -0,0 +1,12 @@
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions;
public interface IWorkflowService
{
bool DeleteWorkflow(string name);
Task<IEnumerable<IFlow>> GetAvailableFlows();
IEnumerable<SyntheticWorkflow> GetWorkflows();
Task<Workflow> InsertOrUpdateWorkflow(SyntheticWorkflow workflowRequest);
}

View File

@@ -0,0 +1,172 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Accounts;
public class AccountService : IAccountService
{
private readonly IAccountRepository _accountRepository;
private readonly IExchangeService _exchangeService;
private readonly IEvmManager _evmManager;
private readonly ICacheService _cacheService;
private readonly ILogger<AccountService> _logger;
public AccountService(
IAccountRepository accountRepository,
ILogger<AccountService> logger,
IExchangeService exchangeService,
IEvmManager evmManager,
ICacheService cacheService)
{
_accountRepository = accountRepository;
_logger = logger;
_exchangeService = exchangeService;
_evmManager = evmManager;
_cacheService = cacheService;
}
public async Task<Account> CreateAccount(User user, Account request)
{
var account = await _accountRepository.GetAccountByNameAsync(request.Name);
if (account != null)
{
throw new Exception($"Account {request.Name} alreary exist");
}
else
{
request.User = user;
if (request.Exchange == Enums.TradingExchanges.Evm
&& request.Type == Enums.AccountType.Trader)
{
var keys = _evmManager.GenerateAddress();
request.Key = keys.Key;
request.Secret = keys.Secret;
}
else
{
request.Key = request.Key;
request.Secret = request.Secret;
}
await _accountRepository.InsertAccountAsync(request);
}
return request;
}
public bool DeleteAccount(User user, string name)
{
try
{
_accountRepository.DeleteAccountByName(name);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public async Task<Account> GetAccount(string name, bool hideSecrets, bool getBalance)
{
var account = await _accountRepository.GetAccountByNameAsync(name);
ManageProperties(hideSecrets, getBalance, account);
return account;
}
public async Task<Account> GetAccountByKey(string key, bool hideSecrets, bool getBalance)
{
var account = await _accountRepository.GetAccountByKeyAsync(key);
ManageProperties(hideSecrets, getBalance, account);
return account;
}
public async Task<Account> GetAccountByUser(User user, string name, bool hideSecrets, bool getBalance)
{
var account = await _accountRepository.GetAccountByNameAsync(name);
ManageProperties(hideSecrets, getBalance, account);
return account;
}
public IEnumerable<Account> GetAccounts(bool hideSecrets, bool getBalance)
{
var result = _accountRepository.GetAccounts();
var accounts = new List<Account>();
foreach (var account in result)
{
ManageProperties(hideSecrets, getBalance, account);
accounts.Add(account);
}
return accounts;
}
public IEnumerable<Account> GetAccountsByUser(User user, bool hideSecrets = true)
{
var cacheKey = $"user-account-{user.Name}";
return _cacheService.GetOrSave(cacheKey, () =>
{
return GetAccounts(user, hideSecrets, false);
}, TimeSpan.FromMinutes(5));
}
private IEnumerable<Account> GetAccounts(User user, bool hideSecrets, bool getBalance)
{
var result = _accountRepository.GetAccounts();
var accounts = new List<Account>();
foreach (var account in result.Where(a => a.User.Name == user.Name))
{
ManageProperties(hideSecrets, getBalance, account);
accounts.Add(account);
}
return accounts;
}
public IEnumerable<Account> GetAccountsBalancesByUser(User user, bool hideSecrets)
{
var cacheKey = $"user-account-balance-{user.Name}";
var accounts = _cacheService.GetOrSave(cacheKey, () =>
{
return GetAccounts(user, true, true);
}, TimeSpan.FromHours(3));
return accounts;
}
private void ManageProperties(bool hideSecrets, bool getBalance, Account account)
{
if (account != null)
{
if (getBalance)
{
try
{
account.Balances = _exchangeService.GetBalances(account).Result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Cannot get account {account.Name} balance");
}
}
if (hideSecrets)
{
account.Secret = "";
}
}
}
}

View File

@@ -0,0 +1,192 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Core;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Backtesting
{
public class Backtester : IBacktester
{
private readonly IBacktestRepository _backtestRepository;
private readonly ILogger<Backtester> _logger;
private readonly IExchangeService _exchangeService;
private readonly IBotFactory _botFactory;
public Backtester(
IExchangeService exchangeService,
IBotFactory botFactory,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger)
{
_exchangeService = exchangeService;
_botFactory = botFactory;
_backtestRepository = backtestRepository;
_logger = logger;
}
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
{
var simplebot = _botFactory.CreateSimpleBot("scenario", workflow);
Backtest result = null;
if (save)
{
_backtestRepository.InsertBacktest(result);
}
return result;
}
public Backtest RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
double days,
decimal balance,
bool isForWatchingOnly = false,
bool save = false)
{
var scalpingBot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario", timeframe, isForWatchingOnly);
scalpingBot.LoadStrategies(ScenarioHelpers.GetStrategiesFromScenario(scenario));
var candles = GetCandles(account, ticker, timeframe, days);
var result = GetBacktestingResult(ticker, scenario, timeframe, scalpingBot, candles, balance, account, moneyManagement);
if (save)
{
_backtestRepository.InsertBacktest(result);
}
return result;
}
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe, double days)
{
var candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker, DateTime.Now.AddDays(Convert.ToDouble(days)), timeframe).Result;
if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {account.Exchange}");
return candles;
}
public Backtest RunFlippingBotBacktest(Account account, MoneyManagement moneyManagement, Ticker ticker, Scenario scenario, Timeframe timeframe,
double days, decimal balance, bool isForWatchingOnly = false, bool save = false)
{
var flippingBot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario", timeframe, false);
var strategy = ScenarioHelpers.GetStrategiesFromScenario(scenario);
flippingBot.LoadStrategies(ScenarioHelpers.GetStrategiesFromScenario(scenario));
var candles = GetCandles(account, ticker, timeframe, days);
var result = GetBacktestingResult(ticker, scenario, timeframe, flippingBot, candles, balance, account, moneyManagement);
if (save)
{
_backtestRepository.InsertBacktest(result);
}
return result;
}
public Backtest RunScalpingBotBacktest(Account account, MoneyManagement moneyManagement, Scenario scenario, Timeframe timeframe, List<Candle> candles, decimal balance)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var bot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario", timeframe, false);
bot.LoadStrategies(ScenarioHelpers.GetStrategiesFromScenario(scenario));
var result = GetBacktestingResult(ticker, scenario, timeframe, bot, candles, balance, account, moneyManagement);
return result;
}
public Backtest RunFlippingBotBacktest(Account account, MoneyManagement moneyManagement, Scenario scenario, Timeframe timeframe, List<Candle> candles, decimal balance)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var bot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario", timeframe, false);
var result = GetBacktestingResult(ticker, scenario, timeframe, bot, candles, balance, account, moneyManagement);
return result;
}
private static Backtest GetBacktestingResult(
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
ITradingBot bot,
List<Candle> candles,
decimal balance,
Account account,
MoneyManagement moneyManagement)
{
if (candles == null || candles.Count == 0)
{
throw new Exception("No candle to backtest");
}
var hodlBalances = new Dictionary<DateTime, decimal>();
bot.WalletBalances.Add(candles.FirstOrDefault().Date, balance);
foreach (var candle in candles)
{
hodlBalances.Add(candle.Date, candle.Close);
bot.Candles.Add(candle);
bot.Run();
}
var finalPnl = bot.GetProfitAndLoss();
var winRate = bot.GetWinRate();
var optimizedMoneyManagement = TradingBox.GetBestMoneyManagement(candles, bot.Positions, moneyManagement);
var stats = TradingHelpers.GetStatistics(bot.WalletBalances);
var growthPercentage = TradingHelpers.GetGrowthFromInitalBalance(balance, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
var result = new Backtest(ticker, scenario.Name, bot.Positions, bot.Signals.ToList(), timeframe, candles, bot.BotType, account.Name)
{
FinalPnl = finalPnl,
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
Fees = bot.GetTotalFees(),
WalletBalances = bot.WalletBalances.ToList(),
Statistics = stats,
OptimizedMoneyManagement = optimizedMoneyManagement,
MoneyManagement = moneyManagement
};
return result;
}
public IEnumerable<Backtest> GetBacktests()
{
return _backtestRepository.GetBacktests();
}
public bool DeleteBacktest(string id)
{
try
{
_backtestRepository.DeleteBacktestById(id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteBacktests()
{
try
{
_backtestRepository.DeleteAllBacktests();
//_backtestRepository.DropCollection();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}

View File

@@ -0,0 +1,111 @@
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Workflows;
using Managing.Domain.Bots;
namespace Managing.Application.Bots.Base
{
public class BotFactory : IBotFactory
{
private readonly IMoneyManagementService _moneyManagementService;
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBot> _tradingBotLogger;
private readonly ITradingService _tradingService;
public BotFactory(
IExchangeService exchangeService,
ILogger<TradingBot> tradingBotLogger,
IMoneyManagementService moneyManagementService,
IMessengerService messengerService,
IAccountService accountService,
ITradingService tradingService)
{
_tradingBotLogger = tradingBotLogger;
_exchangeService = exchangeService;
_moneyManagementService = moneyManagementService;
_messengerService = messengerService;
_accountService = accountService;
_tradingService = tradingService;
}
IBot IBotFactory.CreateSimpleBot(string botName, Workflow workflow)
{
return new SimpleBot(botName, _tradingBotLogger, workflow);
}
ITradingBot IBotFactory.CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly)
{
return new ScalpingBot(
accountName,
moneyManagement,
name,
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_accountService,
_messengerService,
isForWatchingOnly: isForWatchingOnly);
}
ITradingBot IBotFactory.CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly)
{
return new ScalpingBot(
accountName,
moneyManagement,
"BacktestBot",
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_accountService,
_messengerService,
true,
isForWatchingOnly);
}
public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly)
{
return new FlippingBot(
accountName,
moneyManagement,
name,
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_accountService,
_messengerService,
isForWatchingOnly: isForWatchingOnly);
}
public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly)
{
return new FlippingBot(
accountName,
moneyManagement,
"BacktestBot",
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_accountService,
_messengerService,
true,
isForWatchingOnly);
}
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
using Managing.Application.Abstractions.Services;
using Managing.Domain.MoneyManagements;
namespace Managing.Application.Bots
{
public class FlippingBot : TradingBot
{
public FlippingBot(string accountName,
MoneyManagement moneyManagement,
string name,
string scenario,
IExchangeService exchangeService,
Ticker ticker,
ITradingService tradingService,
ILogger<TradingBot> logger,
Timeframe timeframe,
IAccountService accountService,
IMessengerService messengerService,
bool isForBacktest = false,
bool isForWatchingOnly = false)
: base(accountName,
moneyManagement,
name,
ticker,
scenario,
exchangeService,
logger,
tradingService,
timeframe,
accountService,
messengerService,
isForBacktest,
isForWatchingOnly,
flipPosition: true)
{
BotType = BotType.FlippingBot;
Start();
}
public sealed override void Start()
{
Logger.LogInformation($"{Name} - Bot Started");
base.Start();
Logger.LogInformation($"Starting {Name} bot - Status : {Status}");
}
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
using Managing.Application.Abstractions.Services;
using Managing.Domain.MoneyManagements;
namespace Managing.Application.Bots
{
public class ScalpingBot : TradingBot
{
public ScalpingBot(string accountName,
MoneyManagement moneyManagement,
string name,
string scenario,
IExchangeService exchangeService,
Ticker ticker,
ITradingService tradingService,
ILogger<TradingBot> logger,
Timeframe timeframe,
IAccountService accountService,
IMessengerService messengerService,
bool isForBacktest = false,
bool isForWatchingOnly = false)
: base(accountName,
moneyManagement,
name,
ticker,
scenario,
exchangeService,
logger,
tradingService,
timeframe,
accountService,
messengerService,
isForBacktest,
isForWatchingOnly)
{
BotType = BotType.ScalpingBot;
Start();
}
public sealed override void Start()
{
Logger.LogInformation($"{Name} - Bot Started");
base.Start();
Logger.LogInformation($"Starting {Name} bot - Status : {Status}");
}
}
}

View File

@@ -0,0 +1,39 @@
using Managing.Domain.Bots;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Bots
{
public class SimpleBot : Bot
{
public readonly ILogger<TradingBot> Logger;
private readonly Workflow _workflow;
public SimpleBot(string name, ILogger<TradingBot> logger, Workflow workflow) : base(name)
{
Logger = logger;
_workflow = workflow;
Interval = 100;
Start();
}
public override void Start()
{
Task.Run(() => InitWorker(Run));
base.Start();
}
public async Task Run()
{
await Task.Run(
async () =>
{
Logger.LogInformation(Identifier);
Logger.LogInformation(DateTime.Now.ToString());
await _workflow.Execute();
Logger.LogInformation("__________________________________________________");
});
}
}
}

View File

@@ -0,0 +1,651 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots;
public class TradingBot : Bot, ITradingBot
{
public readonly ILogger<TradingBot> Logger;
public readonly IExchangeService ExchangeService;
public readonly IMessengerService MessengerService;
public readonly IAccountService AccountService;
private readonly ITradingService TradingService;
public Account Account { get; set; }
public HashSet<IStrategy> Strategies { get; set; }
public HashSet<Candle> Candles { get; set; }
public HashSet<Signal> Signals { get; set; }
public List<Position> Positions { get; set; }
public Ticker Ticker { get; set; }
public string Scenario { get; set; }
public string AccountName { get; set; }
public MoneyManagement MoneyManagement { get; set; }
public Timeframe Timeframe { get; set; }
public bool IsForBacktest { get; set; }
public DateTime PreloadSince { get; set; }
public bool IsForWatchingOnly { get; set; }
public bool FlipPosition { get; set; }
public int PreloadedCandlesCount { get; set; }
public BotType BotType { get; set; }
public decimal Fee { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public TradingBot(
string accountName,
MoneyManagement moneyManagement,
string name,
Ticker ticker,
string scenario,
IExchangeService exchangeService,
ILogger<TradingBot> logger,
ITradingService tradingService,
Timeframe timeframe,
IAccountService accountService,
IMessengerService messengerService,
bool isForBacktest = false,
bool isForWatchingOnly = false,
bool flipPosition = false)
: base(name)
{
ExchangeService = exchangeService;
AccountService = accountService;
MessengerService = messengerService;
TradingService = tradingService;
IsForWatchingOnly = isForWatchingOnly;
FlipPosition = flipPosition;
AccountName = accountName;
MoneyManagement = moneyManagement;
Ticker = ticker;
Scenario = scenario;
Timeframe = timeframe;
IsForBacktest = isForBacktest;
Logger = logger;
Strategies = new HashSet<IStrategy>();
Signals = new HashSet<Signal>();
Candles = new HashSet<Candle>();
Positions = new List<Position>();
WalletBalances = new Dictionary<DateTime, decimal>();
if (!isForBacktest)
{
Interval = CandleExtensions.GetIntervalFromTimeframe(timeframe);
PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(timeframe);
}
}
public override async void Start()
{
base.Start();
await LoadAccount();
if (!IsForBacktest)
{
LoadScenario();
await PreloadCandles();
await CancelAllOrders();
await MessengerService.SendMessage($"Hi everyone, I'm going to run {Name}. \nI will send a message here everytime a signal is triggered by the {string.Join(",", Strategies.Select(s => s.Name))} strategies.");
await InitWorker(Run);
}
Fee = TradingService.GetFee(Account, IsForBacktest);
}
private async Task LoadAccount()
{
var account = await AccountService.GetAccount(AccountName, false, true);
if (account == null)
{
Logger.LogWarning($"No account found for this {AccountName}");
Stop();
}
else
{
Account = account;
}
}
public void LoadScenario()
{
var scenario = TradingService.GetScenarioByName(Scenario);
if (scenario == null)
{
Logger.LogWarning("No scenario found for this scenario name");
Stop();
}
else
{
LoadStrategies(ScenarioHelpers.GetStrategiesFromScenario(scenario));
}
}
public void LoadStrategies(IEnumerable<IStrategy> strategies)
{
foreach (var strategy in strategies)
{
Strategies.Add(strategy);
}
}
public async Task Run()
{
Logger.LogInformation($"____________________{Name}____________________");
Logger.LogInformation($"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Bot : {Name} - Type {BotType} - Ticker : {Ticker}");
var previousCandleCount = Candles.Count;
if (!IsForBacktest)
await UpdateCandles();
if (Candles.Count > previousCandleCount || IsForBacktest)
await UpdateSignals(Candles);
else
Logger.LogInformation($"No need to update signals for {Ticker}");
if (!IsForWatchingOnly)
await ManagePositions();
await UpdateWalletBalances();
Logger.LogInformation($"Candles : {Candles.Count}");
Logger.LogInformation($"Signals : {Signals.Count}");
Logger.LogInformation($"ExecutionCount : {ExecutionCount}");
Logger.LogInformation($"Positions : {Positions.Count}");
Logger.LogInformation("__________________________________________________");
}
private async Task PreloadCandles()
{
var candles = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, PreloadSince, Timeframe);
foreach (var candle in candles.Where(c => c.Date < DateTime.Now.ToUniversalTime()))
{
if (!Candles.Any(c => c.Date == candle.Date))
{
Candles.Add(candle);
await UpdateSignals(Candles);
}
}
PreloadedCandlesCount = Candles.Count();
}
private async Task UpdateSignals(HashSet<Candle> candles)
{
var signal = TradingBox.GetSignal(candles, Strategies, Signals);
if (signal == null) return;
await AddSignal(signal);
}
private async Task AddSignal(Signal signal)
{
Signals.Add(signal);
if (!IsForBacktest)
TradingService.InsertSignal(signal);
if (IsForWatchingOnly || (ExecutionCount < 1 && !IsForBacktest))
signal.Status = SignalStatus.Expired;
var signalText = $"{Scenario} trigger a signal. Signal told you " +
$"to {signal.Direction} {Ticker} on {Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}";
Logger.LogInformation(signalText);
if (IsForWatchingOnly && !IsForBacktest && ExecutionCount > 0)
{
await MessengerService.SendSignal(signalText, Account.Exchange, Ticker, signal.Direction, Timeframe);
}
}
protected async Task UpdateCandles()
{
if (Candles.Count == 0 || ExecutionCount == 0)
return;
var lastCandle = Candles.Last();
var newCandle = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, lastCandle.Date, Timeframe);
foreach (var candle in newCandle.Where(c => c.Date < DateTime.Now.ToUniversalTime()))
{
Candles.Add(candle);
}
}
private async Task ManagePositions()
{
if (!IsForBacktest && ExecutionCount < 1)
return;
// Update position
foreach (var signal in Signals.Where(s => s.Status == SignalStatus.PositionOpen))
{
var positionForSignal = Positions.FirstOrDefault(p => p.SignalIdentifier == signal.Identifier);
await UpdatePosition(signal, positionForSignal);
}
// Open position for signal waiting for a position open
foreach (var signal in Signals.Where(s => s.Status == SignalStatus.WaitingForPosition))
{
Task.Run(() => OpenPosition(signal)).GetAwaiter().GetResult();
}
}
private async Task UpdateWalletBalances()
{
if (WalletBalances.Count == 0)
{
WalletBalances.Add(Candles.LastOrDefault().Date, await ExchangeService.GetBalance(Account, IsForBacktest));
}
else if (!WalletBalances.Any(w => w.Key == Candles.LastOrDefault().Date))
{
var walletBalance = WalletBalances.FirstOrDefault().Value + GetProfitAndLoss();
WalletBalances.Add(Candles.LastOrDefault().Date, walletBalance);
}
}
private async Task UpdatePosition(Signal signal, Position positionForSignal)
{
try
{
Logger.LogInformation($"Updating position {positionForSignal.SignalIdentifier}");
var position = IsForBacktest ? positionForSignal : TradingService.GetPositionByIdentifier(positionForSignal.Identifier);
if (position.Status == (PositionStatus.Finished | PositionStatus.Flipped))
{
await HandleClosedPosition(positionForSignal);
return;
}
else if (position.Status == PositionStatus.Filled)
{
// For backtesting or force close if not executed on exchange :
// check if position is still open
// Check status, if still open update the status of the position
var lastCandle = IsForBacktest ? Candles.Last() : ExchangeService.GetCandle(Account, Ticker, DateTime.UtcNow);
if (positionForSignal.OriginDirection == TradeDirection.Long)
{
if (positionForSignal.StopLoss.Price >= lastCandle.Low)
{
await LogInformation($"Closing position - SL {positionForSignal.StopLoss.Price} >= Price {lastCandle.Low}");
await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, positionForSignal.StopLoss.Price, true);
positionForSignal.StopLoss.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit1.Price <= lastCandle.High
&& positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
{
await LogInformation($"Closing position - TP1 {positionForSignal.TakeProfit1.Price} <= Price {lastCandle.High}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit2?.Price <= lastCandle.High)
{
await LogInformation($"Closing position - TP2 {positionForSignal.TakeProfit2.Price} <= Price {lastCandle.High}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, positionForSignal.TakeProfit2.Price, true);
positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled);
}
else
{
Logger.LogInformation($"Position {signal.Identifier} don't need to be update. Position still opened");
}
}
if (positionForSignal.OriginDirection == TradeDirection.Short)
{
if (positionForSignal.StopLoss.Price <= lastCandle.High)
{
await LogInformation($"Closing position - SL {positionForSignal.StopLoss.Price} <= Price {lastCandle.High}");
await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, positionForSignal.StopLoss.Price, true);
positionForSignal.StopLoss.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit1.Price >= lastCandle.Low
&& positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
{
await LogInformation($"Closing position - TP1 {positionForSignal.TakeProfit1.Price} >= Price {lastCandle.Low}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit2?.Price >= lastCandle.Low)
{
await LogInformation($"Closing position - TP2 {positionForSignal.TakeProfit2.Price} >= Price {lastCandle.Low}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, positionForSignal.TakeProfit2.Price, true);
positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled);
}
else
{
Logger.LogInformation($"Position {signal.Identifier} don't need to be update. Position still opened");
}
}
}
else if (position.Status == (PositionStatus.Rejected | PositionStatus.Canceled))
{
await LogWarning($"Open position trade is rejected for signal {signal.Identifier}");
// if position is not open
// Re-open the trade for the signal only if signal still up
//if (signal.Status == SignalStatus.PositionOpen)
//{
// Logger.LogInformation($"Try to re-open position");
// OpenPosition(signal);
//}
}
}
catch (Exception ex)
{
Logger.LogError(ex, ex.Message);
return;
}
}
private async Task OpenPosition(Signal signal)
{
// Check if a position is already open
Logger.LogInformation($"Opening position for {signal.Identifier}");
var openedPosition = Positions.FirstOrDefault(p => p.Status == PositionStatus.Filled
&& p.SignalIdentifier != signal.Identifier);
var lastPrice = IsForBacktest ? Candles.Last().Close : ExchangeService.GetPrice(Account, 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");
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
}
else
{
// An operation is already open for the opposite direction
// ==> Flip the position
if (FlipPosition)
{
await LogInformation("Try to flip the position because of an opposite direction signal");
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
await OpenPosition(signal);
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.");
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
}
}
}
else
{
if (!CanOpenPosition(signal))
{
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");
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
return;
}
await LogInformation($"Open position - Date: {signal.Date:T} - SignalIdentifier : {signal.Identifier} - Strategie : {signal.StrategyType}");
try
{
var command = new OpenPositionRequest(
AccountName,
MoneyManagement,
signal.Direction,
Ticker,
PositionInitiator.Bot,
signal.Date,
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);
SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen);
if (!IsForBacktest)
{
await MessengerService.SendPosition(position);
}
Logger.LogInformation($"Position requested");
}
else
{
await SetPositionStatus(signal.Identifier, PositionStatus.Rejected);
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
}
}
}
catch (Exception ex)
{
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
await LogWarning($"Cannot open trade : {ex.Message}");
}
}
}
private bool CanOpenPosition(Signal signal)
{
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;
}
private async Task CloseTrade(Signal signal, Position position, Trade tradeToClose, decimal lastPrice, bool tradeClosingPosition = false)
{
if (position.TakeProfit2 != null && position.TakeProfit1.Status == TradeStatus.Filled && tradeToClose.TradeType == TradeType.StopMarket)
{
// If trade is the 2nd Take profit
tradeToClose.Quantity = position.TakeProfit2.Quantity;
}
await LogInformation($"Trying to close trade {Ticker} at {lastPrice} - Type : {tradeToClose.TradeType} - Quantity : {tradeToClose.Quantity} " +
$"- Closing Position : {tradeClosingPosition}");
// Get status of position before closing it. The position might be already close by the exchange
if (!IsForBacktest && await ExchangeService.GetQuantityInPosition(Account, Ticker) == 0)
{
Logger.LogInformation($"Trade already close on exchange");
await HandleClosedPosition(position);
}
else
{
var command = new ClosePositionCommand(position, lastPrice);
try
{
var closedPosition = await (new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService))
.Handle(command);
if (closedPosition.Status == (PositionStatus.Finished | PositionStatus.Flipped))
{
if (tradeClosingPosition)
{
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
}
await HandleClosedPosition(closedPosition);
}
else
{
throw new Exception($"Wrong position status : {closedPosition.Status}");
}
}
catch (Exception ex)
{
await LogWarning($"Position {signal.Identifier} not closed : {ex.Message}");
if (position.Status == (PositionStatus.Canceled | PositionStatus.Rejected))
{
// Trade close on exchange => Should close trade manually
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
}
}
}
}
private async Task HandleClosedPosition(Position position)
{
if (Positions.Any(p => p.Identifier == position.Identifier))
{
var previousPosition = Positions.First(p => p.Identifier == position.Identifier);
var positionIndex = Positions.IndexOf(previousPosition);
position.SignalIdentifier = previousPosition.SignalIdentifier;
Positions[positionIndex] = position;
SetSignalStatus(position.SignalIdentifier, SignalStatus.Expired);
Logger.LogInformation($"Position {position.SignalIdentifier} type correctly close. Pnl on position : {position.ProfitAndLoss.Realized}");
}
else
{
await LogWarning("Weird things happen - Trying to update position status, but no position found");
}
if (!IsForBacktest)
{
await MessengerService.SendClosingPosition(position);
}
await CancelAllOrders();
}
private async Task CancelAllOrders()
{
if (!IsForBacktest)
{
try
{
var closePendingOrderStatus = await ExchangeService.CancelOrder(Account, Ticker);
Logger.LogInformation($"Closing all {Ticker} orders status : {closePendingOrderStatus}");
}
catch (Exception ex)
{
// Todo handle exception from evm
Logger.LogError(ex, "Error during cancelOrders");
}
}
}
private async Task SetPositionStatus(string signalIdentifier, PositionStatus positionStatus)
{
await LogInformation($"Position {signalIdentifier} is now {positionStatus}");
Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
SetSignalStatus(signalIdentifier, SignalStatus.Expired);
}
private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus)
{
if (Signals.Any(s => s.Identifier == signalIdentifier))
{
Signals.First(s => s.Identifier == signalIdentifier).Status = signalStatus;
Logger.LogInformation($"Signal {signalIdentifier} is now {signalStatus}");
}
}
public int GetWinRate()
{
var succeededPositions = Positions.Where(p => p.IsFinished()).Count(p => p.ProfitAndLoss?.Realized > 0);
var total = Positions.Where(p => p.IsFinished()).Count();
if (total == 0)
return 0;
return (succeededPositions * 100) / total;
}
public decimal GetProfitAndLoss()
{
var pnl = Positions.Where(p => p.ProfitAndLoss != null).Sum(p => p.ProfitAndLoss.Realized);
return pnl - GetTotalFees();
}
/// <summary>
/// Calculates the total fees paid by the trading bot for each position.
/// </summary>
/// <returns>Returns the total fees paid as a decimal value.</returns>
public decimal GetTotalFees()
{
decimal fees = 0;
foreach (var position in Positions.Where(p => p.Open.Fee > 0))
{
fees += position.Open.Fee;
fees += position.StopLoss.Status == TradeStatus.Filled ? position.StopLoss.Fee : 0;
fees += position.TakeProfit1.Status == TradeStatus.Filled ? position.TakeProfit1.Fee : 0;
if (position.IsFinished() &&
position.StopLoss.Status != TradeStatus.Filled && position.TakeProfit1.Status != TradeStatus.Filled)
fees += position.Open.Fee;
if (position.TakeProfit2 != null)
fees += position.TakeProfit2.Fee;
}
return fees;
}
public async Task ToggleIsForWatchOnly()
{
IsForWatchingOnly = (!IsForWatchingOnly);
await LogInformation($"Watch only toggle for bot : {Name} - Watch only : {IsForWatchingOnly}");
}
private async Task LogInformation(string message)
{
Logger.LogInformation(message);
await SendTradeMessage(message);
}
private async Task LogWarning(string message)
{
Logger.LogWarning(message);
await SendTradeMessage(message, true);
}
private async Task SendTradeMessage(string message, bool isBadBehavior = false)
{
if (!IsForBacktest)
{
await MessengerService.SendTradeMessage(message, isBadBehavior);
}
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.SignalR;
namespace Managing.Application.Hubs;
public class BacktestHub : Hub
{
public async override Task OnConnectedAsync()
{
await base.OnConnectedAsync();
await Clients.Caller.SendAsync("Message", $"Connected successfully on backtest hub. ConnectionId : {Context.ConnectionId}");
}
public async Task SubscribeBots() =>
await Clients.All.SendAsync("BacktestsSubscription", "Successfully subscribed");
}

View File

@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.SignalR;
namespace Managing.Application.Hubs;
public class BotHub : Hub
{
public async override Task OnConnectedAsync()
{
await base.OnConnectedAsync();
await Clients.Caller.SendAsync("Message", "Connected successfully!");
}
public async Task SubscribeBots() =>
await Clients.All.SendAsync("BotsSubscription", "Successfully subscribed");
public string GetConnectionId() => Context.ConnectionId;
}

View File

@@ -0,0 +1,41 @@
using Managing.Application.Abstractions.Services;
using Microsoft.AspNetCore.SignalR;
namespace Managing.Application.Hubs;
public class CandleHub : Hub
{
private int ConnectionCount = 0;
private readonly IStreamService _streamService;
public CandleHub(IStreamService streamService)
{
_streamService = streamService;
}
public async override Task OnConnectedAsync()
{
ConnectionCount++;
await Clients.Caller.SendAsync("Message", $"Connected successfully on candle hub. ConnectionId : {Context.ConnectionId}");
//await _streamService.SubscribeCandle(async (candle) => {
// await Clients.All.SendAsync("Candle", candle);
//});
await _streamService.SubscribeCandle();
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception ex)
{
await Clients.Caller.SendAsync("Message", $"Shuting down candle hub. ConnectionId : {Context.ConnectionId}");
ConnectionCount--;
if(ConnectionCount == 0)
{
await _streamService.UnSubscribeCandle();
}
await base.OnDisconnectedAsync(ex);
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.SignalR;
namespace Managing.Application.Hubs;
public class PositionHub : Hub
{
public async override Task OnConnectedAsync()
{
await base.OnConnectedAsync();
await Clients.Caller.SendAsync("Message", $"Connected successfully on position hub. ConnectionId : {Context.ConnectionId}");
}
}

View File

@@ -0,0 +1,13 @@
using MediatR;
namespace Managing.Application.ManageBot.Commands;
public class DeleteBotCommand : IRequest<bool>
{
public string Name { get; }
public DeleteBotCommand(string name)
{
Name = name;
}
}

View File

@@ -0,0 +1,12 @@
using Managing.Application.Abstractions;
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
public class GetActiveBotsCommand : IRequest<List<ITradingBot>>
{
public GetActiveBotsCommand()
{
}
}
}

View File

@@ -0,0 +1,14 @@
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
public class ToggleIsForWatchingCommand : IRequest<string>
{
public string Name { get; }
public ToggleIsForWatchingCommand(string name)
{
Name = name;
}
}
}

View File

@@ -0,0 +1,36 @@
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class StartBotCommand : IRequest<string>
{
public string Name { get; }
public BotType BotType { get; }
public Ticker Ticker { get; internal set; }
public Timeframe Timeframe { get; internal set; }
public bool IsForWatchingOnly { get; internal set; }
public string Scenario { get; internal set; }
public string AccountName { get; internal set; }
public string MoneyManagementName { get; internal set; }
public StartBotCommand(BotType botType,
string name,
Ticker ticker,
string scenario,
Timeframe timeframe,
string accountName,
string moneyManagementName,
bool isForWatchingOnly = false)
{
BotType = botType;
Name = name;
Scenario = scenario;
Ticker = ticker;
Timeframe = timeframe;
IsForWatchingOnly = isForWatchingOnly;
AccountName = accountName;
MoneyManagementName = moneyManagementName;
}
}
}

View File

@@ -0,0 +1,17 @@
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class StopBotCommand : IRequest<string>
{
public string Name { get; }
public BotType BotType { get; }
public StopBotCommand(BotType botType, string name)
{
BotType = botType;
Name = name;
}
}
}

View File

@@ -0,0 +1,17 @@
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class RestartBotCommand : IRequest<string>
{
public string Name { get; }
public BotType BotType { get; }
public RestartBotCommand(BotType botType, string name)
{
BotType = botType;
Name = name;
}
}
}

View File

@@ -0,0 +1,32 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
using Microsoft.Extensions.Logging;
namespace Managing.Application.ManageBot;
public class DeleteBotCommandHandler : IRequestHandler<DeleteBotCommand, bool>
{
private readonly ILogger<DeleteBotCommandHandler> _log;
private readonly ITaskCache _taskCache;
public DeleteBotCommandHandler(ITaskCache taskCache, ILogger<DeleteBotCommandHandler> log)
{
_taskCache = taskCache;
_log = log;
}
public Task<bool> Handle(DeleteBotCommand request, CancellationToken cancellationToken)
{
try
{
_taskCache.Invalidate(request.Name);
return Task.FromResult(true);
}
catch (Exception e)
{
_log.LogError(e.Message);
return Task.FromResult(false);
}
}
}

View File

@@ -0,0 +1,30 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Core;
using MediatR;
namespace Managing.Application.ManageBot
{
public class GetActiveBotsCommandHandler : IRequestHandler<GetActiveBotsCommand, List<ITradingBot>>
{
private readonly ITaskCache taskCache;
public GetActiveBotsCommandHandler(ITaskCache taskCache)
{
this.taskCache = taskCache;
}
public Task<List<ITradingBot>> Handle(GetActiveBotsCommand request, CancellationToken cancellationToken)
{
var cachedTask = taskCache.GetCache<AsyncLazy<ITradingBot>>();
var result = new List<ITradingBot>();
foreach (var item in cachedTask)
{
result.Add(item.Value.Result);
}
return Task.FromResult(result);
}
}
}

View File

@@ -0,0 +1,39 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, string>
{
private readonly ITaskCache _taskCache;
public RestartBotCommandHandler(ITaskCache taskCache)
{
_taskCache = taskCache;
}
public Task<string> Handle(RestartBotCommand request, CancellationToken cancellationToken)
{
switch (request.BotType)
{
case BotType.SimpleBot:
var simpleBot = _taskCache.Get<IBot>(request.Name);
simpleBot.Restart();
return Task.FromResult(simpleBot.GetStatus());
case BotType.ScalpingBot:
var scalpingBot = _taskCache.Get<ITradingBot>(request.Name);
scalpingBot.Restart();
return Task.FromResult(scalpingBot.GetStatus());
case BotType.FlippingBot:
var flippingBot = _taskCache.Get<ITradingBot>(request.Name);
flippingBot.Restart();
return Task.FromResult(flippingBot.GetStatus());
default:
return Task.FromResult(BotStatus.Down.ToString());
}
}
}
}

View File

@@ -0,0 +1,42 @@
using Managing.Domain.Bots;
using MediatR;
using static Managing.Common.Enums;
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
namespace Managing.Application.ManageBot
{
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, string>
{
private readonly IBotFactory _botFactory;
private readonly ITaskCache _taskCache;
private readonly IMoneyManagementService _moneyManagementService;
public StartBotCommandHandler(IBotFactory botFactory, ITaskCache taskCache, IMoneyManagementService moneyManagementService)
{
_botFactory = botFactory;
_taskCache = taskCache;
_moneyManagementService = moneyManagementService;
}
public Task<string> Handle(StartBotCommand request, CancellationToken cancellationToken)
{
BotStatus botStatus = BotStatus.Down;
var moneyManagement = _moneyManagementService.GetMoneyMangement(request.MoneyManagementName).Result;
switch (request.BotType)
{
case BotType.SimpleBot:
Func<Task<IBot>> simpleBot = () => Task.FromResult(_botFactory.CreateSimpleBot(request.Name, null));
return Task.FromResult(_taskCache.AddOrGetExisting(request.Name, simpleBot).Result.GetStatus());
case BotType.ScalpingBot:
Func<Task<ITradingBot>> scalpingBot = () => Task.FromResult(_botFactory.CreateScalpingBot(request.AccountName, moneyManagement, request.Name, request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly));
return Task.FromResult(_taskCache.AddOrGetExisting(request.Name, scalpingBot).Result.GetStatus());
case BotType.FlippingBot:
Func<Task<ITradingBot>> flippingBot = () => Task.FromResult(_botFactory.CreateFlippingBot(request.AccountName, moneyManagement, request.Name, request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly));
return Task.FromResult(_taskCache.AddOrGetExisting(request.Name, flippingBot).Result.GetStatus());
};
return Task.FromResult(botStatus.ToString());
}
}
}

View File

@@ -0,0 +1,39 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, string>
{
private readonly ITaskCache _taskCache;
public StopBotCommandHandler(ITaskCache taskCache)
{
_taskCache = taskCache;
}
public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken)
{
switch (request.BotType)
{
case BotType.SimpleBot:
var simpleBot = _taskCache.Get<IBot>(request.Name);
simpleBot.Stop();
return Task.FromResult(simpleBot.GetStatus());
case BotType.ScalpingBot:
var scalpingBot = _taskCache.Get<ITradingBot>(request.Name);
scalpingBot.Stop();
return Task.FromResult(scalpingBot.GetStatus());
case BotType.FlippingBot:
var flippingBot = _taskCache.Get<ITradingBot>(request.Name);
flippingBot.Stop();
return Task.FromResult(flippingBot.GetStatus());
default:
return Task.FromResult(BotStatus.Down.ToString());
}
}
}
}

View File

@@ -0,0 +1,23 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
namespace Managing.Application.ManageBot
{
public class ToggleIsForWatchingCommandHandler : IRequestHandler<ToggleIsForWatchingCommand, string>
{
private readonly ITaskCache _taskCache;
public ToggleIsForWatchingCommandHandler(ITaskCache taskCache)
{
_taskCache = taskCache;
}
public async Task<string> Handle(ToggleIsForWatchingCommand request, CancellationToken cancellationToken)
{
var bot = _taskCache.Get<ITradingBot>(request.Name);
await bot.ToggleIsForWatchOnly();
return bot.GetStatus();
}
}
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Compile Remove="MoneyManagements\Abstractions\**" />
<EmbeddedResource Remove="MoneyManagements\Abstractions\**" />
<None Remove="MoneyManagements\Abstractions\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.6.0" />
<PackageReference Include="MediatR" Version="11.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Polly" Version="7.2.4" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.4.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj" />
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,79 @@
using Managing.Domain.MoneyManagements;
using Managing.Application.Abstractions;
using Microsoft.Extensions.Logging;
using Managing.Application.Abstractions.Repositories;
namespace Managing.Application.MoneyManagements;
public class MoneyManagementService : IMoneyManagementService
{
private readonly ISettingsRepository _settingsRepository;
private readonly ILogger<MoneyManagementService> _logger;
public MoneyManagementService(
ILogger<MoneyManagementService> logger,
ISettingsRepository settingsRepository)
{
_logger = logger;
_settingsRepository = settingsRepository;
}
public async Task<MoneyManagement> CreateOrUpdateMoneyManagement(MoneyManagement request)
{
var moneyManagement = await _settingsRepository.GetMoneyManagement(request.Name);
if (moneyManagement == null)
{
await _settingsRepository.InsertMoneyManagement(request);
}
else
{
moneyManagement.StopLoss = request.StopLoss;
moneyManagement.TakeProfit = request.TakeProfit;
moneyManagement.BalanceAtRisk = request.BalanceAtRisk;
moneyManagement.Leverage = request.Leverage;
moneyManagement.Timeframe = request.Timeframe;
_settingsRepository.UpdateMoneyManagement(moneyManagement);
}
return moneyManagement;
}
public IEnumerable<MoneyManagement> GetMoneyMangements()
{
return _settingsRepository.GetMoneyManagements();
}
public async Task<MoneyManagement> GetMoneyMangement(string name)
{
return await _settingsRepository.GetMoneyManagement(name);
}
public bool DeleteMoneyManagement(string name)
{
try
{
_settingsRepository.DeleteMoneyManagement(name);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteMoneyManagements()
{
try
{
_settingsRepository.DeleteMoneyManagements();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}

View File

@@ -0,0 +1,144 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using static Managing.Common.Enums;
namespace Managing.Application.Scenarios
{
public class ScenarioService : IScenarioService
{
private readonly ILogger<ScenarioService> _logger;
private readonly ITradingService _tradingService;
public ScenarioService(ILogger<ScenarioService> logger, ITradingService tradingService)
{
_logger = logger;
_tradingService = tradingService;
}
public Scenario CreateScenario(string name, List<string> strategies)
{
var scenario = new Scenario(name);
foreach (var strategy in strategies)
{
scenario.AddStrategy(_tradingService.GetStrategyByName(strategy));
}
try
{
_tradingService.InsertScenario(scenario);
}
catch (MongoCommandException ex)
{
_logger.LogError(ex.Message);
throw new Exception("Cannot create scenario");
}
return scenario;
}
public Strategy CreateStrategy(
StrategyType type,
Timeframe timeframe,
string name,
int? period = null,
int? fastPeriods = null,
int? slowPeriods = null,
int? signalPeriods = null,
double? multiplier = null,
int? stochPeriods = null,
int? smoothPeriods = null,
int? cyclePeriods = null)
{
var strategy = ScenarioHelpers.BuildStrategy(
type,
timeframe,
name,
period,
fastPeriods,
slowPeriods,
signalPeriods,
multiplier,
stochPeriods,
smoothPeriods,
cyclePeriods);
_tradingService.InsertStrategy(strategy);
return strategy;
}
public IEnumerable<Scenario> GetScenarios()
{
return _tradingService.GetScenarios();
}
public Scenario GetScenario(string name)
{
return _tradingService.GetScenarioByName(name);
}
public IEnumerable<Strategy> GetStrategies()
{
return _tradingService.GetStrategies();
}
public bool DeleteScenario(string name)
{
try
{
_tradingService.DeleteScenario(name);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteStrategy(string name)
{
try
{
_tradingService.DeleteStrategy(name);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteStrategies()
{
try
{
_tradingService.DeleteStrategies();
}
catch (Exception)
{
return false;
}
return true;
}
public bool DeleteScenarios()
{
try
{
_tradingService.DeleteScenarios();
}
catch (Exception)
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,9 @@
using MediatR;
namespace Managing.Application.Shared.Behaviours
{
public interface IUnhandledExceptionBehaviour<TRequest, TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}
}

View File

@@ -0,0 +1,31 @@
using MediatR;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Shared.Behaviours
{
public class UnhandledExceptionBehaviour<TRequest, TResponse> : IUnhandledExceptionBehaviour<TRequest, TResponse>
{
private readonly ILogger<TRequest> logger;
public UnhandledExceptionBehaviour(ILogger<TRequest> logger)
{
this.logger = logger;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
try
{
return await next();
}
catch (Exception ex)
{
var requestName = typeof(TRequest).Name;
logger.LogError(ex, $"Unhandled Exception for Request {requestName} {request}");
throw;
}
}
}
}

View File

@@ -0,0 +1,29 @@
using FluentValidation;
using MediatR;
namespace Managing.Application.Shared.Behaviours
{
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> validators;
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
{
this.validators = validators;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();
if (failures.Count != 0) throw new ValidationException(failures);
}
return await next();
}
}
}

View File

@@ -0,0 +1,66 @@
using Managing.Application.Abstractions.Services;
using Managing.Common;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
namespace Managing.Application.Shared;
public class MessengerService : IMessengerService
{
private readonly IDiscordService _discordService;
public MessengerService(IDiscordService discordService)
{
_discordService = discordService;
}
public async Task SendClosedPosition(string address, Trade oldTrade)
{
await _discordService.SendClosedPosition(address, oldTrade);
}
public async Task SendClosingPosition(Position position)
{
await _discordService.SendClosingPosition(position);
}
public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName, Trade? oldTrade = null)
{
await _discordService.SendIncreasePosition(address, trade, copyAccountName, oldTrade);
}
public async Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount)
{
await _discordService.SendDecreasePosition(address, newTrade, decreaseAmount);
}
public async Task SendMessage(string message)
{
await _discordService.SendMessage(message);
}
public async Task SendPosition(Position position)
{
await _discordService.SendPosition(position);
}
public async Task SendSignal(string message, Enums.TradingExchanges exchange, Enums.Ticker ticker, Enums.TradeDirection direction, Enums.Timeframe timeframe)
{
await _discordService.SendSignal(message, exchange, ticker, direction, timeframe);
}
public async Task SendTradeMessage(string message, bool isBadBehavior = false)
{
await _discordService.SendTradeMessage(message, isBadBehavior);
}
public async Task SendBestTraders(List<Trader> traders)
{
await _discordService.SendBestTraders(traders);
}
public async Task SendBadTraders(List<Trader> traders)
{
await _discordService.SendBadTraders(traders);
}
}

View File

@@ -0,0 +1,199 @@
using Managing.Application.Abstractions;
using Managing.Domain.MoneyManagements;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Shared;
public class SettingsService : ISettingsService
{
private readonly IMoneyManagementService _moneyManagementService;
private readonly IScenarioService _scenarioService;
private readonly IBacktester _backtester;
private readonly ILogger<SettingsService> _logger;
public SettingsService(IMoneyManagementService moneyManagementService,
IScenarioService scenarioService,
IBacktester backtester,
ILogger<SettingsService> logger)
{
_moneyManagementService = moneyManagementService;
_scenarioService = scenarioService;
_backtester = backtester;
_logger = logger;
}
public async Task<bool> ResetSettings()
{
if (!_backtester.DeleteBacktests())
{
throw new Exception("Cannot delete all backtests");
}
if (!_scenarioService.DeleteScenarios())
{
throw new Exception("Cannot delete scenarios");
}
if (!_scenarioService.DeleteStrategies())
{
throw new Exception("Cannot delete all strategies");
}
if (!_moneyManagementService.DeleteMoneyManagements())
{
throw new Exception("Cannot delete all money management settings");
}
if (!SetupSettings())
{
throw new Exception("Cannot setup settings");
}
return await Task.FromResult(true);
}
public bool SetupSettings()
{
try
{
SetupMoneyManagementsSeed(Timeframe.FiveMinutes);
SetupMoneyManagementsSeed(Timeframe.FifteenMinutes);
SetupMoneyManagementsSeed(Timeframe.OneHour);
SetupMoneyManagementsSeed(Timeframe.OneDay);
SetupScenariosSeed(Timeframe.FifteenMinutes);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
return true;
}
private async void SetupMoneyManagementsSeed(Timeframe timeframe)
{
var moneyManagement = new MoneyManagement()
{
Timeframe = timeframe,
BalanceAtRisk = 0.05m,
Leverage = 1,
StopLoss = 0.021m,
TakeProfit = 0.042m,
Name = $"{timeframe} Money Management"
};
await _moneyManagementService.CreateOrUpdateMoneyManagement(moneyManagement);
}
private void SetupScenariosSeed(Timeframe timeframe)
{
SetupMacd(timeframe);
SetupRsiDiv(timeframe);
SetupRsiDivConfirm(timeframe);
SetupSuperTrend(timeframe);
SetupChandelierExit(timeframe);
SetupStochRsiTrend(timeframe);
SetupStochSTCTrend(timeframe);
SetupEmaTrend(timeframe);
SetupEmaCross(timeframe);
}
private void SetupStochSTCTrend(Timeframe timeframe)
{
var name = "STCTrend";
var strategy = _scenarioService.CreateStrategy(StrategyType.Stc,
timeframe,
name,
fastPeriods: 23,
slowPeriods: 50,
cyclePeriods: 10);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupMacd(Timeframe timeframe)
{
var name = "MacdCross";
var strategy = _scenarioService.CreateStrategy(StrategyType.MacdCross,
timeframe,
name,
fastPeriods: 12,
slowPeriods: 26,
signalPeriods: 9);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupRsiDiv(Timeframe timeframe)
{
var name = "RsiDiv6";
var strategy = _scenarioService.CreateStrategy(StrategyType.RsiDivergence,
timeframe,
name,
period: 6);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupRsiDivConfirm(Timeframe timeframe)
{
var name = "RsiDivConfirm6";
var strategy = _scenarioService.CreateStrategy(StrategyType.RsiDivergenceConfirm,
timeframe,
name,
period: 6);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupSuperTrend(Timeframe timeframe)
{
var name = "SuperTrend";
var strategy = _scenarioService.CreateStrategy(StrategyType.SuperTrend,
timeframe,
name,
period: 10,
multiplier: 3);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupChandelierExit(Timeframe timeframe)
{
var name = "ChandelierExit";
var strategy = _scenarioService.CreateStrategy(StrategyType.ChandelierExit,
timeframe,
name,
period: 22,
multiplier: 3);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupStochRsiTrend(Timeframe timeframe)
{
var name = "StochRsiTrend";
var strategy = _scenarioService.CreateStrategy(StrategyType.StochRsiTrend,
timeframe,
name,
period: 14,
stochPeriods: 14,
signalPeriods: 3,
smoothPeriods: 1);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupEmaTrend(Timeframe timeframe)
{
var name = "Ema200Trend";
var strategy = _scenarioService.CreateStrategy(StrategyType.EmaTrend,
timeframe,
name,
period: 200);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
private void SetupEmaCross(Timeframe timeframe)
{
var name = "Ema200Cross";
var strategy = _scenarioService.CreateStrategy(StrategyType.EmaCross,
timeframe,
name,
period: 200);
_scenarioService.CreateScenario(name, new List<string> { strategy.Name });
}
}

View File

@@ -0,0 +1,30 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace Managing.Application.Shared;
public class StreamService : IStreamService
{
private readonly IExchangeStream _exchangeStream;
private readonly IHubContext<CandleHub> _hubContext;
public StreamService(IExchangeStream exchangeStream, IHubContext<CandleHub> hubContext)
{
_exchangeStream = exchangeStream;
_hubContext = hubContext;
}
public async Task SubscribeCandle()
{
await _exchangeStream.StartBinanceWorker(Common.Enums.Ticker.BTC, async (candle) => {
await _hubContext.Clients.All.SendAsync(candle.Ticker, candle);
});
}
public async Task UnSubscribeCandle()
{
await _exchangeStream.StopBinanceWorker();
}
}

View File

@@ -0,0 +1,21 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using static Managing.Common.Enums;
namespace Managing.Application.Shared;
public class TickerService : ITickerService
{
private readonly IEvmManager _evmManager;
public TickerService(IEvmManager evmManager)
{
_evmManager = evmManager;
}
public async Task<List<Ticker>> GetAvailableTicker()
{
var tickers = await _evmManager.GetAvailableTicker();
return tickers;
}
}

View File

@@ -0,0 +1,55 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Trading;
public class ClosePositionCommandHandler : ICommandHandler<ClosePositionCommand, Position>
{
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
private readonly ITradingService _tradingService;
public ClosePositionCommandHandler(
IExchangeService exchangeService,
IAccountService accountService,
ITradingService tradingService)
{
_exchangeService = exchangeService;
_accountService = accountService;
_tradingService = tradingService;
}
public async Task<Position> Handle(ClosePositionCommand request)
{
// Get Trade
var account = await _accountService.GetAccount(request.Position.AccountName, false, false);
if (request.Position == null)
{
_ = _exchangeService.CancelOrder(account, request.Position.Ticker).Result;
return request.Position;
}
var isForPaperTrading = request.Position.Initiator == PositionInitiator.PaperTrading;
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading ?
request.ExecutionPrice.GetValueOrDefault() :
_exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
// Close market
var closedPosition = _exchangeService.ClosePosition(account, request.Position, lastPrice, isForPaperTrading).Result;
var closeRequestedOrders = isForPaperTrading ? true : _exchangeService.CancelOrder(account, request.Position.Ticker).Result;
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
{
request.Position.Status = PositionStatus.Finished;
request.Position.ProfitAndLoss = TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice);
_tradingService.UpdatePosition(request.Position);
}
return request.Position;
}
}

View File

@@ -0,0 +1,17 @@
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.Trading.Commands
{
public class ClosePositionCommand : IRequest<Position>
{
public ClosePositionCommand(Position position, decimal? executionPrice = null)
{
Position = position;
ExecutionPrice = executionPrice;
}
public Position Position { get; }
public decimal? ExecutionPrice { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using Managing.Common;
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.Trading.Commands
{
public class GetPositionsCommand : IRequest<List<Position>>
{
public GetPositionsCommand(Enums.PositionInitiator initiator)
{
Initiator = initiator;
}
public Enums.PositionInitiator Initiator { get; internal set; }
}
}

View File

@@ -0,0 +1,20 @@
using Managing.Domain.Trades;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.Trading.Commands
{
public class GetTradeCommand : IRequest<Trade>
{
public GetTradeCommand(string accountName, string exchangeOrderId, Ticker ticker)
{
AccountName = accountName;
ExchangeOrderId = exchangeOrderId;
Ticker = ticker;
}
public string AccountName { get; }
public string ExchangeOrderId { get; }
public Ticker Ticker { get; }
}
}

View File

@@ -0,0 +1,18 @@
using Managing.Domain.Trades;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.Trading.Commands
{
public class GetTradesCommand : IRequest<List<Trade>>
{
public GetTradesCommand(Ticker ticker, string accountName)
{
Ticker = ticker;
AccountName = accountName;
}
public string AccountName { get; }
public Ticker Ticker { get; }
}
}

View File

@@ -0,0 +1,48 @@
using Managing.Domain.MoneyManagements;
using Managing.Domain.Trades;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.Trading.Commands
{
public class OpenPositionRequest : IRequest<Position>
{
public OpenPositionRequest(
string accountName,
MoneyManagement moneyManagement,
TradeDirection direction,
Ticker ticker,
PositionInitiator initiator,
DateTime date,
bool isForPaperTrading = false,
decimal? price = null,
decimal? balance = 1000,
decimal? fee = null,
bool? ignoreSLTP = false)
{
AccountName = accountName;
MoneyManagement = moneyManagement;
Direction = direction;
Ticker = ticker;
IsForPaperTrading = isForPaperTrading;
Price = price;
Date = date;
Balance = balance;
Initiator = initiator;
Fee = fee;
IgnoreSLTP = ignoreSLTP;
}
public string AccountName { get; }
public MoneyManagement MoneyManagement { get; }
public TradeDirection Direction { get; }
public Ticker Ticker { get; }
public bool IsForPaperTrading { get; }
public decimal? Price { get; }
public decimal? Fee { get; }
public bool? IgnoreSLTP { get; }
public decimal? Balance { get; }
public DateTime Date { get; set; }
public PositionInitiator Initiator { get; internal set; }
}
}

View File

@@ -0,0 +1,23 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.Trading
{
public class GetPositionsCommandHandler : IRequestHandler<GetPositionsCommand, List<Position>>
{
private readonly ITradingService _tradingService;
public GetPositionsCommandHandler(ITradingService tradingService)
{
_tradingService = tradingService;
}
public Task<List<Position>> Handle(GetPositionsCommand request, CancellationToken cancellationToken)
{
var positions = _tradingService.GetPositions(request.Initiator);
return Task.FromResult(positions.ToList());
}
}
}

View File

@@ -0,0 +1,23 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.Trading;
public class GetTradeCommandHandler : IRequestHandler<GetTradeCommand, Trade>
{
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
public GetTradeCommandHandler(IExchangeService exchangeService, IAccountService accountService)
{
_exchangeService = exchangeService;
_accountService = accountService;
}
public Task<Trade> Handle(GetTradeCommand request, CancellationToken cancellationToken)
{
var account = _accountService.GetAccount(request.AccountName, true, false).Result;
return _exchangeService.GetTrade(account, request.ExchangeOrderId, request.Ticker);
}
}

View File

@@ -0,0 +1,25 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.Trading
{
public class GetTradesCommandHandler : IRequestHandler<GetTradesCommand, List<Trade>>
{
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
public GetTradesCommandHandler(IExchangeService exchangeService, IAccountService accountService)
{
_exchangeService = exchangeService;
_accountService = accountService;
}
public Task<List<Trade>> Handle(GetTradesCommand request, CancellationToken cancellationToken)
{
var account = _accountService.GetAccount(request.AccountName, true, false).Result;
return _exchangeService.GetTrades(account, request.Ticker);
}
}
}

View File

@@ -0,0 +1,128 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Trading
{
public class OpenPositionCommandHandler : ICommandHandler<OpenPositionRequest, Position>
{
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
private readonly ITradingService _tradingService;
public OpenPositionCommandHandler(
IExchangeService exchangeService,
IAccountService accountService,
ITradingService tradingService)
{
_exchangeService = exchangeService;
_accountService = accountService;
_tradingService = tradingService;
}
public Task<Position> Handle(OpenPositionRequest request)
{
var account = _accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false).Result;
if (!request.IsForPaperTrading && !_exchangeService.CancelOrder(account, request.Ticker).Result)
{
throw new Exception($"Not able to close all orders for {request.Ticker}");
}
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
var position = new Position(request.AccountName, request.Direction, request.Ticker, request.MoneyManagement, initiator, request.Date);
var balance = request.IsForPaperTrading ? request.Balance.GetValueOrDefault() : _exchangeService.GetBalance(account, request.IsForPaperTrading).Result;
var balanceAtRisk = RiskHelpers.GetBalanceAtRisk(balance, request.MoneyManagement);
if (balanceAtRisk < 13)
{
throw new Exception($"Try to risk {balanceAtRisk} $ but inferior to minimum to trade");
}
var price = request.IsForPaperTrading && request.Price.HasValue ?
request.Price.Value :
_exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
var quantity = balanceAtRisk / price;
var fee = request.IsForPaperTrading ? request.Fee.GetValueOrDefault() : _tradingService.GetFee(account, request.IsForPaperTrading);
var expectedStatus = GetExpectedStatus(request);
position.Open = TradingPolicies.OpenPosition(expectedStatus).Execute(
() =>
{
var openPrice = request.IsForPaperTrading || request.Price.HasValue
? request.Price.Value
: _exchangeService.GetBestPrice(account, request.Ticker, price, quantity, request.Direction);
var trade = _exchangeService.OpenTrade(
account,
request.Ticker,
request.Direction,
openPrice,
quantity,
request.MoneyManagement.Leverage,
TradeType.Limit,
isForPaperTrading: request.IsForPaperTrading,
currentDate: request.Date).Result;
trade.Fee = TradingHelpers.GetFeeAmount(fee, openPrice * quantity, account.Exchange);
return trade;
});
if (IsOpenTradeHandled(position.Open.Status, account.Exchange) && !request.IgnoreSLTP.GetValueOrDefault())
{
var closeDirection = request.Direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long;
// Stop loss
position.StopLoss = _exchangeService.BuildEmptyTrade(
request.Ticker,
RiskHelpers.GetStopLossPrice(request.Direction, position.Open.Price, request.MoneyManagement),
position.Open.Quantity,
closeDirection,
request.MoneyManagement.Leverage,
TradeType.StopLoss,
request.Date,
TradeStatus.PendingOpen);
position.StopLoss.Fee = TradingHelpers.GetFeeAmount(fee, position.StopLoss.Price * position.StopLoss.Quantity, account.Exchange);
// Take profit
position.TakeProfit1 = _exchangeService.BuildEmptyTrade(
request.Ticker,
RiskHelpers.GetTakeProfitPrice(request.Direction, position.Open.Price, request.MoneyManagement),
quantity,
closeDirection,
request.MoneyManagement.Leverage,
TradeType.TakeProfit,
request.Date,
TradeStatus.PendingOpen);
position.TakeProfit1.Fee = TradingHelpers.GetFeeAmount(fee, position.TakeProfit1.Price * position.TakeProfit1.Quantity, account.Exchange);
}
position.Status = IsOpenTradeHandled(position.Open.Status, account.Exchange) ? position.Status : PositionStatus.Rejected;
_tradingService.InsertPosition(position);
return Task.FromResult(position);
}
private static TradeStatus GetExpectedStatus(OpenPositionRequest request)
{
if (request.IsForPaperTrading)
{
return TradeStatus.Filled;
}
return TradeStatus.Requested;
}
private static bool IsOpenTradeHandled(TradeStatus tradeStatus, TradingExchanges exchange)
{
return tradeStatus == TradeStatus.Filled
|| (exchange == TradingExchanges.Evm && tradeStatus == TradeStatus.Requested);
}
}
}

View File

@@ -0,0 +1,18 @@
using Managing.Domain.Trades;
using Polly;
using Polly.Retry;
using static Managing.Common.Enums;
namespace Managing.Application.Trading;
public static class TradingPolicies
{
public static RetryPolicy<Trade> OpenPosition(TradeStatus tradeStatus)
{
var policy = Policy
.HandleResult<Trade>(res => res.Status != tradeStatus)
.WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(3));
return policy;
}
}

View File

@@ -0,0 +1,352 @@
using DnsClient.Internal;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Scenarios;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using Managing.Domain.Shared.Helpers;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using static Managing.Common.Enums;
namespace Managing.Application.Trading;
public class TradingService : ITradingService
{
private readonly ITradingRepository _tradingRepository;
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
private readonly ICacheService _cacheService;
private readonly IMessengerService _messengerService;
private readonly IStatisticRepository _statisticRepository;
private readonly ILogger<TradingService> _logger;
public TradingService(
ITradingRepository tradingRepository,
IExchangeService exchangeService,
ILogger<TradingService> logger,
IAccountService accountService,
ICacheService cacheService,
IMessengerService messengerService,
IStatisticRepository statisticRepository)
{
_tradingRepository = tradingRepository;
_exchangeService = exchangeService;
_logger = logger;
_accountService = accountService;
_cacheService = cacheService;
_messengerService = messengerService;
_statisticRepository = statisticRepository;
}
public void DeleteScenario(string name)
{
_tradingRepository.DeleteScenario(name);
}
public void DeleteScenarios()
{
_tradingRepository.DeleteScenarios();
}
public void DeleteStrategies()
{
_tradingRepository.DeleteStrategies();
}
public void DeleteStrategy(string name)
{
_tradingRepository.DeleteStrategy(name);
}
public Position GetPositionByIdentifier(string identifier)
{
return _tradingRepository.GetPositionByIdentifier(identifier);
}
public IEnumerable<Position> GetPositions(PositionInitiator positionInitiator)
{
return _tradingRepository.GetPositions(positionInitiator);
}
public IEnumerable<Position> GetPositionsByStatus(PositionStatus postionStatus)
{
return _tradingRepository.GetPositionsByStatus(postionStatus);
}
public Scenario GetScenarioByName(string scenario)
{
return _tradingRepository.GetScenarioByName(scenario);
}
public IEnumerable<Scenario> GetScenarios()
{
return _tradingRepository.GetScenarios();
}
public IEnumerable<Strategy> GetStrategies()
{
return _tradingRepository.GetStrategies();
}
public Strategy GetStrategyByName(string strategy)
{
return _tradingRepository.GetStrategyByName(strategy);
}
public void InsertPosition(Position position)
{
_tradingRepository.InsertPosition(position);
}
public void InsertScenario(Scenario scenario)
{
_tradingRepository.InsertScenario(scenario);
}
public void InsertSignal(Signal signal)
{
_tradingRepository.InsertSignal(signal);
}
public void InsertStrategy(Strategy strategy)
{
_tradingRepository.InsertStrategy(strategy);
}
public async Task<Position> ManagePosition(Account account, Position position)
{
var lastPrice = _exchangeService.GetPrice(account, position.Ticker, DateTime.UtcNow);
var quantityInPosition = await _exchangeService.GetQuantityInPosition(account, position.Ticker);
var orders = await _exchangeService.GetOpenOrders(account, position.Ticker);
if (quantityInPosition > 0)
{
// Position still open
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.Open.Quantity, lastPrice);
_logger.LogInformation($"Position is still open - PNL : {position.ProfitAndLoss.Realized} $");
_logger.LogInformation($"Requested trades : {orders.Count}");
}
else
{
// No quantity in position = SL/TP hit
if (orders.All(o => o.TradeType != TradeType.StopLoss))
{
// SL hit
_logger.LogInformation($"Stop loss is filled on exchange.");
position.StopLoss.SetStatus(TradeStatus.Filled);
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.StopLoss.Quantity, position.StopLoss.Price);
_ = _exchangeService.CancelOrder(account, position.Ticker);
}
else if (orders.All(o => o.TradeType != TradeType.TakeProfit))
{
// TP Hit
if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit2 != null)
{
position.TakeProfit2.SetStatus(TradeStatus.Filled);
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.TakeProfit2.Quantity, position.TakeProfit2.Price);
_logger.LogInformation($"TakeProfit 2 is filled on exchange.");
}
else
{
position.TakeProfit1.SetStatus(TradeStatus.Filled);
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.TakeProfit1.Quantity, position.TakeProfit1.Price);
_logger.LogInformation($"TakeProfit 1 is filled on exchange.");
}
}
else
{
_logger.LogInformation($"Position closed manually or forced close by exchange because quantity in position is below 0.");
position.Status = PositionStatus.Finished;
if (orders.Any()) await _exchangeService.CancelOrder(account, position.Ticker);
}
}
return position;
}
public void UpdateFee(TradingExchanges exchange)
{
var lastFee = _tradingRepository.GetFee(exchange);
var account = _accountService.GetAccounts(false, false).FirstOrDefault(a => a.Exchange == exchange);
if (lastFee != null)
{
if (DateTime.UtcNow.AddHours(-6) >= lastFee.LastUpdate)
{
lastFee.Cost = _exchangeService.GetFee(account);
lastFee.LastUpdate = DateTime.UtcNow;
_tradingRepository.UpdateFee(lastFee);
}
}
else
{
lastFee = new Fee
{
Cost = _exchangeService.GetFee(account),
Exchange = exchange,
LastUpdate = DateTime.UtcNow
};
_tradingRepository.InsertFee(lastFee);
}
}
public decimal GetFee(Account account, bool isForPaperTrading = false)
{
if (isForPaperTrading && account.Exchange != TradingExchanges.Evm)
{
return 0.000665M;
}
return _cacheService.GetOrSave($"Fee-{account.Exchange}", () =>
{
return _tradingRepository.GetFee(TradingExchanges.Evm).Cost;
}, TimeSpan.FromHours(2));
}
public void UpdatePosition(Position position)
{
_tradingRepository.UpdatePosition(position);
}
public IEnumerable<Position> GetPositions()
{
var positions = new List<Position>();
positions.AddRange(GetPositionsByStatus(PositionStatus.New));
positions.AddRange(GetPositionsByStatus(PositionStatus.Filled));
positions.AddRange(GetPositionsByStatus(PositionStatus.PartiallyFilled));
return positions;
}
public async Task WatchTrader()
{
var availableTickers = new List<Ticker> { Ticker.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK };
var watchAccount = GetTradersWatch();
var key = $"AccountsQuantityInPosition";
var aqip = _cacheService.GetValue<List<TraderFollowup>>(key);
if (aqip == null)
{
aqip = GetAccountsQuantityInPosition(watchAccount);
}
else
{
foreach (var a in watchAccount.Where(w => !aqip.Any(a => a.Account.Address == w.Address)))
{
var newAccount = SetupFollowUp(a);
aqip.Add(newAccount);
}
foreach (var a in aqip)
{
await ManageTrader(a, availableTickers);
}
}
_cacheService.SaveValue(key, aqip, TimeSpan.FromMinutes(10));
}
public IEnumerable<Trader> GetTradersWatch()
{
var watchAccount = _statisticRepository.GetBestTraders();
var customWatchAccount = _accountService.GetAccounts(true, false).Where(a => a.Type == AccountType.Watch).ToList().MapToTraders();
watchAccount.AddRange(customWatchAccount.Where(a => !watchAccount.Any(w => w.Address.Equals(a.Address, StringComparison.InvariantCultureIgnoreCase))));
return watchAccount;
}
private async Task ManageTrader(TraderFollowup a, List<Ticker> tickers)
{
var shortAddress = a.Account.Address.Substring(0, 6);
foreach (var ticker in tickers)
{
try
{
var newTrade = await _exchangeService.GetTrade(a.Account.Address, "", ticker);
var oldTrade = a.Trades.SingleOrDefault(t => t.Ticker == ticker);
if (newTrade == null)
{
if (oldTrade != null)
{
_logger.LogInformation($"[{shortAddress}][{ticker}] Trader previously got a position open but the position was close by trader");
await _messengerService.SendClosedPosition(a.Account.Address, oldTrade);
a.Trades.Remove(oldTrade);
}
}
else if ((newTrade != null && oldTrade == null) || (newTrade.Quantity > oldTrade.Quantity))
{
_logger.LogInformation($"[{shortAddress}][{ticker}] Trader increase {newTrade.Direction} by {newTrade.Quantity - (oldTrade?.Quantity ?? 0)} with leverage {newTrade.Leverage} at {newTrade.Price} leverage.");
var index = a.Trades.IndexOf(oldTrade);
if (index != -1)
{
a.Trades[index] = newTrade;
}
else
{
a.Trades.Add(newTrade);
}
// Open position
await _messengerService.SendIncreasePosition(a.Account.Address, newTrade, "Test6", oldTrade);
// Save position to cache
}
else if (newTrade.Quantity < oldTrade.Quantity && newTrade.Quantity > 0)
{
var decreaseAmount = oldTrade.Quantity - newTrade.Quantity;
var index = a.Trades.IndexOf(oldTrade);
a.Trades[index] = newTrade;
_logger.LogInformation($"[{a.Account.Address.Substring(0, 6)}][{ticker}] Trader decrease position but didnt close it {decreaseAmount}");
await _messengerService.SendDecreasePosition(a.Account.Address, newTrade, decreaseAmount);
}
else
{
_logger.LogInformation($"[{shortAddress}][{ticker}] No change - Quantity still {newTrade.Quantity}");
}
}
catch (Exception ex)
{
_logger.LogError($"[{shortAddress}][{ticker}] Impossible to fetch trader");
}
}
}
private List<TraderFollowup> GetAccountsQuantityInPosition(IEnumerable<Trader> watchAccount)
{
var result = new List<TraderFollowup> ();
foreach (var account in watchAccount)
{
var trader = SetupFollowUp(account);
result.Add(trader);
}
return result;
}
private static TraderFollowup SetupFollowUp(Trader account)
{
var trader = new TraderFollowup
{
Account = account,
Trades = new List<Trade>(),
PositionIdentifiers = new List<string>()
};
return trader;
}
public class TraderFollowup
{
public Trader Account { get; set; }
public List<Trade> Trades { get; set; }
public List<string> PositionIdentifiers { get; set; }
}
}

View File

@@ -0,0 +1,88 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Users;
namespace Managing.Application.Users;
public class UserService : IUserService
{
private readonly IEvmManager _evmManager;
private readonly IUserRepository _userRepository;
private readonly IAccountService _accountService;
public UserService(
IEvmManager evmManager,
IUserRepository userRepository,
IAccountService accountService)
{
_evmManager = evmManager;
_userRepository = userRepository;
_accountService = accountService;
}
public async Task<User> Authenticate(string name, string address, string message, string signature)
{
var recoveredAddress = _evmManager.VerifySignature(signature, message);
if (recoveredAddress == null || !recoveredAddress.Equals(address))
throw new Exception("Address not corresponding");
// Check if account exist
var account = await _accountService.GetAccountByKey(recoveredAddress, true, false);
var user = await _userRepository.GetUserByNameAsync(name);
if (account != null && user != null)
{
// User and account found
user.Accounts = _accountService.GetAccountsByUser(user).ToList();
if (!user.Name.Equals(name))
throw new Exception("Name not corresponding");
return user;
}
else
{
// No account and no
account = new Account
{
Name = $"Auth-{new Random().Next(1, 99)}",
Key = recoveredAddress,
Secret = "",
Exchange = Common.Enums.TradingExchanges.Evm,
Type = Common.Enums.AccountType.Auth,
};
if (user != null)
{
_ = await _accountService.CreateAccount(user, account);
user.Accounts = _accountService.GetAccountsByUser(user).ToList();
return user;
}
else
{
// No user found, we create one
// Create user if not existing
user = new User()
{
Name = name,
Accounts = new List<Account> { account },
};
_ = await _accountService.CreateAccount(user, account);
await _userRepository.InsertUserAsync(user);
}
}
return user;
}
public async Task<User> GetUserByAddressAsync(string address)
{
var account = await _accountService.GetAccountByKey(address, true, false);
var user = await _userRepository.GetUserByNameAsync(account.User.Name);
user.Accounts = _accountService.GetAccountsByUser(user).ToList();
return user;
}
}

View 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;
}
}

View 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; }
}

View File

@@ -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; }
}

View 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; }
}

View 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());
}
}