Fix realized pnl on backtest save + add tests (not all passing)
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
using Managing.Domain.Candles;
|
||||
using Exilion.TradingAtomics;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Trades;
|
||||
@@ -585,208 +589,99 @@ public static class TradingBox
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the ROI for the last 24 hours
|
||||
/// Calculates the total realized profit and loss (before fees) for all valid positions.
|
||||
/// This represents the gross PnL from trading activities.
|
||||
/// </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(Dictionary<Guid, Position> positions)
|
||||
/// <param name="positions">Dictionary of positions to analyze</param>
|
||||
/// <returns>Returns the total realized PnL before fees as a decimal value.</returns>
|
||||
public static decimal GetTotalRealizedPnL(Dictionary<Guid, Position> positions)
|
||||
{
|
||||
decimal profitLast24h = 0;
|
||||
decimal investmentLast24h = 0;
|
||||
DateTime cutoff = DateTime.UtcNow.AddHours(-24);
|
||||
decimal realizedPnl = 0;
|
||||
|
||||
foreach (var position in positions.Values)
|
||||
{
|
||||
// Only count positions that were opened or closed within the last 24 hours
|
||||
if (position.IsValidForMetrics() &&
|
||||
(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)))
|
||||
if (position.IsValidForMetrics() && position.ProfitAndLoss != null)
|
||||
{
|
||||
profitLast24h += position.ProfitAndLoss != null ? position.ProfitAndLoss.Realized : 0;
|
||||
investmentLast24h += position.Open.Quantity * position.Open.Price;
|
||||
realizedPnl += position.ProfitAndLoss.Realized;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid division by zero
|
||||
if (investmentLast24h == 0)
|
||||
return 0;
|
||||
|
||||
return (profitLast24h / investmentLast24h) * 100;
|
||||
return realizedPnl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates profit and loss for positions within a specific time range
|
||||
/// Calculates the total net profit and loss (after fees) for all valid positions.
|
||||
/// This represents the actual profit after accounting for all trading costs.
|
||||
/// </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)
|
||||
/// <param name="positions">Dictionary of positions to analyze</param>
|
||||
/// <returns>Returns the total net PnL after fees as a decimal value.</returns>
|
||||
public static decimal GetTotalNetPnL(Dictionary<Guid, Position> positions)
|
||||
{
|
||||
// If Total, just return the total PnL
|
||||
if (timeFilter == "Total")
|
||||
decimal netPnl = 0;
|
||||
|
||||
foreach (var position in positions.Values)
|
||||
{
|
||||
return positions
|
||||
.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null)
|
||||
.Sum(p => p.ProfitAndLoss.Realized);
|
||||
if (position.IsValidForMetrics() && position.ProfitAndLoss != null)
|
||||
{
|
||||
netPnl += position.ProfitAndLoss.Net;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.IsValidForMetrics() && 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);
|
||||
return netPnl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates ROI for positions within a specific time range
|
||||
/// Calculates the win rate percentage for all valid positions.
|
||||
/// Win rate is the percentage of positions that are in profit.
|
||||
/// </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)
|
||||
/// <param name="positions">Dictionary of positions to analyze</param>
|
||||
/// <returns>Returns the win rate as a percentage (0-100)</returns>
|
||||
public static int GetWinRate(Dictionary<Guid, Position> positions)
|
||||
{
|
||||
// If no positions, return 0
|
||||
if (!positions.Any())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// Win rate only considers closed positions (Finished status)
|
||||
// Open positions have unrealized P&L and shouldn't count toward win rate
|
||||
int succeededPositions = 0;
|
||||
int totalPositions = 0;
|
||||
|
||||
// Convert time filter to a DateTime
|
||||
DateTime cutoffDate = DateTime.UtcNow;
|
||||
|
||||
if (timeFilter != "Total")
|
||||
foreach (var position in positions.Values)
|
||||
{
|
||||
switch (timeFilter)
|
||||
if (position.Status == PositionStatus.Finished)
|
||||
{
|
||||
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;
|
||||
totalPositions++;
|
||||
if (position.IsInProfit())
|
||||
{
|
||||
succeededPositions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter positions in the time range
|
||||
var filteredPositions = timeFilter == "Total"
|
||||
? positions.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null)
|
||||
: positions.Where(p => p.IsValidForMetrics() && 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)
|
||||
{
|
||||
if (totalPositions == 0)
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (totalProfit / totalInvestment) * 100;
|
||||
return (succeededPositions * 100) / totalPositions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the win/loss counts from positions in a specific time range
|
||||
/// Calculates the total fees paid for all valid positions.
|
||||
/// Includes UI fees (0.1% of position size) and network fees ($0.15 for opening).
|
||||
/// Closing fees are handled by oracle, so no network fee for closing.
|
||||
/// </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)
|
||||
/// <param name="positions">Dictionary of positions to analyze</param>
|
||||
/// <returns>Returns the total fees paid as a decimal value.</returns>
|
||||
public static decimal GetTotalFees(Dictionary<Guid, Position> positions)
|
||||
{
|
||||
// Convert time filter to a DateTime
|
||||
DateTime cutoffDate = DateTime.UtcNow;
|
||||
// Optimized: Avoid LINQ Where overhead, inline the check
|
||||
decimal totalFees = 0;
|
||||
|
||||
if (timeFilter != "Total")
|
||||
foreach (var position in positions.Values)
|
||||
{
|
||||
switch (timeFilter)
|
||||
if (position.IsValidForMetrics())
|
||||
{
|
||||
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;
|
||||
totalFees += CalculatePositionFees(position);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter positions in the time range
|
||||
var filteredPositions = timeFilter == "Total"
|
||||
? positions.Where(p => p.IsValidForMetrics())
|
||||
: positions.Where(p => p.IsValidForMetrics() &&
|
||||
(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);
|
||||
return totalFees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -823,4 +718,227 @@ public static class TradingBox
|
||||
|
||||
return indicatorsValues;
|
||||
}
|
||||
|
||||
public static decimal GetHodlPercentage(Candle candle1, Candle candle2)
|
||||
{
|
||||
return candle2.Close * 100 / candle1.Close - 100;
|
||||
}
|
||||
|
||||
public static decimal GetGrowthFromInitalBalance(decimal balance, decimal finalPnl)
|
||||
{
|
||||
var growth = balance + finalPnl;
|
||||
|
||||
return growth * 100 / balance - 100;
|
||||
}
|
||||
|
||||
public static PerformanceMetrics GetStatistics(Dictionary<DateTime, decimal> pnls)
|
||||
{
|
||||
var priceSeries = new TimePriceSeries(pnls.DistinctBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value));
|
||||
|
||||
return priceSeries.CalculatePerformanceMetrics();
|
||||
}
|
||||
|
||||
public static decimal GetFeeAmount(decimal fee, decimal amount)
|
||||
{
|
||||
return fee * amount;
|
||||
}
|
||||
|
||||
public static decimal GetFeeAmount(decimal fee, decimal amount, TradingExchanges exchange)
|
||||
{
|
||||
if (exchange.Equals(TradingExchanges.Evm))
|
||||
return fee;
|
||||
|
||||
return GetFeeAmount(fee, amount);
|
||||
}
|
||||
|
||||
public static bool IsAGoodTrader(Trader trader)
|
||||
{
|
||||
return trader.Winrate > 30
|
||||
&& trader.TradeCount > 8
|
||||
&& trader.AverageWin > Math.Abs(trader.AverageLoss)
|
||||
&& trader.Pnl > 0;
|
||||
}
|
||||
|
||||
public static bool IsABadTrader(Trader trader)
|
||||
{
|
||||
return trader.Winrate < 30
|
||||
&& trader.TradeCount > 8
|
||||
&& trader.AverageWin * 3 < Math.Abs(trader.AverageLoss)
|
||||
&& trader.Pnl < 0;
|
||||
}
|
||||
|
||||
public static List<Trader> FindBadTrader(this List<Trader> traders)
|
||||
{
|
||||
var filteredTrader = new List<Trader>();
|
||||
foreach (var trader in traders)
|
||||
{
|
||||
if (IsABadTrader(trader))
|
||||
{
|
||||
filteredTrader.Add(trader);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTrader;
|
||||
}
|
||||
|
||||
public static List<Trader> FindGoodTrader(this List<Trader> traders)
|
||||
{
|
||||
var filteredTrader = new List<Trader>();
|
||||
foreach (var trader in traders)
|
||||
{
|
||||
if (IsAGoodTrader(trader))
|
||||
{
|
||||
filteredTrader.Add(trader);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTrader;
|
||||
}
|
||||
|
||||
public static List<Trader> MapToTraders(this List<Account> accounts)
|
||||
{
|
||||
var traders = new List<Trader>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
traders.Add(new Trader
|
||||
{
|
||||
Address = account.Key
|
||||
});
|
||||
}
|
||||
|
||||
return traders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total fees for a position based on GMX V2 fee structure
|
||||
/// </summary>
|
||||
/// <param name="position">The position to calculate fees for</param>
|
||||
/// <returns>The total fees for the position</returns>
|
||||
public static decimal CalculatePositionFees(Position position)
|
||||
{
|
||||
var (uiFees, gasFees) = CalculatePositionFeesBreakdown(position);
|
||||
return uiFees + gasFees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the UI and Gas fees breakdown for a position based on GMX V2 fee structure
|
||||
/// </summary>
|
||||
/// <param name="position">The position to calculate fees for</param>
|
||||
/// <returns>A tuple containing (uiFees, gasFees)</returns>
|
||||
public static (decimal uiFees, decimal gasFees) CalculatePositionFeesBreakdown(Position position)
|
||||
{
|
||||
decimal uiFees = 0;
|
||||
decimal gasFees = 0;
|
||||
|
||||
if (position?.Open?.Price <= 0 || position?.Open?.Quantity <= 0)
|
||||
{
|
||||
return (uiFees, gasFees); // Return 0 if position data is invalid
|
||||
}
|
||||
|
||||
// Calculate position size in USD (leverage is already included in quantity calculation)
|
||||
var positionSizeUsd = (position.Open.Price * position.Open.Quantity) * position.Open.Leverage;
|
||||
|
||||
// UI Fee: 0.1% of position size paid on opening
|
||||
var uiFeeOpen = positionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on opening
|
||||
uiFees += uiFeeOpen;
|
||||
|
||||
// UI Fee: 0.1% of position size paid on closing - only if position was actually closed
|
||||
// Check which closing trade was executed (StopLoss, TakeProfit1, or TakeProfit2)
|
||||
if (position.StopLoss?.Status == TradeStatus.Filled)
|
||||
{
|
||||
var stopLossPositionSizeUsd =
|
||||
(position.StopLoss.Price * position.StopLoss.Quantity) * position.StopLoss.Leverage;
|
||||
var uiFeeClose =
|
||||
stopLossPositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via StopLoss
|
||||
uiFees += uiFeeClose;
|
||||
}
|
||||
else if (position.TakeProfit1?.Status == TradeStatus.Filled)
|
||||
{
|
||||
var takeProfit1PositionSizeUsd = (position.TakeProfit1.Price * position.TakeProfit1.Quantity) *
|
||||
position.TakeProfit1.Leverage;
|
||||
var uiFeeClose =
|
||||
takeProfit1PositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via TakeProfit1
|
||||
uiFees += uiFeeClose;
|
||||
}
|
||||
else if (position.TakeProfit2?.Status == TradeStatus.Filled)
|
||||
{
|
||||
var takeProfit2PositionSizeUsd = (position.TakeProfit2.Price * position.TakeProfit2.Quantity) *
|
||||
position.TakeProfit2.Leverage;
|
||||
var uiFeeClose =
|
||||
takeProfit2PositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via TakeProfit2
|
||||
uiFees += uiFeeClose;
|
||||
}
|
||||
|
||||
// Gas Fee: $0.15 for opening position only
|
||||
// Closing is handled by oracle, so no gas fee for closing
|
||||
gasFees += Constants.GMX.Config.GasFeePerTransaction;
|
||||
|
||||
return (uiFees, gasFees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates UI fees for opening a position
|
||||
/// </summary>
|
||||
/// <param name="positionSizeUsd">The position size in USD</param>
|
||||
/// <returns>The UI fees for opening</returns>
|
||||
public static decimal CalculateOpeningUiFees(decimal positionSizeUsd)
|
||||
{
|
||||
return positionSizeUsd * Constants.GMX.Config.UiFeeRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates UI fees for closing a position
|
||||
/// </summary>
|
||||
/// <param name="positionSizeUsd">The position size in USD</param>
|
||||
/// <returns>The UI fees for closing</returns>
|
||||
public static decimal CalculateClosingUiFees(decimal positionSizeUsd)
|
||||
{
|
||||
return positionSizeUsd * Constants.GMX.Config.UiFeeRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates gas fees for opening a position
|
||||
/// </summary>
|
||||
/// <returns>The gas fees for opening (fixed at $0.15)</returns>
|
||||
public static decimal CalculateOpeningGasFees()
|
||||
{
|
||||
return Constants.GMX.Config.GasFeePerTransaction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total volume for a position based on its status and filled trades
|
||||
/// </summary>
|
||||
/// <param name="position">The position to calculate volume for</param>
|
||||
/// <returns>The total volume for the position</returns>
|
||||
public static decimal GetVolumeForPosition(Position position)
|
||||
{
|
||||
// Always include the opening trade volume
|
||||
var totalVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
|
||||
|
||||
// For closed positions, add volume from filled closing trades
|
||||
if (position.IsValidForMetrics())
|
||||
{
|
||||
// Add Stop Loss volume if filled
|
||||
if (position.StopLoss?.Status == TradeStatus.Filled)
|
||||
{
|
||||
totalVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
|
||||
}
|
||||
|
||||
// Add Take Profit 1 volume if filled
|
||||
if (position.TakeProfit1?.Status == TradeStatus.Filled)
|
||||
{
|
||||
totalVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity *
|
||||
position.TakeProfit1.Leverage;
|
||||
}
|
||||
|
||||
// Add Take Profit 2 volume if filled
|
||||
if (position.TakeProfit2?.Status == TradeStatus.Filled)
|
||||
{
|
||||
totalVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity *
|
||||
position.TakeProfit2.Leverage;
|
||||
}
|
||||
}
|
||||
|
||||
return totalVolume;
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
using Exilion.TradingAtomics;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Shared.Helpers;
|
||||
|
||||
public static class TradingHelpers
|
||||
{
|
||||
public static decimal GetHodlPercentage(Candle candle1, Candle candle2)
|
||||
{
|
||||
return candle2.Close * 100 / candle1.Close - 100;
|
||||
}
|
||||
|
||||
public static decimal GetGrowthFromInitalBalance(decimal balance, decimal finalPnl)
|
||||
{
|
||||
var growth = balance + finalPnl;
|
||||
|
||||
return growth * 100 / balance - 100;
|
||||
}
|
||||
|
||||
public static PerformanceMetrics GetStatistics(Dictionary<DateTime, decimal> pnls)
|
||||
{
|
||||
var priceSeries = new TimePriceSeries(pnls.DistinctBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value));
|
||||
|
||||
return priceSeries.CalculatePerformanceMetrics();
|
||||
}
|
||||
|
||||
public static decimal GetFeeAmount(decimal fee, decimal amount)
|
||||
{
|
||||
return fee * amount;
|
||||
}
|
||||
|
||||
public static decimal GetFeeAmount(decimal fee, decimal amount, TradingExchanges exchange)
|
||||
{
|
||||
if (exchange.Equals(TradingExchanges.Evm))
|
||||
return fee;
|
||||
|
||||
return GetFeeAmount(fee, amount);
|
||||
}
|
||||
|
||||
public static bool IsAGoodTrader(Trader trader)
|
||||
{
|
||||
return trader.Winrate > 30
|
||||
&& trader.TradeCount > 8
|
||||
&& trader.AverageWin > Math.Abs(trader.AverageLoss)
|
||||
&& trader.Pnl > 0;
|
||||
}
|
||||
|
||||
public static bool IsABadTrader(Trader trader)
|
||||
{
|
||||
return trader.Winrate < 30
|
||||
&& trader.TradeCount > 8
|
||||
&& trader.AverageWin * 3 < Math.Abs(trader.AverageLoss)
|
||||
&& trader.Pnl < 0;
|
||||
}
|
||||
|
||||
public static List<Trader> FindBadTrader(this List<Trader> traders)
|
||||
{
|
||||
var filteredTrader = new List<Trader>();
|
||||
foreach (var trader in traders)
|
||||
{
|
||||
if (IsABadTrader(trader))
|
||||
{
|
||||
filteredTrader.Add(trader);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTrader;
|
||||
}
|
||||
|
||||
public static List<Trader> FindGoodTrader(this List<Trader> traders)
|
||||
{
|
||||
var filteredTrader = new List<Trader>();
|
||||
foreach (var trader in traders)
|
||||
{
|
||||
if (IsAGoodTrader(trader))
|
||||
{
|
||||
filteredTrader.Add(trader);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTrader;
|
||||
}
|
||||
|
||||
public static List<Trader> MapToTraders(this List<Account> accounts)
|
||||
{
|
||||
var traders = new List<Trader>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
traders.Add(new Trader
|
||||
{
|
||||
Address = account.Key
|
||||
});
|
||||
}
|
||||
|
||||
return traders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total fees for a position based on GMX V2 fee structure
|
||||
/// </summary>
|
||||
/// <param name="position">The position to calculate fees for</param>
|
||||
/// <returns>The total fees for the position</returns>
|
||||
public static decimal CalculatePositionFees(Position position)
|
||||
{
|
||||
var (uiFees, gasFees) = CalculatePositionFeesBreakdown(position);
|
||||
return uiFees + gasFees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the UI and Gas fees breakdown for a position based on GMX V2 fee structure
|
||||
/// </summary>
|
||||
/// <param name="position">The position to calculate fees for</param>
|
||||
/// <returns>A tuple containing (uiFees, gasFees)</returns>
|
||||
public static (decimal uiFees, decimal gasFees) CalculatePositionFeesBreakdown(Position position)
|
||||
{
|
||||
decimal uiFees = 0;
|
||||
decimal gasFees = 0;
|
||||
|
||||
if (position?.Open?.Price <= 0 || position?.Open?.Quantity <= 0)
|
||||
{
|
||||
return (uiFees, gasFees); // Return 0 if position data is invalid
|
||||
}
|
||||
|
||||
// Calculate position size in USD (leverage is already included in quantity calculation)
|
||||
var positionSizeUsd = (position.Open.Price * position.Open.Quantity) * position.Open.Leverage;
|
||||
|
||||
// UI Fee: 0.1% of position size paid on opening
|
||||
var uiFeeOpen = positionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on opening
|
||||
uiFees += uiFeeOpen;
|
||||
|
||||
// UI Fee: 0.1% of position size paid on closing - only if position was actually closed
|
||||
// Check which closing trade was executed (StopLoss, TakeProfit1, or TakeProfit2)
|
||||
if (position.StopLoss?.Status == TradeStatus.Filled)
|
||||
{
|
||||
var stopLossPositionSizeUsd =
|
||||
(position.StopLoss.Price * position.StopLoss.Quantity) * position.StopLoss.Leverage;
|
||||
var uiFeeClose =
|
||||
stopLossPositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via StopLoss
|
||||
uiFees += uiFeeClose;
|
||||
}
|
||||
else if (position.TakeProfit1?.Status == TradeStatus.Filled)
|
||||
{
|
||||
var takeProfit1PositionSizeUsd = (position.TakeProfit1.Price * position.TakeProfit1.Quantity) *
|
||||
position.TakeProfit1.Leverage;
|
||||
var uiFeeClose =
|
||||
takeProfit1PositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via TakeProfit1
|
||||
uiFees += uiFeeClose;
|
||||
}
|
||||
else if (position.TakeProfit2?.Status == TradeStatus.Filled)
|
||||
{
|
||||
var takeProfit2PositionSizeUsd = (position.TakeProfit2.Price * position.TakeProfit2.Quantity) *
|
||||
position.TakeProfit2.Leverage;
|
||||
var uiFeeClose =
|
||||
takeProfit2PositionSizeUsd * Constants.GMX.Config.UiFeeRate; // Fee paid on closing via TakeProfit2
|
||||
uiFees += uiFeeClose;
|
||||
}
|
||||
|
||||
// Gas Fee: $0.15 for opening position only
|
||||
// Closing is handled by oracle, so no gas fee for closing
|
||||
gasFees += Constants.GMX.Config.GasFeePerTransaction;
|
||||
|
||||
return (uiFees, gasFees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates UI fees for opening a position
|
||||
/// </summary>
|
||||
/// <param name="positionSizeUsd">The position size in USD</param>
|
||||
/// <returns>The UI fees for opening</returns>
|
||||
public static decimal CalculateOpeningUiFees(decimal positionSizeUsd)
|
||||
{
|
||||
return positionSizeUsd * Constants.GMX.Config.UiFeeRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates UI fees for closing a position
|
||||
/// </summary>
|
||||
/// <param name="positionSizeUsd">The position size in USD</param>
|
||||
/// <returns>The UI fees for closing</returns>
|
||||
public static decimal CalculateClosingUiFees(decimal positionSizeUsd)
|
||||
{
|
||||
return positionSizeUsd * Constants.GMX.Config.UiFeeRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates gas fees for opening a position
|
||||
/// </summary>
|
||||
/// <returns>The gas fees for opening (fixed at $0.15)</returns>
|
||||
public static decimal CalculateOpeningGasFees()
|
||||
{
|
||||
return Constants.GMX.Config.GasFeePerTransaction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total volume for a position based on its status and filled trades
|
||||
/// </summary>
|
||||
/// <param name="position">The position to calculate volume for</param>
|
||||
/// <returns>The total volume for the position</returns>
|
||||
public static decimal GetVolumeForPosition(Position position)
|
||||
{
|
||||
// Always include the opening trade volume
|
||||
var totalVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
|
||||
|
||||
// For closed positions, add volume from filled closing trades
|
||||
if (position.IsValidForMetrics())
|
||||
{
|
||||
// Add Stop Loss volume if filled
|
||||
if (position.StopLoss?.Status == TradeStatus.Filled)
|
||||
{
|
||||
totalVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
|
||||
}
|
||||
|
||||
// Add Take Profit 1 volume if filled
|
||||
if (position.TakeProfit1?.Status == TradeStatus.Filled)
|
||||
{
|
||||
totalVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity *
|
||||
position.TakeProfit1.Leverage;
|
||||
}
|
||||
|
||||
// Add Take Profit 2 volume if filled
|
||||
if (position.TakeProfit2?.Status == TradeStatus.Filled)
|
||||
{
|
||||
totalVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity *
|
||||
position.TakeProfit2.Leverage;
|
||||
}
|
||||
}
|
||||
|
||||
return totalVolume;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user