using Managing.Core; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Strategies; using Managing.Domain.Trades; using static Managing.Common.Enums; namespace Managing.Domain.Shared.Helpers; public static class TradingBox { public static Signal GetSignal(HashSet newCandles, HashSet strategies, HashSet previousSignal, int? loopbackPeriod = 1) { var signalOnCandles = new HashSet(); var limitedCandles = newCandles.ToList().TakeLast(600).ToList(); foreach (var strategy in strategies) { strategy.UpdateCandles(limitedCandles.ToHashSet()); var signals = strategy.Run(); if (signals == null || signals.Count == 0) continue; // Ensure limitedCandles is ordered chronologically var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList(); var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1; var candleLoopback = orderedCandles.TakeLast(loopback).ToList(); if (!candleLoopback.Any()) { // Handle empty case (e.g., log warning, skip processing) continue; } var loopbackStartDate = candleLoopback.First().Date; foreach (var signal in signals.Where(s => s.Date >= loopbackStartDate)) { var hasExistingSignal = previousSignal.Any(s => s.Identifier == signal.Identifier); if (!hasExistingSignal) { bool shouldAdd = previousSignal.Count == 0 || previousSignal.Last().Date < signal.Date; if (shouldAdd) { signalOnCandles.Add(signal); } } } } if (signalOnCandles.Count != strategies.Count) return null; var data = newCandles.First(); return ComputeSignals(strategies, signalOnCandles, MiscExtensions.ParseEnum(data.Ticker), data.Timeframe); } public static Signal ComputeSignals(HashSet strategies, HashSet signalOnCandles, Ticker ticker, Timeframe timeframe) { Signal signal = null; if (strategies.Count > 1) { var trendSignal = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList(); var signals = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList(); var contextStrategiesCount = strategies.Count(s => s.SignalType == SignalType.Context); var validContext = true; if (contextStrategiesCount > 0 && signalOnCandles.Count(s => s.SignalType == SignalType.Context) != contextStrategiesCount) { validContext = false; } if (signals.All(s => s.Direction == TradeDirection.Long) && trendSignal.All(t => t.Direction == TradeDirection.Long) && validContext) { signal = new Signal( ticker, TradeDirection.Long, Confidence.High, signals.Last().Candle, signals.Last().Date, signals.Last().Exchange, StrategyType.Composite, SignalType.Signal); } else if (signals.All(s => s.Direction == TradeDirection.Short) && trendSignal.All(t => t.Direction == TradeDirection.Short) && validContext) { signal = new Signal( ticker, TradeDirection.Short, Confidence.High, signals.Last().Candle, signals.Last().Date, signals.Last().Exchange, StrategyType.Composite, SignalType.Signal); } } else { // Only one strategy, we just add the single signal to the bot signal = signalOnCandles.Single(); } return signal; } public static MoneyManagement GetBestMoneyManagement(List candles, List positions, MoneyManagement originMoneyManagement) { // Foreach positions, identitify the price when the position is open // Then, foreach candles, get the maximum price before the next position // Then, identify the lowest price before the maximum price // Base on that, return the best StopLoss and TakeProfit to use and build a var moneyManagement = new MoneyManagement(); var stoplossPercentage = new List(); var takeProfitsPercentage = new List(); if (positions.Count == 0) return null; for (var i = 0; i < positions.Count; i++) { var position = positions[i]; var nextPosition = i + 1 < positions.Count ? positions[i + 1] : null; var (stopLoss, takeProfit) = GetBestSltpForPosition(candles, position, nextPosition); stoplossPercentage.Add(stopLoss); takeProfitsPercentage.Add(takeProfit); } moneyManagement.StopLoss = stoplossPercentage.Average(); moneyManagement.TakeProfit = takeProfitsPercentage.Average(); moneyManagement.BalanceAtRisk = originMoneyManagement.BalanceAtRisk * 100; moneyManagement.Timeframe = originMoneyManagement.Timeframe; moneyManagement.Leverage = originMoneyManagement.Leverage; moneyManagement.Name = "Optimized"; return moneyManagement; } public static (decimal Stoploss, decimal TakeProfit) GetBestSltpForPosition(List candles, Position position, Position nextPosition) { var stopLoss = 0M; var takeProfit = 0M; var candlesBeforeNextPosition = candles.Where(c => c.Date >= position.Date && c.Date <= (nextPosition == null ? candles.Last().Date : nextPosition.Date)) .ToList(); if (position.OriginDirection == TradeDirection.Long) { var maxPrice = candlesBeforeNextPosition.Max(c => c.High); var minPrice = candlesBeforeNextPosition.TakeWhile(c => c.High <= maxPrice).Min(c => c.Low); stopLoss = GetPercentageFromEntry(position.Open.Price, minPrice); takeProfit = GetPercentageFromEntry(position.Open.Price, maxPrice); } else if (position.OriginDirection == TradeDirection.Short) { var minPrice = candlesBeforeNextPosition.Min(c => c.Low); var maxPrice = candlesBeforeNextPosition.TakeWhile(c => c.Low >= minPrice).Max(c => c.High); stopLoss = GetPercentageFromEntry(position.Open.Price, maxPrice); takeProfit = GetPercentageFromEntry(position.Open.Price, minPrice); } return (stopLoss, takeProfit); } private static decimal GetPercentageFromEntry(decimal entry, decimal price) { return Math.Abs(100 - ((100 * price) / entry)); } public static ProfitAndLoss GetProfitAndLoss(Position position, decimal quantity, decimal price, decimal leverage) { var orders = new List> { new Tuple(position.Open.Quantity, position.Open.Price), new Tuple(-quantity, price) }; var pnl = new ProfitAndLoss(orders, position.OriginDirection); // Apply leverage on the realized pnl pnl.Realized = pnl.Realized * leverage; return pnl; } /// /// Calculates the total volume traded across all positions /// /// List of positions to analyze /// The total volume traded in decimal public static decimal GetTotalVolumeTraded(List positions) { decimal totalVolume = 0; foreach (var position in positions) { // Add entry volume totalVolume += position.Open.Quantity * position.Open.Price; // Add exit volumes from stop loss or take profits if they were executed if (position.StopLoss.Status == TradeStatus.Filled) { totalVolume += position.StopLoss.Quantity * position.StopLoss.Price; } if (position.TakeProfit1.Status == TradeStatus.Filled) { totalVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price; } if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled) { totalVolume += position.TakeProfit2.Quantity * position.TakeProfit2.Price; } } return totalVolume; } /// /// Calculates the volume traded in the last 24 hours /// /// List of positions to analyze /// The volume traded in the last 24 hours in decimal public static decimal GetLast24HVolumeTraded(List positions) { decimal last24hVolume = 0; DateTime cutoff = DateTime.UtcNow.AddHours(-24); foreach (var position in positions) { // Check if any part of this position was traded in the last 24 hours // Add entry volume if it was within the last 24 hours if (position.Open.Date >= cutoff) { last24hVolume += position.Open.Quantity * position.Open.Price; } // Add exit volumes if they were executed within the last 24 hours if (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) { last24hVolume += position.StopLoss.Quantity * position.StopLoss.Price; } if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) { last24hVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price; } if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date >= cutoff) { last24hVolume += position.TakeProfit2.Quantity * position.TakeProfit2.Price; } } return last24hVolume; } /// /// Gets the win/loss counts from positions /// /// List of positions to analyze /// A tuple containing (wins, losses) public static (int Wins, int Losses) GetWinLossCount(List positions) { int wins = 0; int losses = 0; foreach (var position in positions) { // Only count finished positions if (position.IsFinished()) { if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) { wins++; } else { losses++; } } } return (wins, losses); } /// /// Calculates the ROI for the last 24 hours /// /// List of positions to analyze /// The ROI for the last 24 hours as a percentage public static decimal GetLast24HROI(List positions) { decimal profitLast24h = 0; decimal investmentLast24h = 0; DateTime cutoff = DateTime.UtcNow.AddHours(-24); foreach (var position in positions) { // Only count positions that were opened or closed within the last 24 hours if (position.IsFinished() && (position.Open.Date >= cutoff || (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) || (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) || (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date >= cutoff))) { profitLast24h += position.ProfitAndLoss != null ? position.ProfitAndLoss.Realized : 0; investmentLast24h += position.Open.Quantity * position.Open.Price; } } // Avoid division by zero if (investmentLast24h == 0) return 0; return (profitLast24h / investmentLast24h) * 100; } /// /// Calculates profit and loss for positions within a specific time range /// /// List of positions to analyze /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) /// The PnL for positions in the specified range public static decimal GetPnLInTimeRange(List positions, string timeFilter) { // If Total, just return the total PnL if (timeFilter == "Total") { return positions .Where(p => p.IsFinished() && p.ProfitAndLoss != null) .Sum(p => p.ProfitAndLoss.Realized); } // Convert time filter to a DateTime DateTime cutoffDate = DateTime.UtcNow; switch (timeFilter) { case "24H": cutoffDate = DateTime.UtcNow.AddHours(-24); break; case "3D": cutoffDate = DateTime.UtcNow.AddDays(-3); break; case "1W": cutoffDate = DateTime.UtcNow.AddDays(-7); break; case "1M": cutoffDate = DateTime.UtcNow.AddMonths(-1); break; case "1Y": cutoffDate = DateTime.UtcNow.AddYears(-1); break; } // Include positions that were closed within the time range return positions .Where(p => p.IsFinished() && p.ProfitAndLoss != null && (p.Date >= cutoffDate || (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))) .Sum(p => p.ProfitAndLoss.Realized); } /// /// Calculates ROI for positions within a specific time range /// /// List of positions to analyze /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) /// The ROI as a percentage for positions in the specified range public static decimal GetROIInTimeRange(List positions, string timeFilter) { // If no positions, return 0 if (!positions.Any()) { return 0; } // Convert time filter to a DateTime DateTime cutoffDate = DateTime.UtcNow; if (timeFilter != "Total") { switch (timeFilter) { case "24H": cutoffDate = DateTime.UtcNow.AddHours(-24); break; case "3D": cutoffDate = DateTime.UtcNow.AddDays(-3); break; case "1W": cutoffDate = DateTime.UtcNow.AddDays(-7); break; case "1M": cutoffDate = DateTime.UtcNow.AddMonths(-1); break; case "1Y": cutoffDate = DateTime.UtcNow.AddYears(-1); break; } } // Filter positions in the time range var filteredPositions = timeFilter == "Total" ? positions.Where(p => p.IsFinished() && p.ProfitAndLoss != null) : positions.Where(p => p.IsFinished() && p.ProfitAndLoss != null && (p.Date >= cutoffDate || (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))); // Calculate investment and profit decimal totalInvestment = filteredPositions.Sum(p => p.Open.Quantity * p.Open.Price); decimal totalProfit = filteredPositions.Sum(p => p.ProfitAndLoss.Realized); // Calculate ROI if (totalInvestment == 0) { return 0; } return (totalProfit / totalInvestment) * 100; } /// /// Gets the win/loss counts from positions in a specific time range /// /// List of positions to analyze /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) /// A tuple containing (wins, losses) public static (int Wins, int Losses) GetWinLossCountInTimeRange(List positions, string timeFilter) { // Convert time filter to a DateTime DateTime cutoffDate = DateTime.UtcNow; if (timeFilter != "Total") { switch (timeFilter) { case "24H": cutoffDate = DateTime.UtcNow.AddHours(-24); break; case "3D": cutoffDate = DateTime.UtcNow.AddDays(-3); break; case "1W": cutoffDate = DateTime.UtcNow.AddDays(-7); break; case "1M": cutoffDate = DateTime.UtcNow.AddMonths(-1); break; case "1Y": cutoffDate = DateTime.UtcNow.AddYears(-1); break; } } // Filter positions in the time range var filteredPositions = timeFilter == "Total" ? positions.Where(p => p.IsFinished()) : positions.Where(p => p.IsFinished() && (p.Date >= cutoffDate || (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))); int wins = 0; int losses = 0; foreach (var position in filteredPositions) { if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) { wins++; } else { losses++; } } return (wins, losses); } }