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; /// /// Configuration for strategy combination logic /// public class IndicatorComboConfig { /// /// Minimum percentage of trend strategies that must agree for strong trend (default: 66%) /// public decimal TrendStrongAgreementThreshold { get; set; } = 0.66m; /// /// Minimum percentage of signal strategies that must agree (default: 50%) /// public decimal SignalAgreementThreshold { get; set; } = 0.5m; /// /// Whether to allow signal strategies to override conflicting trends (default: true) /// This is useful for trend reversal signals /// public bool AllowSignalTrendOverride { get; set; } = true; /// /// Minimum confidence level to return a signal (default: Low) /// public Confidence MinimumConfidence { get; set; } = Confidence.Low; /// /// Minimum confidence level required from context strategies (default: Medium) /// Context strategies evaluate market conditions - higher requirements mean more conservative trading /// public Confidence MinimumContextConfidence { get; set; } = Confidence.Medium; /// /// Default exchange to use when signals don't specify one /// public TradingExchanges DefaultExchange { get; set; } = TradingExchanges.Binance; } public static class TradingBox { private static readonly IndicatorComboConfig _defaultConfig = new(); public static Signal GetSignal(HashSet newCandles, HashSet strategies, HashSet previousSignal, int? loopbackPeriod = 1) { return GetSignal(newCandles, strategies, previousSignal, _defaultConfig, loopbackPeriod); } public static Signal GetSignal(HashSet newCandles, HashSet strategies, HashSet previousSignal, IndicatorComboConfig config, 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) { // For trend and context strategies, lack of signal might be meaningful // Signal strategies are expected to be sparse, so we continue if (strategy.SignalType == SignalType.Signal) { continue; } // For trend strategies, no signal might mean neutral trend // For context strategies, no signal might mean no restrictions // We'll let the ComputeSignals method handle these cases 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); } } } } // Remove the restrictive requirement that ALL strategies must produce signals // Instead, let ComputeSignals handle the logic based on what we have if (!signalOnCandles.Any()) { return null; // No signals from any strategy } var data = newCandles.First(); return ComputeSignals(strategies, signalOnCandles, MiscExtensions.ParseEnum(data.Ticker), data.Timeframe, config); } public static Signal ComputeSignals(HashSet strategies, HashSet signalOnCandles, Ticker ticker, Timeframe timeframe) { return ComputeSignals(strategies, signalOnCandles, ticker, timeframe, _defaultConfig); } public static Signal ComputeSignals(HashSet strategies, HashSet signalOnCandles, Ticker ticker, Timeframe timeframe, IndicatorComboConfig config) { if (strategies.Count == 1) { // Only one strategy, return the single signal return signalOnCandles.Single(); } // Check if all strategies produced signals - this is required for composite signals if (signalOnCandles.Count != strategies.Count) { // Not all strategies produced signals - composite signal requires all strategies to contribute return null; } // Group signals by type for analysis var signalStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList(); var trendStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList(); var contextStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList(); // Context validation - evaluates market conditions based on confidence levels if (!ValidateContextStrategies(strategies, contextStrategies, config)) { return null; // Context strategies are blocking the trade } // Trend analysis - evaluate overall market direction var trendDirection = EvaluateTrendDirection(trendStrategies, config); // Signal analysis - evaluate entry signals var signalDirection = EvaluateSignalDirection(signalStrategies, config); // Determine final direction and confidence var (finalDirection, confidence) = DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config); if (finalDirection == TradeDirection.None || confidence < config.MinimumConfidence) { return null; // No valid signal or below minimum confidence } // Create composite signal var lastSignal = signalStrategies.LastOrDefault() ?? trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault(); return new Signal( ticker, finalDirection, confidence, lastSignal?.Candle, lastSignal?.Date ?? DateTime.UtcNow, lastSignal?.Exchange ?? config.DefaultExchange, IndicatorType.Composite, SignalType.Signal); } /// /// Validates context strategies based on confidence levels indicating market condition quality /// private static bool ValidateContextStrategies(HashSet allStrategies, List contextSignals, IndicatorComboConfig config) { var contextStrategiesCount = allStrategies.Count(s => s.SignalType == SignalType.Context); if (contextStrategiesCount == 0) { return true; // No context strategies, no restrictions } // Check if we have signals from all context strategies if (contextSignals.Count != contextStrategiesCount) { return false; // Missing context information } // All context strategies must meet minimum confidence requirements // This ensures market conditions are suitable for trading return contextSignals.All(s => s.Confidence >= config.MinimumContextConfidence); } /// /// Evaluates trend direction using majority voting with neutral handling /// private static TradeDirection EvaluateTrendDirection(List trendSignals, IndicatorComboConfig config) { if (!trendSignals.Any()) { return TradeDirection.None; // No trend information available } var longCount = trendSignals.Count(s => s.Direction == TradeDirection.Long); var shortCount = trendSignals.Count(s => s.Direction == TradeDirection.Short); var neutralCount = trendSignals.Count(s => s.Direction == TradeDirection.None); // Strong trend agreement using configurable threshold var totalTrend = trendSignals.Count; if (longCount > totalTrend * config.TrendStrongAgreementThreshold) return TradeDirection.Long; if (shortCount > totalTrend * config.TrendStrongAgreementThreshold) return TradeDirection.Short; // Moderate trend agreement (> 50% but <= strong threshold) if (longCount > shortCount && longCount > neutralCount) return TradeDirection.Long; if (shortCount > longCount && shortCount > neutralCount) return TradeDirection.Short; // No clear trend or too many neutrals return TradeDirection.None; } /// /// Evaluates signal direction using weighted majority voting /// private static TradeDirection EvaluateSignalDirection(List signalStrategies, IndicatorComboConfig config) { if (!signalStrategies.Any()) { return TradeDirection.None; // No signal strategies } // For signal strategies, we need stronger agreement since they're rare and should be precise var longCount = signalStrategies.Count(s => s.Direction == TradeDirection.Long); var shortCount = signalStrategies.Count(s => s.Direction == TradeDirection.Short); // Use configurable agreement threshold for signals var totalSignals = signalStrategies.Count; if (longCount > totalSignals * config.SignalAgreementThreshold) return TradeDirection.Long; if (shortCount > totalSignals * config.SignalAgreementThreshold) return TradeDirection.Short; return TradeDirection.None; } /// /// Determines final signal direction and confidence based on signal and trend analysis /// private static (TradeDirection Direction, Confidence Confidence) DetermineFinalSignal( TradeDirection signalDirection, TradeDirection trendDirection, List signalStrategies, List trendStrategies, IndicatorComboConfig config) { // Priority 1: If we have signal strategies, they take precedence if (signalDirection != TradeDirection.None) { // Signal strategies have fired - check if trend supports or conflicts if (trendDirection == signalDirection) { // Perfect alignment - signal and trend agree return (signalDirection, Confidence.High); } else if (trendDirection == TradeDirection.None) { // No trend information or neutral trend - medium confidence return (signalDirection, Confidence.Medium); } else if (config.AllowSignalTrendOverride) { // Trend conflicts with signal but we allow override // This could be a trend reversal signal return (signalDirection, Confidence.Low); } else { // Trend conflicts and we don't allow override return (TradeDirection.None, Confidence.None); } } // Priority 2: Only trend strategies available if (trendDirection != TradeDirection.None) { // Calculate confidence based on trend strength var totalTrend = trendStrategies.Count; var majorityDirection = trendDirection == TradeDirection.Long ? trendStrategies.Count(s => s.Direction == TradeDirection.Long) : trendStrategies.Count(s => s.Direction == TradeDirection.Short); var agreementPercentage = (decimal)majorityDirection / totalTrend; if (agreementPercentage >= 0.8m) return (trendDirection, Confidence.High); else if (agreementPercentage >= config.TrendStrongAgreementThreshold) return (trendDirection, Confidence.Medium); else return (trendDirection, Confidence.Low); } // No valid signal found return (TradeDirection.None, Confidence.None); } 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.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); } }