From b60295fcb226f6db412e425832d504122b0430b1 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 14 Nov 2025 19:42:52 +0700 Subject: [PATCH] Add test for dailysnapshot --- .../TradingBoxAgentSummaryMetricsTests.cs | 698 +++++++++++++++++- .../Bots/Grains/LiveTradingBotGrain.cs | 27 +- .../Shared/Helpers/TradingBox.cs | 4 +- 3 files changed, 702 insertions(+), 27 deletions(-) diff --git a/src/Managing.Application.Tests/TradingBoxAgentSummaryMetricsTests.cs b/src/Managing.Application.Tests/TradingBoxAgentSummaryMetricsTests.cs index 6702658a..0b0bd3d5 100644 --- a/src/Managing.Application.Tests/TradingBoxAgentSummaryMetricsTests.cs +++ b/src/Managing.Application.Tests/TradingBoxAgentSummaryMetricsTests.cs @@ -51,10 +51,386 @@ public class TradingBoxAgentSummaryMetricsTests Assert.Equal(1, metrics.Losses); } + [Fact] + public void CalculateAgentSummaryMetrics_WithEmptyPositions_ReturnsZeroMetrics() + { + var positions = new List(); + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(0m, metrics.TotalPnL); + Assert.Equal(0m, metrics.NetPnL); + Assert.Equal(0m, metrics.TotalROI); + Assert.Equal(0m, metrics.TotalVolume); + Assert.Equal(0, metrics.Wins); + Assert.Equal(0, metrics.Losses); + Assert.Equal(0m, metrics.TotalFees); + Assert.Equal(0m, metrics.Collateral); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithNullPositions_ReturnsZeroMetrics() + { + var metrics = TradingBox.CalculateAgentSummaryMetrics(null); + + Assert.Equal(0m, metrics.TotalPnL); + Assert.Equal(0m, metrics.NetPnL); + Assert.Equal(0m, metrics.TotalROI); + Assert.Equal(0m, metrics.TotalVolume); + Assert.Equal(0, metrics.Wins); + Assert.Equal(0, metrics.Losses); + Assert.Equal(0m, metrics.TotalFees); + Assert.Equal(0m, metrics.Collateral); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithInvalidPositions_ExcludesThemFromCalculations() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 2m, + direction: TradeDirection.Long, + realizedPnL: 10m, + netPnL: 8m, + uiFees: 1m, + gasFees: 1m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Filled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Filled), + CreateInvalidPosition() // This should be excluded + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + // Should only include the valid position + Assert.Equal(10m, metrics.TotalPnL); + Assert.Equal(2m, metrics.TotalFees); + Assert.Equal(8m, metrics.NetPnL); + Assert.Equal(8m / 200m * 100m, metrics.TotalROI); // 8 / 200 * 100 + Assert.Equal(610m, metrics.TotalVolume); // 100*2*1 + 95*2*1 + 110*2*1 = 200 + 190 + 220 + Assert.Equal(200m, metrics.Collateral); // 100 * 2 + Assert.Equal(1, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithCanceledPositions_ExcludesThemFromCalculations() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 2m, + direction: TradeDirection.Long, + realizedPnL: 10m, + netPnL: 8m, + uiFees: 1m, + gasFees: 1m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Filled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Filled), + CreateCanceledPosition() + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + // Should only include the finished position + Assert.Equal(10m, metrics.TotalPnL); + Assert.Equal(2m, metrics.TotalFees); + Assert.Equal(8m, metrics.NetPnL); + Assert.Equal(8m / 200m * 100m, metrics.TotalROI); + Assert.Equal(610m, metrics.TotalVolume); // 100*2*1 + 95*2*1 + 110*2*1 = 200 + 190 + 220 + Assert.Equal(200m, metrics.Collateral); + Assert.Equal(1, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithRejectedPositions_ExcludesThemFromCalculations() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 2m, + direction: TradeDirection.Long, + realizedPnL: 10m, + netPnL: 8m, + uiFees: 1m, + gasFees: 1m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Filled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Filled), + CreateRejectedPosition() + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + // Should only include the finished position + Assert.Equal(10m, metrics.TotalPnL); + Assert.Equal(2m, metrics.TotalFees); + Assert.Equal(8m, metrics.NetPnL); + Assert.Equal(8m / 200m * 100m, metrics.TotalROI); + Assert.Equal(610m, metrics.TotalVolume); // 100*2*1 + 95*2*1 + 110*2*1 = 200 + 190 + 220 + Assert.Equal(200m, metrics.Collateral); + Assert.Equal(1, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithZeroPnL_HandlesCorrectly() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 2m, + direction: TradeDirection.Long, + realizedPnL: 0m, + netPnL: -1m, // Loss due to fees + uiFees: 0.5m, + gasFees: 0.5m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Cancelled) + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(0m, metrics.TotalPnL); + Assert.Equal(1m, metrics.TotalFees); + Assert.Equal(-1m, metrics.NetPnL); + Assert.Equal(-1m / 200m * 100m, metrics.TotalROI); // -1 / 200 * 100 + Assert.Equal(200m, metrics.TotalVolume); // Only opening volume + Assert.Equal(200m, metrics.Collateral); + Assert.Equal(0, metrics.Wins); + Assert.Equal(1, metrics.Losses); // Net PnL <= 0 + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithZeroCollateral_HandlesDivisionByZero() + { + var positions = new List + { + CreatePositionWithZeroCollateral() + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(10m, metrics.TotalPnL); + Assert.Equal(1m, metrics.TotalFees); + Assert.Equal(9m, metrics.NetPnL); + Assert.Equal(0m, metrics.TotalROI); // ROI = 0 when collateral = 0 to avoid division by zero + Assert.Equal(10m, metrics.TotalVolume); // 0 * 1 * 1 (open) + 10 * 1 * 1 (close) + Assert.Equal(0m, metrics.Collateral); // Zero collateral + Assert.Equal(1, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithAllWinningPositions_CalculatesCorrectWinRate() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 1m, + direction: TradeDirection.Long, + realizedPnL: 5m, + netPnL: 4m, + uiFees: 0.5m, + gasFees: 0.5m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Filled), + CreatePosition( + openPrice: 200m, + quantity: 1m, + direction: TradeDirection.Short, + realizedPnL: 8m, + netPnL: 7m, + uiFees: 0.5m, + gasFees: 0.5m, + stopLossPrice: 210m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 190m, + takeProfitStatus: TradeStatus.Filled) + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(13m, metrics.TotalPnL); + Assert.Equal(2m, metrics.TotalFees); + Assert.Equal(11m, metrics.NetPnL); + Assert.Equal(11m / 300m * 100m, metrics.TotalROI); // 11 / 300 * 100 + Assert.Equal(600m, metrics.TotalVolume); // 100*1*1 + 110*1*1 + 200*1*1 + 190*1*1 = 210 + 390 = 600 + Assert.Equal(300m, metrics.Collateral); // 100*1 + 200*1 + Assert.Equal(2, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithAllLosingPositions_CalculatesCorrectWinRate() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 1m, + direction: TradeDirection.Long, + realizedPnL: -5m, + netPnL: -6m, + uiFees: 0.5m, + gasFees: 0.5m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Filled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Cancelled), + CreatePosition( + openPrice: 200m, + quantity: 1m, + direction: TradeDirection.Short, + realizedPnL: -3m, + netPnL: -4m, + uiFees: 0.5m, + gasFees: 0.5m, + stopLossPrice: 210m, + stopLossStatus: TradeStatus.Filled, + takeProfitPrice: 190m, + takeProfitStatus: TradeStatus.Cancelled) + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(-8m, metrics.TotalPnL); + Assert.Equal(2m, metrics.TotalFees); + Assert.Equal(-10m, metrics.NetPnL); + Assert.Equal(-10m / 300m * 100m, metrics.TotalROI); // -10 / 300 * 100 + Assert.Equal(605m, metrics.TotalVolume); // 100*1 + 95*1 + 200*1 + 210*1 = 195 + 410 = 605 + Assert.Equal(300m, metrics.Collateral); // 100*1 + 200*1 + Assert.Equal(0, metrics.Wins); + Assert.Equal(2, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithHighLeverage_IncludesLeverageInVolume() + { + var positions = new List + { + CreatePositionWithLeverage( + openPrice: 100m, + quantity: 1m, + leverage: 5m, + direction: TradeDirection.Long, + realizedPnL: 10m, + netPnL: 9m, + uiFees: 1m, + gasFees: 1m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Filled) + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(10m, metrics.TotalPnL); + Assert.Equal(2m, metrics.TotalFees); + Assert.Equal(8m, metrics.NetPnL); + Assert.Equal(8m, metrics.TotalROI); // 8 / 100 * 100 + Assert.Equal(1050m, metrics.TotalVolume); // (100*1*5) + (110*1*5) = 500 + 550 + Assert.Equal(100m, metrics.Collateral); // 100 * 1 (quantity, not leveraged) + Assert.Equal(1, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithZeroFees_CalculatesCorrectly() + { + var positions = new List + { + CreatePosition( + openPrice: 100m, + quantity: 1m, + direction: TradeDirection.Long, + realizedPnL: 10m, + netPnL: 10m, // No fees deducted + uiFees: 0m, + gasFees: 0m, + stopLossPrice: 95m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 110m, + takeProfitStatus: TradeStatus.Filled) + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(10m, metrics.TotalPnL); + Assert.Equal(0m, metrics.TotalFees); + Assert.Equal(10m, metrics.NetPnL); + Assert.Equal(10m, metrics.TotalROI); + Assert.Equal(210m, metrics.TotalVolume); + Assert.Equal(100m, metrics.Collateral); + Assert.Equal(1, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithMultipleAssets_AggregatesCorrectly() + { + var positions = new List + { + CreatePositionWithAsset( + openPrice: 50000m, + quantity: 0.1m, + direction: TradeDirection.Long, + asset: Ticker.BTC, + realizedPnL: 100m, + netPnL: 98m, + uiFees: 1m, + gasFees: 1m, + stopLossPrice: 47500m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 52500m, + takeProfitStatus: TradeStatus.Filled), + CreatePositionWithAsset( + openPrice: 3000m, + quantity: 1m, + direction: TradeDirection.Short, + asset: Ticker.ETH, + realizedPnL: 50m, + netPnL: 48m, + uiFees: 1m, + gasFees: 1m, + stopLossPrice: 3150m, + stopLossStatus: TradeStatus.Cancelled, + takeProfitPrice: 2850m, + takeProfitStatus: TradeStatus.Filled) + }; + + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(150m, metrics.TotalPnL); + Assert.Equal(4m, metrics.TotalFees); + Assert.Equal(146m, metrics.NetPnL); + Assert.Equal(146m / 8000m * 100m, metrics.TotalROI); // 146 / 8000 * 100 + Assert.Equal(10250m + 5850m, metrics.TotalVolume); // BTC: 5000+5250, ETH: 3000+2850 + Assert.Equal(5000m + 3000m, metrics.Collateral); // 50000*0.1 + 3000*1 + Assert.Equal(2, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } + private static Position CreatePosition(decimal openPrice, decimal quantity, TradeDirection direction, decimal realizedPnL, decimal netPnL, decimal uiFees, decimal gasFees, decimal stopLossPrice, TradeStatus stopLossStatus, decimal takeProfitPrice, - TradeStatus takeProfitStatus) + TradeStatus takeProfitStatus, decimal leverage = 1m) { var position = new Position( Guid.NewGuid(), @@ -74,11 +450,11 @@ public class TradingBoxAgentSummaryMetricsTests user: new User { Id = 1, Name = "tester" }); position.Status = PositionStatus.Finished; - position.Open = BuildTrade(direction, TradeStatus.Filled, openPrice, quantity); + position.Open = BuildTrade(direction, TradeStatus.Filled, openPrice, quantity, leverage); position.StopLoss = BuildTrade(direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long, - stopLossStatus, stopLossPrice, quantity); + stopLossStatus, stopLossPrice, quantity, leverage); position.TakeProfit1 = BuildTrade(direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long, - takeProfitStatus, takeProfitPrice, quantity); + takeProfitStatus, takeProfitPrice, quantity, leverage); position.ProfitAndLoss = new ProfitAndLoss { Realized = realizedPnL, @@ -90,7 +466,7 @@ public class TradingBoxAgentSummaryMetricsTests return position; } - private static Trade BuildTrade(TradeDirection direction, TradeStatus status, decimal price, decimal quantity) + private static Trade BuildTrade(TradeDirection direction, TradeStatus status, decimal price, decimal quantity, decimal leverage = 1m) { return new Trade( date: DateTime.UtcNow, @@ -100,9 +476,317 @@ public class TradingBoxAgentSummaryMetricsTests ticker: Ticker.BTC, quantity: quantity, price: price, - leverage: 1m, + leverage: leverage, exchangeOrderId: Guid.NewGuid().ToString(), message: "unit-trade"); } -} + private static Position CreateInvalidPosition() + { + var position = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: TradeDirection.Long, + ticker: Ticker.BTC, + moneyManagement: new LightMoneyManagement + { + Name = "unit-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 1m + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + position.Status = PositionStatus.New; // New positions are not valid for metrics + return position; + } + + private static Position CreateCanceledPosition() + { + var position = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: TradeDirection.Long, + ticker: Ticker.BTC, + moneyManagement: new LightMoneyManagement + { + Name = "unit-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 1m + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + position.Status = PositionStatus.Canceled; // Canceled positions are not valid for metrics + return position; + } + + private static Position CreateRejectedPosition() + { + var position = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: TradeDirection.Long, + ticker: Ticker.BTC, + moneyManagement: new LightMoneyManagement + { + Name = "unit-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 1m + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + position.Status = PositionStatus.Rejected; // Rejected positions are not valid for metrics + return position; + } + + private static Position CreatePositionWithZeroCollateral() + { + var position = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: TradeDirection.Long, + ticker: Ticker.BTC, + moneyManagement: new LightMoneyManagement + { + Name = "unit-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 1m + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + position.Status = PositionStatus.Finished; + position.Open = BuildTrade(TradeDirection.Long, TradeStatus.Filled, 0m, 1m); // Zero price = zero collateral + position.TakeProfit1 = BuildTrade(TradeDirection.Short, TradeStatus.Filled, 10m, 1m); + position.ProfitAndLoss = new ProfitAndLoss + { + Realized = 10m, + Net = 9m + }; + position.UiFees = 0.5m; + position.GasFees = 0.5m; + + return position; + } + + private static Position CreatePositionWithLeverage(decimal openPrice, decimal quantity, decimal leverage, + TradeDirection direction, decimal realizedPnL, decimal netPnL, decimal uiFees, decimal gasFees, + decimal stopLossPrice, TradeStatus stopLossStatus, decimal takeProfitPrice, TradeStatus takeProfitStatus) + { + var position = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: direction, + ticker: Ticker.BTC, + moneyManagement: new LightMoneyManagement + { + Name = "unit-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = leverage + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + position.Status = PositionStatus.Finished; + position.Open = BuildTrade(direction, TradeStatus.Filled, openPrice, quantity, leverage); + position.StopLoss = BuildTrade(direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long, + stopLossStatus, stopLossPrice, quantity, leverage); + position.TakeProfit1 = BuildTrade(direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long, + takeProfitStatus, takeProfitPrice, quantity, leverage); + position.ProfitAndLoss = new ProfitAndLoss + { + Realized = realizedPnL, + Net = netPnL + }; + position.UiFees = uiFees; + position.GasFees = gasFees; + + return position; + } + + private static Position CreatePositionWithAsset(decimal openPrice, decimal quantity, TradeDirection direction, + Ticker asset, decimal realizedPnL, decimal netPnL, decimal uiFees, decimal gasFees, + decimal stopLossPrice, TradeStatus stopLossStatus, decimal takeProfitPrice, TradeStatus takeProfitStatus) + { + var position = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: direction, + ticker: asset, + moneyManagement: new LightMoneyManagement + { + Name = "unit-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 1m + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + position.Status = PositionStatus.Finished; + position.Open = new Trade( + date: DateTime.UtcNow, + direction: direction, + status: TradeStatus.Filled, + tradeType: TradeType.Market, + ticker: asset, + quantity: quantity, + price: openPrice, + leverage: 1m, + exchangeOrderId: Guid.NewGuid().ToString(), + message: "unit-trade"); + position.StopLoss = new Trade( + date: DateTime.UtcNow, + direction: direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long, + status: stopLossStatus, + tradeType: TradeType.Market, + ticker: asset, + quantity: quantity, + price: stopLossPrice, + leverage: 1m, + exchangeOrderId: Guid.NewGuid().ToString(), + message: "unit-trade"); + position.TakeProfit1 = new Trade( + date: DateTime.UtcNow, + direction: direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long, + status: takeProfitStatus, + tradeType: TradeType.Market, + ticker: asset, + quantity: quantity, + price: takeProfitPrice, + leverage: 1m, + exchangeOrderId: Guid.NewGuid().ToString(), + message: "unit-trade"); + position.ProfitAndLoss = new ProfitAndLoss + { + Realized = realizedPnL, + Net = netPnL + }; + position.UiFees = uiFees; + position.GasFees = gasFees; + + return position; + } + + [Fact] + public void CalculateAgentSummaryMetrics_WithHighLeverage_CalculatesCorrectVolumeAndCollateral() + { + // Create positions with high leverage manually + var btcPosition = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: TradeDirection.Long, + ticker: Ticker.BTC, + moneyManagement: new LightMoneyManagement + { + Name = "high-leverage-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 10m // High leverage + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + btcPosition.Status = PositionStatus.Finished; + btcPosition.Open = BuildTrade(TradeDirection.Long, TradeStatus.Filled, 50000m, 0.1m, 10m); // With leverage + btcPosition.StopLoss = BuildTrade(TradeDirection.Short, TradeStatus.Cancelled, 47500m, 0.1m, 10m); + btcPosition.TakeProfit1 = BuildTrade(TradeDirection.Short, TradeStatus.Filled, 52500m, 0.1m, 10m); + btcPosition.ProfitAndLoss = new ProfitAndLoss { Realized = 1000m, Net = 990m }; + btcPosition.UiFees = 5m; + btcPosition.GasFees = 5m; + + var ethPosition = new Position( + Guid.NewGuid(), + accountId: 1, + originDirection: TradeDirection.Short, + ticker: Ticker.ETH, + moneyManagement: new LightMoneyManagement + { + Name = "high-leverage-test", + Timeframe = Timeframe.OneHour, + StopLoss = 0.02m, + TakeProfit = 0.04m, + Leverage = 5m // Moderate leverage + }, + initiator: PositionInitiator.User, + date: DateTime.UtcNow, + user: new User { Id = 1, Name = "tester" }); + + ethPosition.Status = PositionStatus.Finished; + ethPosition.Open = new Trade( + date: DateTime.UtcNow, + direction: TradeDirection.Short, + status: TradeStatus.Filled, + tradeType: TradeType.Market, + ticker: Ticker.ETH, + quantity: 1m, + price: 3000m, + leverage: 5m, + exchangeOrderId: Guid.NewGuid().ToString(), + message: "unit-trade"); + ethPosition.StopLoss = new Trade( + date: DateTime.UtcNow, + direction: TradeDirection.Long, + status: TradeStatus.Cancelled, + tradeType: TradeType.Market, + ticker: Ticker.ETH, + quantity: 1m, + price: 3150m, + leverage: 5m, + exchangeOrderId: Guid.NewGuid().ToString(), + message: "unit-trade"); + ethPosition.TakeProfit1 = new Trade( + date: DateTime.UtcNow, + direction: TradeDirection.Long, + status: TradeStatus.Filled, + tradeType: TradeType.Market, + ticker: Ticker.ETH, + quantity: 1m, + price: 2850m, + leverage: 5m, + exchangeOrderId: Guid.NewGuid().ToString(), + message: "unit-trade"); + ethPosition.ProfitAndLoss = new ProfitAndLoss { Realized = 300m, Net = 295m }; + ethPosition.UiFees = 2.5m; + ethPosition.GasFees = 2.5m; + + var positions = new List { btcPosition, ethPosition }; + var metrics = TradingBox.CalculateAgentSummaryMetrics(positions); + + Assert.Equal(1300m, metrics.TotalPnL); // 1000 + 300 + Assert.Equal(15m, metrics.TotalFees); // 5+5 + 2.5+2.5 + Assert.Equal(1285m, metrics.NetPnL); // 1300 - 15 + Assert.Equal(1285m / 8000m * 100m, metrics.TotalROI); // 1285 / 8000 * 100 + + // Volume calculations with leverage: price * quantity * leverage + // BTC: (50000 * 0.1 * 10) + (52500 * 0.1 * 10) = 50000 + 52500 = 102500 + // ETH: (3000 * 1 * 5) + (2850 * 1 * 5) = 15000 + 14250 = 29250 + Assert.Equal(102500m + 29250m, metrics.TotalVolume); // 131750 + + // Collateral is price * quantity (without leverage) + Assert.Equal(5000m + 3000m, metrics.Collateral); // 8000 + Assert.Equal(2, metrics.Wins); + Assert.Equal(0, metrics.Losses); + } +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index 42269cb8..0f8d16bd 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -849,17 +849,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable .Where(p => p.IsValidForMetrics()).ToList(); }); - // Calculate statistics using TradingBox helpers - var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(positionForMetrics); - var pnl = positionForMetrics.Sum(p => p.ProfitAndLoss.Realized); - var fees = positionForMetrics.Sum(p => p.CalculateTotalFees()); - var netPnl = pnl - fees; // Net PnL after fees - var volume = TradingBox.GetTotalVolumeTraded(positionForMetrics); - - // Calculate ROI based on total investment (Net PnL) - var totalInvestment = positionForMetrics - .Sum(p => p.Open.Quantity * p.Open.Price); - var roi = totalInvestment > 0 ? (netPnl / totalInvestment) * 100 : 0; + // Calculate statistics using TradingBox.CalculateAgentSummaryMetrics + var agentMetrics = TradingBox.CalculateAgentSummaryMetrics(positionForMetrics); // Calculate long and short position counts var longPositionCount = positionForMetrics @@ -880,13 +871,13 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable LastStopTime = _state.State.LastStopTime, AccumulatedRunTimeSeconds = _state.State.AccumulatedRunTimeSeconds, CreateDate = _state.State.CreateDate, - TradeWins = tradeWins, - TradeLosses = tradeLosses, - Pnl = pnl, // Gross PnL before fees - NetPnL = netPnl, // Net PnL after fees - Roi = roi, - Volume = volume, - Fees = fees, + TradeWins = agentMetrics.Wins, + TradeLosses = agentMetrics.Losses, + Pnl = agentMetrics.TotalPnL, // Gross PnL before fees + NetPnL = agentMetrics.NetPnL, // Net PnL after fees + Roi = agentMetrics.TotalROI, + Volume = agentMetrics.TotalVolume, + Fees = agentMetrics.TotalFees, LongPositionCount = longPositionCount, ShortPositionCount = shortPositionCount }; diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 1b933244..62720c3b 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -540,12 +540,12 @@ public static class TradingBox totalVolume += position.Open.Quantity * position.Open.Price * position.Open.Leverage; // Add exit volumes from stop loss or take profits if they were executed - if (position.StopLoss.Status == TradeStatus.Filled) + if (position.StopLoss != null && position.StopLoss.Status == TradeStatus.Filled) { totalVolume += position.StopLoss.Quantity * position.StopLoss.Price * position.StopLoss.Leverage; } - if (position.TakeProfit1.Status == TradeStatus.Filled) + if (position.TakeProfit1 != null && position.TakeProfit1.Status == TradeStatus.Filled) { totalVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price * position.TakeProfit1.Leverage;