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);
}
}