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