712 lines
28 KiB
C#
712 lines
28 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Configuration for strategy combination logic
|
|
/// </summary>
|
|
public class IndicatorComboConfig
|
|
{
|
|
/// <summary>
|
|
/// Minimum percentage of trend strategies that must agree for strong trend (default: 66%)
|
|
/// </summary>
|
|
public decimal TrendStrongAgreementThreshold { get; set; } = 0.66m;
|
|
|
|
/// <summary>
|
|
/// Minimum percentage of signal strategies that must agree (default: 50%)
|
|
/// </summary>
|
|
public decimal SignalAgreementThreshold { get; set; } = 0.5m;
|
|
|
|
/// <summary>
|
|
/// Whether to allow signal strategies to override conflicting trends (default: true)
|
|
/// This is useful for trend reversal signals
|
|
/// </summary>
|
|
public bool AllowSignalTrendOverride { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Minimum confidence level to return a signal (default: Low)
|
|
/// </summary>
|
|
public Confidence MinimumConfidence { get; set; } = Confidence.Low;
|
|
|
|
/// <summary>
|
|
/// Minimum confidence level required from context strategies (default: Medium)
|
|
/// Context strategies evaluate market conditions - higher requirements mean more conservative trading
|
|
/// </summary>
|
|
public Confidence MinimumContextConfidence { get; set; } = Confidence.Medium;
|
|
|
|
/// <summary>
|
|
/// Default exchange to use when signals don't specify one
|
|
/// </summary>
|
|
public TradingExchanges DefaultExchange { get; set; } = TradingExchanges.Binance;
|
|
}
|
|
|
|
public static class TradingBox
|
|
{
|
|
private static readonly IndicatorComboConfig _defaultConfig = new();
|
|
|
|
public static Signal GetSignal(HashSet<Candle> newCandles, HashSet<IIndicator> strategies,
|
|
HashSet<Signal> previousSignal, int? loopbackPeriod = 1)
|
|
{
|
|
return GetSignal(newCandles, strategies, previousSignal, _defaultConfig, loopbackPeriod);
|
|
}
|
|
|
|
public static Signal GetSignal(HashSet<Candle> newCandles, HashSet<IIndicator> strategies,
|
|
HashSet<Signal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1)
|
|
{
|
|
var signalOnCandles = new HashSet<Signal>();
|
|
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<Ticker>(data.Ticker),
|
|
data.Timeframe, config);
|
|
}
|
|
|
|
public static Signal ComputeSignals(HashSet<IIndicator> strategies, HashSet<Signal> signalOnCandles, Ticker ticker,
|
|
Timeframe timeframe)
|
|
{
|
|
return ComputeSignals(strategies, signalOnCandles, ticker, timeframe, _defaultConfig);
|
|
}
|
|
|
|
public static Signal ComputeSignals(HashSet<IIndicator> strategies, HashSet<Signal> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates context strategies based on confidence levels indicating market condition quality
|
|
/// </summary>
|
|
private static bool ValidateContextStrategies(HashSet<IIndicator> allStrategies, List<Signal> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates trend direction using majority voting with neutral handling
|
|
/// </summary>
|
|
private static TradeDirection EvaluateTrendDirection(List<Signal> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates signal direction using weighted majority voting
|
|
/// </summary>
|
|
private static TradeDirection EvaluateSignalDirection(List<Signal> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines final signal direction and confidence based on signal and trend analysis
|
|
/// </summary>
|
|
private static (TradeDirection Direction, Confidence Confidence) DetermineFinalSignal(
|
|
TradeDirection signalDirection,
|
|
TradeDirection trendDirection,
|
|
List<Signal> signalStrategies,
|
|
List<Signal> 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<Candle> candles, List<Position> 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<decimal>();
|
|
var takeProfitsPercentage = new List<decimal>();
|
|
|
|
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<Candle> 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<Tuple<decimal, decimal>>
|
|
{
|
|
new Tuple<decimal, decimal>(position.Open.Quantity, position.Open.Price),
|
|
new Tuple<decimal, decimal>(-quantity, price)
|
|
};
|
|
|
|
|
|
var pnl = new ProfitAndLoss(orders, position.OriginDirection);
|
|
// Apply leverage on the realized pnl
|
|
pnl.Realized = pnl.Realized * leverage;
|
|
return pnl;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the total volume traded across all positions
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <returns>The total volume traded in decimal</returns>
|
|
public static decimal GetTotalVolumeTraded(List<Position> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the volume traded in the last 24 hours
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <returns>The volume traded in the last 24 hours in decimal</returns>
|
|
public static decimal GetLast24HVolumeTraded(List<Position> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the win/loss counts from positions
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <returns>A tuple containing (wins, losses)</returns>
|
|
public static (int Wins, int Losses) GetWinLossCount(List<Position> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the ROI for the last 24 hours
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <returns>The ROI for the last 24 hours as a percentage</returns>
|
|
public static decimal GetLast24HROI(List<Position> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates profit and loss for positions within a specific time range
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
|
|
/// <returns>The PnL for positions in the specified range</returns>
|
|
public static decimal GetPnLInTimeRange(List<Position> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates ROI for positions within a specific time range
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
|
|
/// <returns>The ROI as a percentage for positions in the specified range</returns>
|
|
public static decimal GetROIInTimeRange(List<Position> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the win/loss counts from positions in a specific time range
|
|
/// </summary>
|
|
/// <param name="positions">List of positions to analyze</param>
|
|
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
|
|
/// <returns>A tuple containing (wins, losses)</returns>
|
|
public static (int Wins, int Losses) GetWinLossCountInTimeRange(List<Position> 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);
|
|
}
|
|
} |