using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; using Managing.Domain.Backtests; 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 IAccountRepository _accountRepository; private readonly ICacheService _cacheService; private readonly IMessengerService _messengerService; private readonly IStatisticRepository _statisticRepository; private readonly IEvmManager _evmManager; private readonly ILogger _logger; private readonly ISynthPredictionService _synthPredictionService; private readonly IWeb3ProxyService _web3ProxyService; public TradingService( ITradingRepository tradingRepository, IExchangeService exchangeService, ILogger logger, IAccountService accountService, IAccountRepository accountRepository, ICacheService cacheService, IMessengerService messengerService, IStatisticRepository statisticRepository, IEvmManager evmManager, ISynthPredictionService synthPredictionService, IWeb3ProxyService web3ProxyService) { _tradingRepository = tradingRepository; _exchangeService = exchangeService; _logger = logger; _accountService = accountService; _accountRepository = accountRepository; _cacheService = cacheService; _messengerService = messengerService; _statisticRepository = statisticRepository; _evmManager = evmManager; _synthPredictionService = synthPredictionService; _web3ProxyService = web3ProxyService; } 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> GetScenariosByUserAsync(User user) { return await _tradingRepository.GetScenariosByUserAsync(user); } 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 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); } public async Task> GetAllDatabasePositionsAsync() { return await _tradingRepository.GetAllPositionsAsync(); } public async Task> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier) { return await _tradingRepository.GetPositionsByInitiatorIdentifierAsync(initiatorIdentifier); } public async Task> GetPositionsByInitiatorIdentifiersAsync( IEnumerable initiatorIdentifiers) { return await _tradingRepository.GetPositionsByInitiatorIdentifiersAsync(initiatorIdentifiers); } public async Task DeletePositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier) { await _tradingRepository.DeletePositionsByInitiatorIdentifierAsync(initiatorIdentifier); } public async Task GetGlobalPnLFromPositionsAsync() { return await _tradingRepository.GetGlobalPnLFromPositionsAsync(); } 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"); // TODO : Send position closed 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) { _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, TradingExchanges tradingExchange) { 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" }; } // Check if the account is already initialized var account = await _accountRepository.GetAccountByKeyAsync(publicAddress); if (account != null && account.IsGmxInitialized) { _logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX", publicAddress); return new PrivyInitAddressResponse { Success = true, Address = publicAddress, IsAlreadyInitialized = true }; } PrivyInitAddressResponse initResult; switch (tradingExchange) { case TradingExchanges.GmxV2: initResult = await _evmManager.InitAddressForGMX(publicAddress); break; default: initResult = await _evmManager.InitAddressForGMX(publicAddress); break; } // If initialization was successful, update the account's initialization status if (initResult.Success && account != null) { account.IsGmxInitialized = true; await _accountRepository.UpdateAccountAsync(account); _logger.LogInformation("Updated account {AccountName} GMX initialization status to true", account.Name); } return initResult; } 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); } 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); } public async Task> GetPositionByUserIdAsync(int userId) { return await _tradingRepository.GetPositionByUserIdAsync(userId); } public async Task SwapGmxTokensAsync(User user, string accountName, Ticker fromTicker, Ticker toTicker, double amount, string orderType = "market", double? triggerRatio = null, double? allowedSlippage = null) { // Get the account for the user var account = await _accountService.GetAccountByUser(user, accountName, true, false); if (account == null) { throw new ArgumentException($"Account '{accountName}' not found for user '{user.Name}'"); } // Ensure the account has a valid address/key if (string.IsNullOrEmpty(account.Key)) { throw new ArgumentException($"Account '{accountName}' does not have a valid address"); } try { // Get user's config to access default slippage value var config = TradingBox.CreateConfigFromUserSettings(user); // Use provided allowedSlippage if specified, otherwise use user's GmxSlippage setting, or default from IndicatorComboConfig var slippageToUse = (double)(allowedSlippage.HasValue ? (decimal)allowedSlippage.Value : (user.GmxSlippage ?? config.GmxSlippage)); // Call the Web3ProxyService to swap GMX tokens var swapInfos = await _web3ProxyService.SwapGmxTokensAsync( account.Key, fromTicker, toTicker, amount, orderType, triggerRatio, slippageToUse ); return swapInfos; } catch (Exception ex) when (!(ex is ArgumentException || ex is InvalidOperationException)) { _logger.LogError(ex, "Error swapping GMX tokens for account {AccountName} and user {UserName}", accountName, user.Name); SentrySdk.CaptureException(ex); throw new InvalidOperationException($"Failed to swap GMX tokens: {ex.Message}", ex); } } /// /// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators. /// public async Task RefineIndicatorsAsync( Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate, List indicators) { // Get candles for the specified period var candles = await _exchangeService.GetCandlesInflux( TradingExchanges.Evm, ticker, startDate, timeframe, endDate); if (candles == null || candles.Count == 0) { return new RefineIndicatorsResult { IndicatorsValues = new Dictionary(), Signals = new List() }; } // Convert to ordered List to preserve chronological order for indicators var candlesList = candles.OrderBy(c => c.Date).ToList(); // Map request indicators to domain Scenario var scenario = MapRefineIndicatorsToScenario(indicators); // Calculate indicators values var indicatorsValues = TradingBox.CalculateIndicatorsValues(scenario, candlesList); // Generate signals for the date range using rolling window approach var signals = GenerateSignalsForDateRange(candlesList, scenario, indicatorsValues); return new RefineIndicatorsResult { IndicatorsValues = indicatorsValues, Signals = signals }; } /// /// Maps IndicatorRequest list to a domain Scenario object. /// private Scenario MapRefineIndicatorsToScenario(List indicators) { var scenario = new Scenario("RefineIndicators", 1); foreach (var indicatorRequest in indicators) { var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type) { SignalType = indicatorRequest.SignalType, MinimumHistory = indicatorRequest.MinimumHistory, Period = indicatorRequest.Period, FastPeriods = indicatorRequest.FastPeriods, SlowPeriods = indicatorRequest.SlowPeriods, SignalPeriods = indicatorRequest.SignalPeriods, Multiplier = indicatorRequest.Multiplier, StDev = indicatorRequest.StDev, SmoothPeriods = indicatorRequest.SmoothPeriods, StochPeriods = indicatorRequest.StochPeriods, CyclePeriods = indicatorRequest.CyclePeriods, KFactor = indicatorRequest.KFactor, DFactor = indicatorRequest.DFactor, TenkanPeriods = indicatorRequest.TenkanPeriods, KijunPeriods = indicatorRequest.KijunPeriods, SenkouBPeriods = indicatorRequest.SenkouBPeriods, OffsetPeriods = indicatorRequest.OffsetPeriods, SenkouOffset = indicatorRequest.SenkouOffset, ChikouOffset = indicatorRequest.ChikouOffset }; scenario.AddIndicator(indicator); } return scenario; } /// /// Generates signals for a date range using a rolling window approach to capture signals throughout the entire range. /// This is necessary because some indicators (like STC) only process recent candles, so we need to process incrementally. /// private List GenerateSignalsForDateRange( List candles, Scenario scenario, Dictionary preCalculatedIndicatorValues) { var allSignals = new List(); var seenSignalIdentifiers = new HashSet(); var lightScenario = LightScenario.FromScenario(scenario); // Use rolling window approach to process candles incrementally // This ensures we capture signals throughout the entire date range, not just recent ones const int RollingWindowSize = 600; var rollingWindowCandles = new List(RollingWindowSize); // Process each candle with a rolling window foreach (var candle in candles) { // Maintain rolling window if (rollingWindowCandles.Count >= RollingWindowSize) { rollingWindowCandles.RemoveAt(0); } rollingWindowCandles.Add(candle); // Only process if we have enough candles for indicators if (rollingWindowCandles.Count < 2) { continue; } // Process each indicator individually for this rolling window position foreach (var lightIndicator in lightScenario.Indicators) { // Build the indicator instance (create fresh instance for each indicator to avoid state issues) var indicatorInstance = lightIndicator.ToInterface(); // Use pre-calculated indicator values if available List indicatorSignals; if (preCalculatedIndicatorValues != null && preCalculatedIndicatorValues.ContainsKey(lightIndicator.Type)) { // Use pre-calculated values to avoid recalculating indicators indicatorSignals = indicatorInstance.Run(rollingWindowCandles, preCalculatedIndicatorValues[lightIndicator.Type]); } else { // Normal path: calculate indicators on the fly indicatorSignals = indicatorInstance.Run(rollingWindowCandles); } // Add new signals from this indicator to the collection if (indicatorSignals != null && indicatorSignals.Count > 0) { // Filter signals to only include those within the requested date range var firstCandleDate = candles.First().Date; var lastCandleDate = candles.Last().Date; foreach (var signal in indicatorSignals) { // Only add signals within the date range and avoid duplicates if (signal.Date >= firstCandleDate && signal.Date <= lastCandleDate && !seenSignalIdentifiers.Contains(signal.Identifier)) { allSignals.Add(signal); seenSignalIdentifiers.Add(signal.Identifier); } } } } } // Sort by date and return return allSignals.OrderBy(s => s.Date).ToList(); } }