using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; using Managing.Domain.Strategies; using Managing.Domain.Trades; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Application.Workers; public class StatisticService : IStatisticService { private readonly IStatisticRepository _statisticRepository; private readonly IExchangeService _exchangeService; private readonly IAccountService _accountService; private readonly IEvmManager _evmManager; private readonly ITradingService _tradingService; private readonly IBacktester _backtester; private readonly ITradaoService _tradaoService; private readonly IMessengerService _messengerService; private readonly ICacheService _cacheService; private readonly IAgentBalanceRepository _agentBalanceRepository; private readonly ILogger _logger; public StatisticService( IExchangeService exchangeService, IAccountService accountService, ILogger logger, IStatisticRepository statisticRepository, IEvmManager evmManager, ITradingService tradingService, IBacktester backtester, ITradaoService tradaoService, IMessengerService messengerService, ICacheService cacheService, IAgentBalanceRepository agentBalanceRepository) { _exchangeService = exchangeService; _accountService = accountService; _logger = logger; _statisticRepository = statisticRepository; _evmManager = evmManager; _tradingService = tradingService; _backtester = backtester; _tradaoService = tradaoService; _messengerService = messengerService; _cacheService = cacheService; _agentBalanceRepository = agentBalanceRepository; } public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top) { var account = _accountService.GetAccounts(false, false).FirstOrDefault(a => a.Exchange == exchange); var date = DateTime.UtcNow; if (account == null) throw new Exception($"Enable to found account for exchange {exchange}"); var lastTop = GetLastTopVolumeTicker(); if (lastTop != null && lastTop.Count > 0) { _logger.LogInformation($"A top of {lastTop.Count} already exist for the current rage"); return; } var volumeTickers = new Dictionary(); foreach (var ticker in (Ticker[])Enum.GetValues(typeof(Ticker))) { var volume = _exchangeService.GetVolume(account, ticker); var price = _exchangeService.GetPrice(account, ticker, date); volumeTickers.Add(ticker, volume * price); } var currentTop = volumeTickers.OrderByDescending(v => v.Value).Take(top).ToList(); for (int rank = 0; rank < currentTop.Count; rank++) { var dto = new TopVolumeTicker() { Date = date, Rank = rank + 1, Ticker = currentTop[rank].Key, Volume = currentTop[rank].Value, Exchange = exchange }; await _statisticRepository.InsertTopVolumeTicker(dto); } } public async Task UpdateFundingRates() { // Get fundingRate from database var previousFundingRate = await GetFundingRates(); var newFundingRates = await _evmManager.GetFundingRates(); var topRates = newFundingRates .Where(fr => fr.Direction == TradeDirection.Short && fr.Rate > 0) .OrderByDescending(fr => fr.Rate) .Take(3) .ToList(); topRates.AddRange(newFundingRates .Where(fr => fr.Direction == TradeDirection.Long && fr.Rate > 0) .OrderBy(fr => fr.Rate) .TakeLast(3) .ToList()); // Old position not in the new top foreach (var oldRate in previousFundingRate) { if (topRates.All(tr => !SameFundingRate(tr, oldRate))) { // Close position await _messengerService.SendDowngradedFundingRate(oldRate); await _statisticRepository.RemoveFundingRate(oldRate); } } // New position not in the old top foreach (var newRate in topRates) { if (previousFundingRate.All(tr => !SameFundingRate(tr, newRate))) { // Open position await _messengerService.SendNewTopFundingRate(newRate); await _statisticRepository.InsertFundingRate(newRate); } else if (previousFundingRate.Any(tr => SameFundingRate(tr, newRate))) { var oldRate = previousFundingRate.FirstOrDefault(tr => SameFundingRate(tr, newRate)); if (oldRate != null && Math.Abs(oldRate.Rate - newRate.Rate) > 5m) { await _messengerService.SendFundingRateUpdate(oldRate, newRate); _statisticRepository.UpdateFundingRate(oldRate, newRate); } } } } private bool SameFundingRate(FundingRate oldRate, FundingRate newRate) { return oldRate.Ticker == newRate.Ticker && oldRate.Exchange == newRate.Exchange && oldRate.Direction == newRate.Direction; } public Task> GetFundingRates() { var previousFundingRate = _statisticRepository.GetFundingRates(); return Task.FromResult(previousFundingRate); } public IList GetLastTopVolumeTicker() { var from = DateTime.UtcNow.AddDays(-1); return _statisticRepository.GetTopVolumeTickers(from); } public async Task> GetTickers() { var cachedTickers = _cacheService.GetValue>("tickers"); if (cachedTickers != null) { return cachedTickers; } var tickers = await _evmManager.GetAvailableTicker(); _cacheService.SaveValue("tickers", tickers, TimeSpan.FromDays(1)); return tickers; } public async Task UpdateSpotlight() { var scenarios = _tradingService.GetScenarios(); var account = _accountService.GetAccounts(false, false).FirstOrDefault(a => a.Exchange == TradingExchanges.Evm); if (account == null) throw new Exception($"Enable to found default account"); var overview = GetLastSpotlight(DateTime.Now.AddMinutes(-20)); if (overview != null) { if (overview.Spotlights.Count < overview.ScenarioCount) { _logger.LogInformation( $"Spotlights not up to date. {overview.Spotlights.Count}/{overview.ScenarioCount}"); } else { _logger.LogInformation("No need to update spotlights"); return; } } else { overview = new SpotlightOverview { Spotlights = new List(), DateTime = DateTime.Now, Identifier = Guid.NewGuid(), ScenarioCount = scenarios.Count(), }; await _statisticRepository.SaveSpotligthtOverview(overview); } var tickers = await GetTickers(); foreach (var scenario in scenarios) { if (overview.Spotlights.Any(s => s.Scenario.Name == scenario.Name)) continue; var spotlight = new Spotlight { TickerSignals = new List(), Scenario = scenario }; var options = new ParallelOptions() { MaxDegreeOfParallelism = 2 }; _ = Parallel.ForEach(tickers, options, async ticker => { spotlight.TickerSignals.Add(new TickerSignal { Ticker = ticker, FiveMinutes = await GetSignals(account, scenario, ticker, Timeframe.FiveMinutes), FifteenMinutes = await GetSignals(account, scenario, ticker, Timeframe.FifteenMinutes), OneHour = await GetSignals(account, scenario, ticker, Timeframe.OneHour), FourHour = await GetSignals(account, scenario, ticker, Timeframe.FourHour), OneDay = await GetSignals(account, scenario, ticker, Timeframe.OneDay) }); }); overview.Spotlights.Add(spotlight); _statisticRepository.UpdateSpotlightOverview(overview); } overview.DateTime = DateTime.Now; _statisticRepository.UpdateSpotlightOverview(overview); } private async Task> GetSignals(Account account, Scenario scenario, Ticker ticker, Timeframe timeframe) { try { var moneyManagement = new MoneyManagement() { Leverage = 1, Timeframe = timeframe, StopLoss = 0.008m, TakeProfit = 0.02m }; var config = new TradingBotConfig { AccountName = account.Name, MoneyManagement = moneyManagement, Ticker = ticker, ScenarioName = scenario.Name, Timeframe = timeframe, IsForWatchingOnly = true, BotTradingBalance = 1000, BotType = BotType.ScalpingBot, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, Name = "StatisticsBacktest", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }; var backtest = await _backtester.RunScalpingBotBacktest( config, DateTime.Now.AddDays(-7), DateTime.Now, null, false, null); return backtest.Signals; } catch (Exception ex) { _logger.LogError("Backtest cannot be run {message}", ex.Message); } return null; } public SpotlightOverview GetLastSpotlight(DateTime dateTime) { var overviews = _statisticRepository.GetSpotlightOverviews(dateTime); if (overviews.Any()) { return overviews.OrderBy(o => o.DateTime).Last(); } return null; } public List GetBestTraders() { return _statisticRepository.GetBestTraders(); } public List GetBadTraders() { return _statisticRepository.GetBadTraders(); } public async Task> GetLeadboardPositons() { var customWatchAccount = _tradingService.GetTradersWatch(); var trades = new List(); foreach (var trader in customWatchAccount) { trades.AddRange(await _tradaoService.GetTrades(trader.Address)); } return trades; } public async Task UpdateLeaderboard() { var previousBestTraders = _statisticRepository.GetBestTraders(); var lastBestTrader = (await _tradaoService.GetBestTrader()).FindGoodTrader(); // Update / Insert best trader foreach (var trader in lastBestTrader) { if (previousBestTraders.Exists((p) => p.Address == trader.Address)) { _statisticRepository.UpdateBestTrader(trader); } else { await _statisticRepository.InsertBestTrader(trader); } } // Remove trader that wasnt good enough foreach (var trader in previousBestTraders) { if (!lastBestTrader.Exists((t) => t.Address == trader.Address)) { await _statisticRepository.RemoveBestTrader(trader); } } await _messengerService.SendBestTraders(lastBestTrader); } public async Task UpdateNoobiesboard() { var previousBadTraders = _statisticRepository.GetBadTraders(); var lastBadTrader = (await _tradaoService.GetBadTrader()).FindBadTrader(); // Update / Insert best trader foreach (var trader in lastBadTrader) { if (previousBadTraders.Exists((p) => p.Address == trader.Address)) { _statisticRepository.UpdateBadTrader(trader); } else { await _statisticRepository.InsertBadTrader(trader); } } // Remove trader that wasnt good enough foreach (var trader in previousBadTraders) { if (!lastBadTrader.Exists((t) => t.Address == trader.Address)) { await _statisticRepository.RemoveBadTrader(trader); } } await _messengerService.SendBadTraders(lastBadTrader); } public async Task GetAgentBalances(string agentName, DateTime start, DateTime? end = null) { var effectiveEnd = end ?? DateTime.UtcNow; string cacheKey = $"AgentBalances_{agentName}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}"; // Check if the balances are already cached var cachedBalances = _cacheService.GetValue(cacheKey); if (cachedBalances != null) { return cachedBalances; } var balances = await _agentBalanceRepository.GetAgentBalances(agentName, start, end); // Create a single AgentBalanceHistory with all balances var result = new AgentBalanceHistory { AgentName = agentName, AgentBalances = balances.OrderBy(b => b.Time).ToList() }; // Cache the results for 5 minutes _cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5)); return result; } public async Task<(IList Agents, int TotalCount)> GetBestAgents( DateTime start, DateTime? end = null, int page = 1, int pageSize = 10) { var effectiveEnd = end ?? DateTime.UtcNow; string cacheKey = $"BestAgents_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}"; // Check if the results are already cached var cachedResult = _cacheService.GetValue<(IList, int)>(cacheKey); if (cachedResult != default) { // Apply pagination to cached results var (cachedAgents, cachedTotalCount) = cachedResult; var paginatedAgents = cachedAgents .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); return (paginatedAgents, cachedTotalCount); } // Get all agents with their balance history var (fetchedAgents, fetchedTotalCount) = await _agentBalanceRepository.GetAllAgentBalancesWithHistory(start, end); // Cache all results for 5 minutes _cacheService.SaveValue(cacheKey, (fetchedAgents, fetchedTotalCount), TimeSpan.FromMinutes(5)); // Apply pagination var result = fetchedAgents .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); return (result, fetchedTotalCount); } }