495 lines
19 KiB
C#
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);
|
|
}
|
|
} |