Files
managing-apps/src/Managing.Domain/Shared/Helpers/TradingBox.cs
2025-04-24 23:48:28 +07:00

495 lines
19 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;
public static class TradingBox
{
public static Signal GetSignal(HashSet<Candle> newCandles, HashSet<IStrategy> strategies,
HashSet<Signal> previousSignal, 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) 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<Ticker>(data.Ticker),
data.Timeframe);
}
public static Signal ComputeSignals(HashSet<IStrategy> strategies, HashSet<Signal> 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<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.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<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);
}
}