using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Indicators; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; using Managing.Domain.Strategies; using Managing.Domain.Strategies.Base; using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Managing.Domain.Users; using Managing.Infrastructure.Evm.Models.Privy; using Microsoft.Extensions.Logging; 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 IEvmManager _evmManager; private readonly ILogger _logger; private readonly ISynthPredictionService _synthPredictionService; public TradingService( ITradingRepository tradingRepository, IExchangeService exchangeService, ILogger logger, IAccountService accountService, ICacheService cacheService, IMessengerService messengerService, IStatisticRepository statisticRepository, IEvmManager evmManager, ISynthPredictionService synthPredictionService) { _tradingRepository = tradingRepository; _exchangeService = exchangeService; _logger = logger; _accountService = accountService; _cacheService = cacheService; _messengerService = messengerService; _statisticRepository = statisticRepository; _evmManager = evmManager; _synthPredictionService = synthPredictionService; } public async Task DeleteScenarioAsync(string name) { await _tradingRepository.DeleteScenarioAsync(name); } public async Task DeleteIndicatorAsync(string name) { await _tradingRepository.DeleteIndicatorAsync(name); } public async Task GetPositionByIdentifierAsync(Guid identifier) { return await _tradingRepository.GetPositionByIdentifierAsync(identifier); } public async Task> GetPositionsAsync(PositionInitiator positionInitiator) { return await _tradingRepository.GetPositionsAsync(positionInitiator); } public async Task> GetPositionsByStatusAsync(PositionStatus postionStatus) { return await _tradingRepository.GetPositionsByStatusAsync(postionStatus); } public async Task GetScenarioByNameAsync(string scenario) { return await _tradingRepository.GetScenarioByNameAsync(scenario); } public async Task> GetScenariosAsync() { return await _tradingRepository.GetScenariosAsync(); } public async Task> GetIndicatorsAsync() { return await _tradingRepository.GetStrategiesAsync(); } public async Task GetIndicatorByNameAsync(string strategy) { return await _tradingRepository.GetStrategyByNameAsync(strategy); } public async Task InsertPositionAsync(Position position) { await _tradingRepository.InsertPositionAsync(position); } public async Task InsertScenarioAsync(Scenario scenario) { await _tradingRepository.InsertScenarioAsync(scenario); } public async Task InsertIndicatorAsync(IndicatorBase indicatorBase) { await _tradingRepository.InsertIndicatorAsync(indicatorBase); } public async Task ManagePosition(Account account, Position position) { var lastPrice = await _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, position.Open.Leverage); _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, position.Open.Leverage); _ = _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, 1); _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, 1); _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 async Task UpdatePositionAsync(Position position) { await _tradingRepository.UpdatePositionAsync(position); } public async Task> GetPositionsAsync() { var positions = new List(); positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.New)); positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.Filled)); positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.PartiallyFilled)); return positions; } public async Task WatchTrader() { var availableTickers = new List { Ticker.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK }; var watchAccount = await 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 async Task> GetTradersWatch() { var watchAccount = await _statisticRepository.GetBestTradersAsync(); var customWatchAccount = (await _accountService.GetAccountsAsync(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; } public void UpdateDeltaNeutralOpportunities() { var fundingRates = _exchangeService.GetFundingRates(); } public async Task UpdateScenarioAsync(Scenario scenario) { await _tradingRepository.UpdateScenarioAsync(scenario); } public async Task UpdateIndicatorAsync(IndicatorBase indicatorBase) { await _tradingRepository.UpdateStrategyAsync(indicatorBase); } public async Task> GetBrokerPositions(Account account) { return await _exchangeService.GetBrokerPositions(account); } 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; } } public async Task InitPrivyWallet(string publicAddress) { try { if (string.IsNullOrEmpty(publicAddress)) { _logger.LogWarning("Attempted to initialize Privy wallet with null or empty public address"); return new PrivyInitAddressResponse { Success = false, Error = "Public address cannot be null or empty" }; } return await _evmManager.InitAddress(publicAddress); } catch (Exception ex) { _logger.LogError(ex, "Error initializing Privy wallet for address {PublicAddress}", publicAddress); return new PrivyInitAddressResponse { Success = false, Error = ex.Message }; } } // Synth API integration methods public async Task ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice, TradingBotConfig botConfig, bool isBacktest) { return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest); } public async Task AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, TradingBotConfig botConfig, bool isBacktest) { return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice, botConfig, isBacktest); } public async Task MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig) { return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice, positionIdentifier, botConfig); } /// /// Calculates indicators values for a given scenario and candles. /// /// The scenario containing indicators. /// The candles to calculate indicators for. /// A dictionary of indicator types to their calculated values. public Dictionary CalculateIndicatorsValuesAsync( Scenario scenario, HashSet candles) { var indicatorsValues = new Dictionary(); if (scenario?.Indicators == null || scenario.Indicators.Count == 0) { return indicatorsValues; } // Build indicators from scenario foreach (var indicator in scenario.Indicators) { try { indicatorsValues[indicator.Type] = indicator.GetIndicatorValues(candles); } catch (Exception ex) { _logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}", indicator.Name, ex.Message); } } return indicatorsValues; } public async Task GetIndicatorByNameUserAsync(string name, User user) { return await _tradingRepository.GetStrategyByNameUserAsync(name, user); } public async Task GetScenarioByNameUserAsync(string scenarioName, User user) { return await _tradingRepository.GetScenarioByNameUserAsync(scenarioName, user); } }