using Managing.Api.Models.Requests; using Managing.Api.Models.Responses; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.ManageBot.Commands; using Managing.Core; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; 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.Trades; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using static Managing.Common.Enums; using DailySnapshot = Managing.Api.Models.Responses.DailySnapshot; namespace Managing.Api.Controllers; /// /// Controller for handling data-related operations such as retrieving tickers, spotlight data, and candles. /// Requires authorization for access. /// [AllowAnonymous] [ApiController] [Route("[controller]")] public class DataController : ControllerBase { private readonly IExchangeService _exchangeService; private readonly IAccountService _accountService; private readonly ICacheService _cacheService; private readonly IStatisticService _statisticService; private readonly IAgentService _agentService; private readonly IMediator _mediator; private readonly ITradingService _tradingService; private readonly IGrainFactory _grainFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IBotService _botService; private readonly IConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// Service for interacting with exchanges. /// Service for account management. /// Service for caching data. /// Service for statistical analysis. /// Service for agents /// Mediator for handling commands and queries. /// Service for trading operations. /// Orleans grain factory for accessing grains. /// Service scope factory for creating scoped services. /// Service for bot operations. /// Configuration for accessing environment variables. public DataController( IExchangeService exchangeService, IAccountService accountService, ICacheService cacheService, IStatisticService statisticService, IAgentService agentService, IMediator mediator, ITradingService tradingService, IGrainFactory grainFactory, IServiceScopeFactory serviceScopeFactory, IBotService botService, IConfiguration configuration) { _exchangeService = exchangeService; _accountService = accountService; _cacheService = cacheService; _statisticService = statisticService; _agentService = agentService; _mediator = mediator; _tradingService = tradingService; _grainFactory = grainFactory; _serviceScopeFactory = serviceScopeFactory; _botService = botService; _configuration = configuration; } /// /// Retrieves tickers for a given account and timeframe, utilizing caching to improve performance. /// /// The timeframe for which to retrieve tickers. /// An array of tickers. [HttpGet("GetTickers")] public async Task>> GetTickers([FromQuery] Timeframe timeframe) { var cacheKey = string.Concat(timeframe.ToString()); var tickers = _cacheService.GetValue>(cacheKey); if (tickers == null || tickers.Count == 0) { var availableTicker = await _exchangeService.GetTickers(timeframe); tickers = MapTickerToTickerInfos(availableTicker); _cacheService.SaveValue(cacheKey, tickers, TimeSpan.FromHours(2)); } return Ok(tickers); } private List MapTickerToTickerInfos(List availableTicker) { var tickerInfos = new List(); var tokens = new Dictionary { { "AAVE", "https://assets.coingecko.com/coins/images/12645/standard/AAVE.png?1696512452" }, { "ADA", "https://assets.coingecko.com/coins/images/975/standard/cardano.png?1696502090" }, { "APE", "https://assets.coingecko.com/coins/images/24383/standard/apecoin.jpg?1696523566" }, { "ARB", "https://assets.coingecko.com/coins/images/16547/small/photo_2023-03-29_21.47.00.jpeg?1680097630" }, { "ATOM", "https://assets.coingecko.com/coins/images/1481/standard/cosmos_hub.png?1696502525" }, { "AVAX", "https://assets.coingecko.com/coins/images/12559/small/coin-round-red.png?1604021818" }, { "BNB", "https://assets.coingecko.com/coins/images/825/standard/bnb-icon2_2x.png?1696501970" }, { "BTC", "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579" }, { "DOGE", "https://assets.coingecko.com/coins/images/5/small/dogecoin.png?1547792256" }, { "DOT", "https://static.coingecko.com/s/polkadot-73b0c058cae10a2f076a82dcade5cbe38601fad05d5e6211188f09eb96fa4617.gif" }, { "ETH", "https://assets.coingecko.com/coins/images/279/small/ethereum.png?1595348880" }, { "FIL", "https://assets.coingecko.com/coins/images/12817/standard/filecoin.png?1696512609" }, { "GMX", "https://assets.coingecko.com/coins/images/18323/small/arbit.png?1631532468" }, { "LINK", "https://assets.coingecko.com/coins/images/877/thumb/chainlink-new-logo.png?1547034700" }, { "LTC", "https://assets.coingecko.com/coins/images/2/small/litecoin.png?1547033580" }, { "MATIC", "https://assets.coingecko.com/coins/images/32440/standard/polygon.png?1698233684" }, { "NEAR", "https://assets.coingecko.com/coins/images/10365/standard/near.jpg?1696510367" }, { "OP", "https://assets.coingecko.com/coins/images/25244/standard/Optimism.png?1696524385" }, { "PEPE", "https://assets.coingecko.com/coins/images/29850/standard/pepe-token.jpeg?1696528776" }, { "SOL", "https://assets.coingecko.com/coins/images/4128/small/solana.png?1640133422" }, { "UNI", "https://assets.coingecko.com/coins/images/12504/thumb/uniswap-uni.png?1600306604" }, { "USDC", "https://assets.coingecko.com/coins/images/6319/thumb/USD_Coin_icon.png?1547042389" }, { "USDT", "https://assets.coingecko.com/coins/images/325/thumb/Tether-logo.png?1598003707" }, { "WIF", "https://assets.coingecko.com/coins/images/33566/standard/dogwifhat.jpg?1702499428" }, { "XRP", "https://assets.coingecko.com/coins/images/44/small/xrp-symbol-white-128.png?1605778731" }, { "SHIB", "https://assets.coingecko.com/coins/images/11939/standard/shiba.png?1696511800" }, { "STX", "https://assets.coingecko.com/coins/images/2069/standard/Stacks_Logo_png.png?1709979332" }, { "ORDI", "https://assets.coingecko.com/coins/images/30162/standard/ordi.png?1696529082" }, { "APT", "https://assets.coingecko.com/coins/images/26455/standard/aptos_round.png?1696525528" }, { "BOME", "https://assets.coingecko.com/coins/images/36071/standard/bome.png?1710407255" }, { "MEME", "https://assets.coingecko.com/coins/images/32528/standard/memecoin_%282%29.png?1698912168" }, { "FLOKI", "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318" }, { "MEW", "https://assets.coingecko.com/coins/images/36440/standard/MEW.png?1711442286" }, { "TAO", "https://assets.coingecko.com/coins/images/28452/standard/ARUsPeNQ_400x400.jpeg?1696527447" }, { "BONK", "https://assets.coingecko.com/coins/images/28600/standard/bonk.jpg?1696527587" }, { "WLD", "https://assets.coingecko.com/coins/images/31069/standard/worldcoin.jpeg?1696529903" }, { "tBTC", "https://assets.coingecko.com/coins/images/11224/standard/0x18084fba666a33d37592fa2633fd49a74dd93a88.png?1696511155" }, { "EIGEN", "https://assets.coingecko.com/coins/images/37441/standard/eigen.jpg?1728023974" }, { "SUI", "https://assets.coingecko.com/coins/images/26375/standard/sui-ocean-square.png?1727791290" }, { "SEI", "https://assets.coingecko.com/coins/images/28205/standard/Sei_Logo_-_Transparent.png?1696527207" }, { "DAI", "https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734" }, { "TIA", "https://assets.coingecko.com/coins/images/31967/standard/tia.jpg?1696530772" }, { "TRX", "https://assets.coingecko.com/coins/images/1094/standard/tron-logo.png?1696502193" }, { "TON", "https://assets.coingecko.com/coins/images/17980/standard/photo_2024-09-10_17.09.00.jpeg?1725963446" }, { "PENDLE", "https://assets.coingecko.com/coins/images/15069/standard/Pendle_Logo_Normal-03.png?1696514728" }, { "wstETH", "https://assets.coingecko.com/coins/images/18834/standard/wstETH.png?1696518295" }, { "USDe", "https://assets.coingecko.com/coins/images/33613/standard/USDE.png?1716355685" }, { "SATS", "https://assets.coingecko.com/coins/images/30666/standard/_dD8qr3M_400x400.png?1702913020" }, { "POL", "https://assets.coingecko.com/coins/images/32440/standard/polygon.png?1698233684" }, { "XLM", "https://assets.coingecko.com/coins/images/100/standard/Stellar_symbol_black_RGB.png?1696501482" }, { "BCH", "https://assets.coingecko.com/coins/images/780/standard/bitcoin-cash-circle.png?1696501932" }, { "ICP", "https://assets.coingecko.com/coins/images/14495/standard/Internet_Computer_logo.png?1696514180" }, { "RENDER", "https://assets.coingecko.com/coins/images/11636/standard/rndr.png?1696511529" }, { "INJ", "https://assets.coingecko.com/coins/images/12882/standard/Secondary_Symbol.png?1696512670" }, { "TRUMP", "https://assets.coingecko.com/coins/images/53746/standard/trump.png?1737171561" }, { "MELANIA", "https://assets.coingecko.com/coins/images/53775/standard/melania-meme.png?1737329885" }, { "ENA", "https://assets.coingecko.com/coins/images/36530/standard/ethena.png?1711701436" }, { "FARTCOIN", "https://assets.coingecko.com/coins/images/50891/standard/fart.jpg?1729503972" }, { "AI16Z", "https://assets.coingecko.com/coins/images/51090/standard/AI16Z.jpg?1730027175" }, { "ANIME", "https://assets.coingecko.com/coins/images/53575/standard/anime.jpg?1736748703" }, { "BERA", "https://assets.coingecko.com/coins/images/25235/standard/BERA.png?1738822008" }, { "VIRTUAL", "https://assets.coingecko.com/coins/images/34057/standard/LOGOMARK.png?1708356054" }, { "PENGU", "https://assets.coingecko.com/coins/images/52622/standard/PUDGY_PENGUINS_PENGU_PFP.png?1733809110" }, { "FET", "https://assets.coingecko.com/coins/images/5681/standard/ASI.png?1719827289" }, { "ONDO", "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656" }, { "AIXBT", "https://assets.coingecko.com/coins/images/51784/standard/3.png?1731981138" }, { "CAKE", "https://assets.coingecko.com/coins/images/12632/standard/pancakeswap-cake-logo_%281%29.png?1696512440" }, { "S", "https://assets.coingecko.com/coins/images/38108/standard/200x200_Sonic_Logo.png?1734679256" }, { "JUP", "https://assets.coingecko.com/coins/images/34188/standard/jup.png?1704266489" }, { "HYPE", "https://assets.coingecko.com/coins/images/50882/standard/hyperliquid.jpg?1729431300" }, { "OM", "https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991" } }; var tokenNames = new Dictionary { { "AAVE", "Aave" }, { "ADA", "Cardano" }, { "APE", "ApeCoin" }, { "ARB", "Arbitrum" }, { "ATOM", "Cosmos" }, { "AVAX", "Avalanche" }, { "BNB", "BNB" }, { "BTC", "Bitcoin" }, { "DOGE", "Dogecoin" }, { "DOT", "Polkadot" }, { "ETH", "Ethereum" }, { "FIL", "Filecoin" }, { "GMX", "GMX" }, { "LINK", "Chainlink" }, { "LTC", "Litecoin" }, { "MATIC", "Polygon" }, { "NEAR", "NEAR Protocol" }, { "OP", "Optimism" }, { "PEPE", "Pepe" }, { "SOL", "Solana" }, { "UNI", "Uniswap" }, { "USDC", "USD Coin" }, { "USDT", "Tether" }, { "WIF", "dogwifhat" }, { "XRP", "XRP" }, { "SHIB", "Shiba Inu" }, { "STX", "Stacks" }, { "ORDI", "ORDI" }, { "APT", "Aptos" }, { "BOME", "BOOK OF MEME" }, { "MEME", "Memecoin" }, { "FLOKI", "Floki" }, { "MEW", "cat in a dogs world" }, { "TAO", "Bittensor" }, { "BONK", "Bonk" }, { "WLD", "Worldcoin" }, { "tBTC", "tBTC" }, { "EIGEN", "Eigenlayer" }, { "SUI", "Sui" }, { "SEI", "Sei" }, { "DAI", "Dai" }, { "TIA", "Celestia" }, { "TRX", "TRON" }, { "TON", "Toncoin" }, { "PENDLE", "Pendle" }, { "wstETH", "Wrapped stETH" }, { "USDe", "Ethena USDe" }, { "SATS", "1000SATS" }, { "POL", "Polygon Ecosystem Token" }, { "XLM", "Stellar" }, { "BCH", "Bitcoin Cash" }, { "ICP", "Internet Computer" }, { "RENDER", "Render" }, { "INJ", "Injective" }, { "TRUMP", "TRUMP" }, { "MELANIA", "MELANIA" }, { "ENA", "Ethena" }, { "FARTCOIN", "FARTCOIN" }, { "AI16Z", "AI16Z" }, { "ANIME", "ANIME" }, { "BERA", "Berachain" }, { "VIRTUAL", "Virtual Protocol" }, { "PENGU", "Pudgy Penguins" }, { "FET", "Artificial Superintelligence Alliance" }, { "ONDO", "Ondo" }, { "AIXBT", "AIXBT" }, { "CAKE", "PancakeSwap" }, { "S", "Sonic" }, { "JUP", "Jupiter" }, { "HYPE", "Hyperliquid" }, { "OM", "MANTRA" } }; foreach (var ticker in availableTicker) { var tickerInfo = new TickerInfos { Ticker = ticker, ImageUrl = tokens.GetValueOrDefault(ticker.ToString(), "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"), // Default to BTC image if not found Name = tokenNames.GetValueOrDefault(ticker.ToString(), ticker.ToString()) }; tickerInfos.Add(tickerInfo); } return tickerInfos; } /// /// Retrieves the latest spotlight overview, using caching to enhance response times. /// /// A object containing spotlight data. [Authorize] [HttpGet("Spotlight")] public async Task> GetSpotlight() { var cacheKey = $"Spotlight_{DateTime.Now.AddDays(-2).ToString("yyyy-MM-dd")}"; var overview = _cacheService.GetValue(cacheKey); if (overview == null) { overview = await _statisticService.GetLastSpotlight(DateTime.Now.AddDays(-2)); _cacheService.SaveValue(cacheKey, overview, TimeSpan.FromMinutes(2)); } return Ok(overview); } /// /// Retrieves candles with indicators values for backtest details display. /// /// The trading exchange. /// The ticker symbol. /// The start date for the candles. /// The end date for the candles. /// The timeframe for the candles. /// The scenario object to calculate indicators values (optional). /// A response containing candles and indicators values. [Authorize] [HttpPost("GetCandlesWithIndicators")] public async Task> GetCandlesWithIndicators( [FromBody] GetCandlesWithIndicatorsRequest request) { try { // Get candles for the specified period var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, request.Ticker, request.StartDate, request.Timeframe, request.EndDate); if (candles == null || candles.Count == 0) { return Ok(new CandlesWithIndicatorsResponse { Candles = new HashSet(), IndicatorsValues = new Dictionary() }); } // Calculate indicators values if scenario is provided Dictionary indicatorsValues = null; if (request.Scenario != null && request.Scenario.Indicators != null && request.Scenario.Indicators.Count > 0) { // Map ScenarioRequest to domain Scenario object var domainScenario = MapScenarioRequestToScenario(request.Scenario); // Convert to ordered List to preserve chronological order for indicators var candlesList = candles.OrderBy(c => c.Date).ToList(); indicatorsValues = TradingBox.CalculateIndicatorsValues(domainScenario, candlesList); } return Ok(new CandlesWithIndicatorsResponse { Candles = candles, IndicatorsValues = indicatorsValues ?? new Dictionary() }); } catch (Exception ex) { return StatusCode(500, $"Error retrieving candles with indicators: {ex.Message}"); } } /// /// Retrieves statistics about currently running bots and their change in the last 24 hours. /// /// A containing bot statistics. [HttpGet("GetStrategiesStatistics")] public async Task> GetStrategiesStatistics() { const string cacheKey = "StrategiesStatistics"; const string previousCountKey = "PreviousBotsCount"; // Check if the statistics are already cached var cachedStats = _cacheService.GetValue(cacheKey); if (cachedStats != null) { return Ok(cachedStats); } // Get active bots var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running)); var currentCount = activeBots.Count(); // Get previous count from cache var previousCount = _cacheService.GetValue(previousCountKey); // Calculate change - if no previous value, set current count as the change (all are new) int change; if (previousCount == 0) { // First time running - assume all bots are new (positive change) change = currentCount; } else { // Calculate actual difference between current and previous count change = currentCount - previousCount; } // Create the response var botsStatistics = new StrategiesStatisticsViewModel { TotalStrategiesRunning = currentCount, ChangeInLast24Hours = change }; // Store current count for future comparison (with 24 hour expiration) _cacheService.SaveValue(previousCountKey, currentCount, TimeSpan.FromHours(24)); // Cache the statistics for 5 minutes _cacheService.SaveValue(cacheKey, botsStatistics, TimeSpan.FromMinutes(5)); return Ok(botsStatistics); } /// /// Retrieves the top 3 performing strategies based on PnL. /// /// A containing the top performing strategies. [HttpGet("GetTopStrategies")] public async Task> GetTopStrategies() { // Get top 3 bots by PnL directly from database (both running and stopped) var bots = await _mediator.Send(new GetTopBotsByPnLCommand(new[] { BotStatus.Running, BotStatus.Stopped }, 3)); // Map to view model var topStrategies = new TopStrategiesViewModel { TopStrategies = bots .Select(bot => new StrategyPerformance { StrategyName = bot.Name, PnL = bot.Pnl, NetPnL = bot.NetPnL, AgentName = bot.User.AgentName, }) .ToList() }; return Ok(topStrategies); } /// /// Retrieves the top 3 performing strategies based on ROI percentage. /// /// A containing the top performing strategies by ROI. [HttpGet("GetTopStrategiesByRoi")] public async Task> GetTopStrategiesByRoi() { // Get top 3 bots by ROI directly from database (both running and stopped) var bots = await _mediator.Send(new GetTopBotsByRoiCommand(new[] { BotStatus.Running, BotStatus.Stopped }, 3)); // Map to view model var topStrategiesByRoi = new TopStrategiesByRoiViewModel { TopStrategiesByRoi = bots .Select(bot => new StrategyRoiPerformance { StrategyName = bot.Name, Roi = bot.Roi, PnL = bot.Pnl, NetPnL = bot.NetPnL, Volume = bot.Volume }) .ToList() }; return Ok(topStrategiesByRoi); } /// /// Retrieves list of the active strategies for a user with detailed information /// /// The agentName to retrieve strategies for /// A list of detailed strategy information [HttpGet("GetUserStrategies")] public async Task>> GetUserStrategies(string agentName) { if (string.IsNullOrEmpty(agentName)) { return BadRequest("Agent name cannot be null or empty."); } // Get all strategies for the specified user var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName)); // Get agent balance history for the last 30 days var startDate = DateTime.UtcNow.AddDays(-30); var endDate = DateTime.UtcNow; var agentBalanceHistory = await _agentService.GetAgentBalances(agentName, startDate, endDate); // Convert to detailed view model with additional information using separate scopes to avoid DbContext concurrency var result = await Task.WhenAll( userStrategies.Select(strategy => ServiceScopeHelpers.WithScopedService( _serviceScopeFactory, async tradingService => await MapStrategyToViewModelAsync(strategy, agentBalanceHistory, tradingService))) ); return Ok(result); } /// /// Retrieves a specific strategy for a user by strategy name /// /// The agent/user name to retrieve the strategy for /// The name of the strategy to retrieve /// Detailed information about the requested strategy [HttpGet("GetUserStrategy")] public async Task> GetUserStrategy(string agentName, string strategyName) { if (string.IsNullOrEmpty(agentName)) { return BadRequest("Agent name cannot be null or empty."); } if (string.IsNullOrEmpty(strategyName)) { return BadRequest("Strategy name cannot be null or empty."); } // Get the specific strategy for the user var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName)); if (strategy == null) { return NotFound($"Strategy '{strategyName}' not found for user '{agentName}'"); } // Get agent balance history for the last 30 days var startDate = DateTime.UtcNow.AddDays(-30); var endDate = DateTime.UtcNow; var agentBalanceHistory = await _agentService.GetAgentBalances(agentName, startDate, endDate); // Map the strategy to a view model using the shared method var result = await MapStrategyToViewModelAsync(strategy, agentBalanceHistory, _tradingService); return Ok(result); } /// /// Maps a trading bot to a strategy view model with detailed statistics /// /// The trading bot to map /// Agent balance history data /// Trading service for fetching positions /// A view model with detailed strategy information private async Task MapStrategyToViewModelAsync(Bot strategy, AgentBalanceHistory agentBalanceHistory, ITradingService tradingService) { // Use caching for position data in UI context (not critical trading operations) var cacheKey = $"positions_{strategy.Identifier}"; var cachedPositions = _cacheService.GetValue>(cacheKey); List positions; if (cachedPositions != null) { positions = cachedPositions; } else { // Fetch positions associated with this bot using the provided trading service positions = (await tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier)).ToList(); // Cache positions for 2 minutes for UI display purposes _cacheService.SaveValue(cacheKey, positions, TimeSpan.FromMinutes(2)); } // Calculate volume statistics using cached positions decimal totalVolume = strategy.Volume; // Use caching for volume calculation to avoid recalculation every time var volumeCacheKey = $"volume_last24h_{strategy.Identifier}"; var cachedVolume = _cacheService.GetValue(volumeCacheKey); decimal volumeLast24h; if (cachedVolume != default(decimal)) { volumeLast24h = cachedVolume; } else { // Calculate volume for the last 24 hours volumeLast24h = TradingBox.GetLast24HVolumeTraded(positions.ToDictionary(p => p.Identifier)); // Cache volume for 2 minutes for UI display purposes _cacheService.SaveValue(volumeCacheKey, volumeLast24h, TimeSpan.FromMinutes(2)); } var positionsForMetrics = positions.Where(p => p.IsValidForMetrics()); // Calculate win/loss statistics from actual positions (including open positions) int wins = positionsForMetrics.Count(p => p.ProfitAndLoss != null && p.ProfitAndLoss.Realized > 0); int losses = positionsForMetrics.Count(p => p.ProfitAndLoss != null && p.ProfitAndLoss.Realized <= 0); int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0; // Convert positions to view models var positionViewModels = positions.Select(MapPositionToViewModel).ToList(); // Convert agent balance history to wallet balances dictionary var walletBalances = agentBalanceHistory?.AgentBalances? .ToDictionary(b => b.Time, b => b.TotalBalanceValue) ?? new Dictionary(); return new UserStrategyDetailsViewModel { Name = strategy.Name, State = strategy.Status, PnL = strategy.Pnl, NetPnL = strategy.NetPnL, ROIPercentage = strategy.Roi, Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null, LastStartTime = strategy.LastStartTime, LastStopTime = strategy.LastStopTime, AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds, TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(), WinRate = winRate, TotalVolumeTraded = totalVolume, VolumeLast24H = volumeLast24h, Wins = wins, Losses = losses, Positions = positionViewModels, Identifier = strategy.Identifier, WalletBalances = walletBalances, Ticker = strategy.Ticker, MasterAgentName = strategy.MasterBotUser?.AgentName, BotTradingBalance = strategy.BotTradingBalance }; } /// /// Retrieves a summary of platform activity across all agents (platform-level data only) /// Uses Orleans grain for efficient caching and real-time updates /// /// A summary of platform activity without individual agent details [HttpGet("GetPlatformSummary")] public async Task> GetPlatformSummary() { try { // Get the platform summary grain var platformSummaryGrain = _grainFactory.GetGrain("platform-summary"); // Get the platform summary from the grain (handles caching and real-time updates) var state = await platformSummaryGrain.GetPlatformSummaryAsync(); // Map the state to the view model var summary = MapPlatformSummaryStateToViewModel(state); return Ok(summary); } catch (Exception ex) { // Log the error and return a fallback response // In production, you might want to return cached data or partial data return StatusCode(500, $"Error retrieving platform summary: {ex.Message}"); } } /// /// Retrieves a paginated list of agent summaries for the agent index page /// /// Page number (defaults to 1) /// Number of items per page (defaults to 10, max 100) /// Field to sort by (TotalPnL, TotalROI, Wins, Losses, AgentName, CreatedAt, UpdatedAt) /// Sort order - "asc" or "desc" (defaults to "desc") /// Optional comma-separated list of agent names to filter by /// A paginated list of agent summaries sorted by the specified field [AllowAnonymous] [HttpGet("GetAgentIndexPaginated")] public async Task> GetAgentIndexPaginated( int page = 1, int pageSize = 10, SortableFields sortBy = SortableFields.NetPnL, string sortOrder = "desc", string? agentNames = null) { // Validate pagination parameters if (page < 1) { return BadRequest("Page must be greater than 0"); } if (pageSize < 1 || pageSize > 100) { return BadRequest("Page size must be between 1 and 100"); } // Validate sort order if (sortOrder != "asc" && sortOrder != "desc") { return BadRequest("Sort order must be 'asc' or 'desc'"); } // Parse agent names filter IEnumerable? agentNamesList = null; if (!string.IsNullOrWhiteSpace(agentNames)) { agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(name => name.Trim()) .Where(name => !string.IsNullOrWhiteSpace(name)) .ToList(); } // Check environment variable for filtering profitable agents only var showOnlyProfitable = _configuration.GetValue("showOnlyProfitable", false); // Get paginated results from database var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList, showOnlyProfitable); var result = await _mediator.Send(command); var agentSummaries = result.Results; var totalCount = result.TotalCount; // Map to view models var agentSummaryViewModels = new List(); foreach (var agentSummary in agentSummaries) { // Calculate win rate int averageWinRate = 0; if (agentSummary.Wins + agentSummary.Losses > 0) { averageWinRate = (agentSummary.Wins * 100) / (agentSummary.Wins + agentSummary.Losses); } // Map to view model var agentSummaryViewModel = new AgentSummaryViewModel { AgentName = agentSummary.AgentName, TotalPnL = agentSummary.TotalPnL, NetPnL = agentSummary.NetPnL, TotalROI = agentSummary.TotalROI, Wins = agentSummary.Wins, Losses = agentSummary.Losses, ActiveStrategiesCount = agentSummary.ActiveStrategiesCount, TotalVolume = agentSummary.TotalVolume, TotalBalance = agentSummary.TotalBalance, TotalFees = agentSummary.TotalFees, BacktestCount = agentSummary.BacktestCount, }; agentSummaryViewModels.Add(agentSummaryViewModel); } var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var response = new PaginatedAgentIndexResponse { AgentSummaries = agentSummaryViewModels, TotalCount = totalCount, CurrentPage = page, PageSize = pageSize, TotalPages = totalPages, HasNextPage = page < totalPages, HasPreviousPage = page > 1, SortBy = sortBy, SortOrder = sortOrder, FilteredAgentNames = agentNames }; return Ok(response); } /// /// Retrieves balance history for a specific agent within a date range /// /// The name of the agent to retrieve balances for /// The start date for the balance history /// Optional end date for the balance history (defaults to current time) /// A list of agent balances within the specified date range [HttpGet("GetAgentBalances")] public async Task> GetAgentBalances( string agentName, DateTime startDate, DateTime? endDate = null) { var balances = await _agentService.GetAgentBalances(agentName, startDate, endDate); return Ok(balances); } /// /// Retrieves an array of online agent names /// /// An array of online agent names [HttpGet("GetOnlineAgent")] public async Task>> GetOnlineAgent() { const string cacheKey = "OnlineAgentNames"; // Check if the online agent names are already cached var cachedAgentNames = _cacheService.GetValue>(cacheKey); if (cachedAgentNames != null) { return Ok(cachedAgentNames); } // Get only online agent names var onlineAgentNames = await _mediator.Send(new GetOnlineAgentNamesCommand()); // Cache the results for 2 minutes _cacheService.SaveValue(cacheKey, onlineAgentNames, TimeSpan.FromMinutes(2)); return Ok(onlineAgentNames); } /// /// Maps a ScenarioRequest to a domain Scenario object. /// /// The scenario request to map. /// A domain Scenario object. private Scenario MapScenarioRequestToScenario(ScenarioRequest scenarioRequest) { var scenario = new Scenario(scenarioRequest.Name, scenarioRequest.LookbackPeriod); foreach (var indicatorRequest in scenarioRequest.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; } /// /// Maps PlatformSummaryGrainState to PlatformSummaryViewModel /// /// The platform summary grain state /// A mapped platform summary view model private PlatformSummaryViewModel MapPlatformSummaryStateToViewModel(PlatformSummaryGrainState state) { return new PlatformSummaryViewModel { // Metadata LastUpdated = state.LastUpdated, LastSnapshot = state.LastSnapshot, HasPendingChanges = state.HasPendingChanges, // Current metrics TotalAgents = state.TotalAgents, TotalActiveStrategies = state.TotalActiveStrategies, TotalPlatformPnL = state.TotalPlatformPnL, TotalPlatformVolume = state.TotalPlatformVolume, OpenInterest = state.OpenInterest, TotalPositionCount = state.TotalLifetimePositionCount, TotalPlatformFees = state.TotalPlatformFees, // Historical snapshots DailySnapshots = state.DailySnapshots .OrderBy(s => s.Date) .Select(s => new DailySnapshot { Date = s.Date, TotalAgents = s.TotalAgents, TotalStrategies = s.TotalStrategies, TotalVolume = s.TotalVolume, TotalPnL = s.TotalPnL, NetPnL = s.NetPnL, TotalOpenInterest = s.TotalOpenInterest, TotalPositionCount = s.TotalLifetimePositionCount }) .ToList(), // Breakdowns VolumeByAsset = state.VolumeByAsset ?? new Dictionary(), PositionCountByAsset = state.PositionCountByAsset ?? new Dictionary(), PositionCountByDirection = state.PositionCountByDirection ?? new Dictionary() }; } /// /// Retrieves a paginated list of strategies (bots) excluding those with Saved status /// /// Page number (1-based, defaults to 1) /// Number of items per page (defaults to 10, max 100) /// Filter by name (partial match, case-insensitive) /// Filter by ticker (partial match, case-insensitive) /// Filter by agent name (partial match, case-insensitive) /// Filter by minimum BotTradingBalance (optional) /// Filter by maximum BotTradingBalance (optional) /// Sort field (defaults to CreateDate) /// Sort direction - Asc or Desc (defaults to Desc) /// A paginated list of strategies excluding Saved status bots [HttpGet("GetStrategiesPaginated")] public async Task>> GetStrategiesPaginated( int pageNumber = 1, int pageSize = 10, BotStatus? status = null, string? name = null, string? ticker = null, string? agentName = null, decimal? minBalance = null, decimal? maxBalance = null, BotSortableColumn sortBy = BotSortableColumn.CreateDate, SortDirection sortDirection = SortDirection.Desc) { // Validate pagination parameters if (pageNumber < 1) { return BadRequest("Page number must be greater than 0"); } if (pageSize < 1 || pageSize > 100) { return BadRequest("Page size must be between 1 and 100"); } try { // Check environment variable for filtering profitable strategies only var showOnlyProfitable = _configuration.GetValue("showOnlyProfitable", false); // Default to Running status if not provided var statusFilter = status ?? BotStatus.Running; // Get paginated bots with status filter var (bots, totalCount) = await _botService.GetBotsPaginatedAsync( pageNumber, pageSize, statusFilter, name, ticker, agentName, minBalance, maxBalance, sortBy, sortDirection, showOnlyProfitable); // No additional filtering needed since we're using the status filter directly var filteredBots = bots.ToList(); var filteredCount = totalCount; // Map to response objects var tradingBotResponses = MapBotsToTradingBotResponse(filteredBots); // Calculate pagination metadata var totalPages = (int)Math.Ceiling((double)filteredCount / pageSize); var response = new PaginatedResponse { Items = tradingBotResponses.ToList(), TotalCount = filteredCount, PageNumber = pageNumber, PageSize = pageSize, TotalPages = totalPages, HasPreviousPage = pageNumber > 1, HasNextPage = pageNumber < totalPages }; return Ok(response); } catch (Exception ex) { return StatusCode(500, $"Error retrieving strategies: {ex.Message}"); } } /// /// Maps a Position domain object to a PositionViewModel /// /// The position domain object to map /// A PositionViewModel with the same properties private static PositionViewModel MapPositionToViewModel(Position position) { return new PositionViewModel { Date = position.Date, AccountId = position.AccountId, OriginDirection = position.OriginDirection, Ticker = position.Ticker, Open = position.Open, StopLoss = position.StopLoss, TakeProfit1 = position.TakeProfit1, ProfitAndLoss = position.ProfitAndLoss, UiFees = position.UiFees, GasFees = position.GasFees, Status = position.Status, SignalIdentifier = position.SignalIdentifier, Identifier = position.Identifier, }; } /// /// Maps a collection of Bot entities to TradingBotResponse objects. /// /// The collection of bots to map /// A list of TradingBotResponse objects private static List MapBotsToTradingBotResponse(IEnumerable bots) { var list = new List(); foreach (var item in bots) { list.Add(new TradingBotResponse { Status = item.Status.ToString(), WinRate = (item.TradeWins + item.TradeLosses) != 0 ? item.TradeWins / (item.TradeWins + item.TradeLosses) : 0, ProfitAndLoss = item.NetPnL, Roi = item.Roi, Identifier = item.Identifier.ToString(), AgentName = item.User.AgentName, CreateDate = item.CreateDate, StartupTime = item.StartupTime, Name = item.Name, Ticker = item.Ticker, TradingType = item.TradingType, MasterAgentName = item.MasterBotUser?.AgentName, BotTradingBalance = item.BotTradingBalance, }); } return list; } }