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 _logger; public TradingService( ITradingRepository tradingRepository, IExchangeService exchangeService, ILogger 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 GetPositions(PositionInitiator positionInitiator) { return _tradingRepository.GetPositions(positionInitiator); } public IEnumerable GetPositionsByStatus(PositionStatus postionStatus) { return _tradingRepository.GetPositionsByStatus(postionStatus); } public Scenario GetScenarioByName(string scenario) { return _tradingRepository.GetScenarioByName(scenario); } public IEnumerable GetScenarios() { return _tradingRepository.GetScenarios(); } public IEnumerable 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 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 GetPositions() { var positions = new List(); 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.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK }; var watchAccount = GetTradersWatch(); var key = $"AccountsQuantityInPosition"; var aqip = _cacheService.GetValue>(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 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 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 GetAccountsQuantityInPosition(IEnumerable watchAccount) { var result = new List (); 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(), PositionIdentifiers = new List() }; return trader; } public class TraderFollowup { public Trader Account { get; set; } public List Trades { get; set; } public List PositionIdentifiers { get; set; } } }