979 lines
42 KiB
C#
979 lines
42 KiB
C#
using FluentAssertions;
|
|
using Managing.Domain.Shared.Helpers;
|
|
using Managing.Domain.Trades;
|
|
using Managing.Domain.Users;
|
|
using Xunit;
|
|
using static Managing.Common.Enums;
|
|
|
|
namespace Managing.Domain.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for Platform Summary Metrics calculations in TradingBox
|
|
/// </summary>
|
|
public class PlatformSummaryMetricsTests
|
|
{
|
|
protected static readonly DateTime TestDate = new(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
// Test data builders for Position with different statuses and scenarios
|
|
|
|
// Main Position builder with status control
|
|
protected static Position CreateTestPosition(decimal openPrice = 50000m, decimal quantity = 0.001m,
|
|
TradeDirection direction = TradeDirection.Long, decimal leverage = 1m,
|
|
decimal? stopLossPercentage = 0.02m, decimal? takeProfitPercentage = 0.04m,
|
|
PositionStatus positionStatus = PositionStatus.Filled, PositionInitiator initiator = PositionInitiator.User,
|
|
bool includeTrades = true)
|
|
{
|
|
var user = new User { Id = 1, Name = "TestUser" };
|
|
var moneyManagement = new LightMoneyManagement
|
|
{
|
|
Name = "TestMM",
|
|
Timeframe = Timeframe.OneHour,
|
|
StopLoss = stopLossPercentage ?? 0.02m,
|
|
TakeProfit = takeProfitPercentage ?? 0.04m,
|
|
Leverage = leverage
|
|
};
|
|
|
|
var position = new Position(
|
|
identifier: Guid.NewGuid(),
|
|
accountId: 1,
|
|
originDirection: direction,
|
|
ticker: Ticker.BTC,
|
|
moneyManagement: moneyManagement,
|
|
initiator: initiator,
|
|
date: TestDate,
|
|
user: user
|
|
);
|
|
|
|
// Override the status set by constructor
|
|
position.Status = positionStatus;
|
|
|
|
if (includeTrades)
|
|
{
|
|
// Set the Open trade
|
|
position.Open = new Trade(
|
|
date: TestDate,
|
|
direction: direction,
|
|
status: positionStatus == PositionStatus.New ? TradeStatus.PendingOpen : TradeStatus.Filled,
|
|
tradeType: TradeType.Market,
|
|
ticker: Ticker.BTC,
|
|
quantity: quantity,
|
|
price: openPrice,
|
|
leverage: leverage,
|
|
exchangeOrderId: Guid.NewGuid().ToString(),
|
|
message: "Open position"
|
|
);
|
|
|
|
// Calculate SL/TP prices based on direction
|
|
decimal stopLossPrice, takeProfitPrice;
|
|
|
|
if (direction == TradeDirection.Long)
|
|
{
|
|
stopLossPrice = openPrice * (1 - (stopLossPercentage ?? 0.02m));
|
|
takeProfitPrice = openPrice * (1 + (takeProfitPercentage ?? 0.04m));
|
|
}
|
|
else // Short
|
|
{
|
|
stopLossPrice = openPrice * (1 + (stopLossPercentage ?? 0.02m));
|
|
takeProfitPrice = openPrice * (1 - (takeProfitPercentage ?? 0.04m));
|
|
}
|
|
|
|
// Set the StopLoss trade with status based on position status
|
|
TradeStatus slTpStatus = positionStatus == PositionStatus.Finished ? TradeStatus.Filled :
|
|
positionStatus == PositionStatus.Filled ? TradeStatus.PendingOpen : TradeStatus.PendingOpen;
|
|
position.StopLoss = new Trade(
|
|
date: TestDate.AddMinutes(5),
|
|
direction: direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long,
|
|
status: slTpStatus,
|
|
tradeType: TradeType.Market,
|
|
ticker: Ticker.BTC,
|
|
quantity: quantity,
|
|
price: stopLossPrice,
|
|
leverage: leverage,
|
|
exchangeOrderId: Guid.NewGuid().ToString(),
|
|
message: "Stop Loss"
|
|
);
|
|
|
|
// Set the TakeProfit trade
|
|
position.TakeProfit1 = new Trade(
|
|
date: TestDate.AddMinutes(10),
|
|
direction: direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long,
|
|
status: slTpStatus,
|
|
tradeType: TradeType.Market,
|
|
ticker: Ticker.BTC,
|
|
quantity: quantity,
|
|
price: takeProfitPrice,
|
|
leverage: leverage,
|
|
exchangeOrderId: Guid.NewGuid().ToString(),
|
|
message: "Take Profit"
|
|
);
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
// Filled position (active trading position)
|
|
protected static Position CreateFilledPosition(decimal openPrice = 50000m, decimal quantity = 0.001m,
|
|
TradeDirection direction = TradeDirection.Long, decimal leverage = 1m,
|
|
decimal? stopLossPercentage = 0.02m, decimal? takeProfitPercentage = 0.04m)
|
|
{
|
|
return CreateTestPosition(openPrice, quantity, direction, leverage,
|
|
stopLossPercentage, takeProfitPercentage, PositionStatus.Filled, includeTrades: true);
|
|
}
|
|
|
|
// Finished position (closed, profit/loss realized)
|
|
protected static Position CreateFinishedPosition(decimal openPrice = 50000m, decimal quantity = 0.001m,
|
|
TradeDirection direction = TradeDirection.Long, decimal leverage = 1m,
|
|
decimal closePrice = 51000m, bool closedBySL = false)
|
|
{
|
|
var position = CreateTestPosition(openPrice, quantity, direction, leverage,
|
|
positionStatus: PositionStatus.Finished, includeTrades: true);
|
|
|
|
// Update open trade to be closed
|
|
position.Open.Status = TradeStatus.Filled;
|
|
|
|
// Determine which trade closed the position
|
|
if (closedBySL)
|
|
{
|
|
position.StopLoss.Status = TradeStatus.Filled;
|
|
position.StopLoss.Date = TestDate.AddMinutes(30);
|
|
position.TakeProfit1.Status = TradeStatus.Cancelled;
|
|
}
|
|
else
|
|
{
|
|
position.TakeProfit1.Status = TradeStatus.Filled;
|
|
position.TakeProfit1.Date = TestDate.AddMinutes(30);
|
|
position.StopLoss.Status = TradeStatus.Cancelled;
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
// Canceled position
|
|
protected static Position CreateCanceledPosition(decimal openPrice = 50000m, decimal quantity = 0.001m,
|
|
TradeDirection direction = TradeDirection.Long, decimal leverage = 1m)
|
|
{
|
|
var position = CreateTestPosition(openPrice, quantity, direction, leverage,
|
|
positionStatus: PositionStatus.Canceled, includeTrades: true);
|
|
|
|
position.Open.Status = TradeStatus.Cancelled;
|
|
position.StopLoss.Status = TradeStatus.Cancelled;
|
|
position.TakeProfit1.Status = TradeStatus.Cancelled;
|
|
|
|
return position;
|
|
}
|
|
|
|
// Rejected position
|
|
protected static Position CreateRejectedPosition(decimal openPrice = 50000m, decimal quantity = 0.001m,
|
|
TradeDirection direction = TradeDirection.Long, decimal leverage = 1m)
|
|
{
|
|
var position = CreateTestPosition(openPrice, quantity, direction, leverage,
|
|
positionStatus: PositionStatus.Rejected, includeTrades: true);
|
|
|
|
position.Open.Status = TradeStatus.Cancelled;
|
|
position.StopLoss.Status = TradeStatus.Cancelled;
|
|
position.TakeProfit1.Status = TradeStatus.Cancelled;
|
|
|
|
return position;
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithEmptyPositions_ReturnsEmptyMetrics()
|
|
{
|
|
// Arrange
|
|
var positions = new List<Position>();
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(0);
|
|
result.TotalPlatformFees.Should().Be(0);
|
|
result.TotalPlatformPnL.Should().Be(0);
|
|
result.NetPnL.Should().Be(0);
|
|
result.OpenInterest.Should().Be(0);
|
|
result.TotalLifetimePositionCount.Should().Be(0);
|
|
result.VolumeByAsset.Should().BeEmpty();
|
|
result.PositionCountByAsset.Should().BeEmpty();
|
|
result.PositionCountByDirection.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithNullPositions_ReturnsEmptyMetrics()
|
|
{
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(null);
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(0);
|
|
result.TotalPlatformFees.Should().Be(0);
|
|
result.TotalPlatformPnL.Should().Be(0);
|
|
result.NetPnL.Should().Be(0);
|
|
result.OpenInterest.Should().Be(0);
|
|
result.TotalLifetimePositionCount.Should().Be(0);
|
|
result.VolumeByAsset.Should().BeEmpty();
|
|
result.PositionCountByAsset.Should().BeEmpty();
|
|
result.PositionCountByDirection.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithSingleFinishedPosition_CalculatesCorrectMetrics()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 0.1m,
|
|
direction: TradeDirection.Long,
|
|
leverage: 2m,
|
|
closePrice: 51000m,
|
|
closedBySL: false
|
|
);
|
|
|
|
// Calculate expected values - use the actual TP1 price from the position
|
|
var expectedOpenVolume = 50000m * 0.1m * 2m; // Open volume
|
|
var expectedCloseVolume = position.TakeProfit1.Price * 0.1m * 2m; // Close volume using actual TP1 price
|
|
var expectedVolume = expectedOpenVolume + expectedCloseVolume;
|
|
var expectedFees = position.CalculateTotalFees();
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalPlatformFees.Should().Be(expectedFees);
|
|
result.TotalLifetimePositionCount.Should().Be(1);
|
|
result.OpenInterest.Should().Be(0); // Position is closed
|
|
result.VolumeByAsset.Should().ContainKey("BTC").WhoseValue.Should().Be(expectedVolume);
|
|
result.PositionCountByAsset.Should().ContainKey("BTC").WhoseValue.Should().Be(1);
|
|
result.PositionCountByDirection.Should().BeEmpty(); // No open positions
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithMultiplePositions_CalculatesAggregatedMetrics()
|
|
{
|
|
// Arrange
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 2m, 51000m, false),
|
|
CreateFinishedPosition(40000m, 0.2m, TradeDirection.Short, 1m, 39000m, false),
|
|
CreateFilledPosition(60000m, 0.05m, TradeDirection.Long, 3m) // Open position
|
|
};
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalLifetimePositionCount.Should().Be(3);
|
|
result.VolumeByAsset.Should().ContainKey("BTC");
|
|
result.PositionCountByAsset.Should().ContainKey("BTC").WhoseValue.Should().Be(3);
|
|
result.PositionCountByDirection.Should().ContainKey(TradeDirection.Long).WhoseValue.Should().Be(1); // Only the open position
|
|
result.OpenInterest.Should().Be(60000m * 0.05m * 3m); // Only from the open position: price * quantity * leverage = 9000
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithPreviousVolume_PreventsVolumeDecrease()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false);
|
|
var previousVolume = 100000m; // Higher than calculated volume
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position }, previousVolume);
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(previousVolume); // Should not decrease
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithInvalidPositions_ExcludesThemFromCalculations()
|
|
{
|
|
// Arrange
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFinishedPosition(), // Valid position
|
|
CreateCanceledPosition(), // Invalid position
|
|
CreateRejectedPosition() // Invalid position
|
|
};
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalLifetimePositionCount.Should().Be(1); // Only the finished position counts
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithMultipleAssets_CalculatesAssetBreakdowns()
|
|
{
|
|
// Arrange - Create positions for different assets
|
|
var btcPosition = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false);
|
|
var ethPosition = CreateFinishedPosition(3000m, 1m, TradeDirection.Short, 1m, 2900m, false);
|
|
|
|
// Manually set different tickers (since CreateFinishedPosition defaults to BTC)
|
|
ethPosition.Ticker = Ticker.ETH;
|
|
|
|
var positions = new List<Position> { btcPosition, ethPosition };
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.VolumeByAsset.Should().HaveCount(2);
|
|
result.VolumeByAsset.Should().ContainKey("BTC");
|
|
result.VolumeByAsset.Should().ContainKey("ETH");
|
|
result.PositionCountByAsset.Should().HaveCount(2);
|
|
result.PositionCountByAsset["BTC"].Should().Be(1);
|
|
result.PositionCountByAsset["ETH"].Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithOpenPositions_IncludesOpenInterestAndDirectionCounts()
|
|
{
|
|
// Arrange
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFilledPosition(50000m, 0.1m, TradeDirection.Long, 2m), // Open long
|
|
CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m), // Open short
|
|
CreateFinishedPosition(30000m, 0.05m, TradeDirection.Long, 1m, 31000m, false) // Closed
|
|
};
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalLifetimePositionCount.Should().Be(3);
|
|
result.PositionCountByDirection.Should().HaveCount(2);
|
|
result.PositionCountByDirection[TradeDirection.Long].Should().Be(1);
|
|
result.PositionCountByDirection[TradeDirection.Short].Should().Be(1);
|
|
result.OpenInterest.Should().BeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithLongPosition_CalculatesCorrectVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 52000m, false);
|
|
|
|
// Expected volume: Open volume + Close volume
|
|
// Open: 50000 * 0.1 * 1 = 5000
|
|
// Close: 52000 * 0.1 * 1 = 5200 (TP1 price)
|
|
var expectedVolume = 5000m + 5200m;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.VolumeByAsset["BTC"].Should().Be(expectedVolume);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithShortPosition_CalculatesCorrectVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Short, 1m, 48000m, false);
|
|
|
|
// Expected volume: Open volume + Close volume
|
|
// Open: 50000 * 0.1 * 1 = 5000
|
|
// Close: 48000 * 0.1 * 1 = 4800 (TP1 price)
|
|
var expectedVolume = 5000m + 4800m;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.VolumeByAsset["BTC"].Should().Be(expectedVolume);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithFlippedPosition_IncludesAllVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateTestPosition(50000m, 0.1m, TradeDirection.Long, 1m,
|
|
positionStatus: PositionStatus.Flipped, includeTrades: true);
|
|
|
|
// Set up a flipped position - typically closed by TP1, then reopened in opposite direction
|
|
position.Open.Status = TradeStatus.Filled;
|
|
position.TakeProfit1.Status = TradeStatus.Filled; // Position was closed by take profit
|
|
position.StopLoss.Status = TradeStatus.Cancelled; // SL was cancelled when TP was hit
|
|
|
|
// Expected volume: Open + TP1 (since position was closed by take profit)
|
|
var expectedVolume = (50000m * 0.1m * 1m) + // Open
|
|
(position.TakeProfit1.Price * 0.1m * 1m); // TP1 only
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalLifetimePositionCount.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithHighLeverage_CalculatesCorrectVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 0.1m,
|
|
direction: TradeDirection.Long,
|
|
leverage: 50m, // Very high leverage
|
|
closePrice: 51000m,
|
|
closedBySL: false
|
|
);
|
|
|
|
// Expected volume: Open volume + Close volume with high leverage
|
|
var expectedOpenVolume = 50000m * 0.1m * 50m; // 250,000
|
|
var expectedCloseVolume = position.TakeProfit1.Price * 0.1m * 50m; // ~255,000
|
|
var expectedVolume = expectedOpenVolume + expectedCloseVolume;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalPlatformVolume.Should().BeGreaterThan(500000m); // Should be over 500k with 50x leverage
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithFractionalLeverage_CalculatesCorrectVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 0.1m,
|
|
direction: TradeDirection.Long,
|
|
leverage: 0.5m, // Fractional leverage (less than 1)
|
|
closePrice: 51000m,
|
|
closedBySL: false
|
|
);
|
|
|
|
// Expected volume: Open volume + Close volume with fractional leverage
|
|
var expectedOpenVolume = 50000m * 0.1m * 0.5m; // 2,500
|
|
var expectedCloseVolume = position.TakeProfit1.Price * 0.1m * 0.5m; // ~2,550
|
|
var expectedVolume = expectedOpenVolume + expectedCloseVolume;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalPlatformVolume.Should().BeLessThan(10000m); // Should be under 10k with 0.5x leverage
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithVerySmallQuantity_CalculatesCorrectVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 0.00001m, // Very small quantity (dust position)
|
|
direction: TradeDirection.Long,
|
|
leverage: 1m,
|
|
closePrice: 51000m,
|
|
closedBySL: false
|
|
);
|
|
|
|
// Expected volume: Should handle very small numbers correctly
|
|
var expectedOpenVolume = 50000m * 0.00001m * 1m; // 0.5
|
|
var expectedCloseVolume = position.TakeProfit1.Price * 0.00001m * 1m; // ~0.51
|
|
var expectedVolume = expectedOpenVolume + expectedCloseVolume;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalPlatformVolume.Should().BeLessThan(2m); // Should be very small
|
|
result.TotalPlatformVolume.Should().BeGreaterThan(0m); // But still positive
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithPositionClosedBySL_CalculatesCorrectVolume()
|
|
{
|
|
// Arrange
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 0.1m,
|
|
direction: TradeDirection.Long,
|
|
leverage: 1m,
|
|
closePrice: 49000m, // Price below entry (SL hit)
|
|
closedBySL: true // This creates a position closed by StopLoss
|
|
);
|
|
|
|
// Expected volume: Open volume + SL volume (TP1 should be cancelled)
|
|
var expectedOpenVolume = 50000m * 0.1m * 1m; // 5,000
|
|
var expectedCloseVolume = position.StopLoss.Price * 0.1m * 1m; // SL price * quantity * leverage
|
|
var expectedVolume = expectedOpenVolume + expectedCloseVolume;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalPlatformVolume.Should().Be(50000m * 0.1m + position.StopLoss.Price * 0.1m); // Open + SL only
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithMultipleClosingTrades_IncludesAllVolume()
|
|
{
|
|
// Arrange - Create a position that has both TP1 and SL filled (flipped position)
|
|
var position = CreateTestPosition(50000m, 0.1m, TradeDirection.Long, 1m,
|
|
positionStatus: PositionStatus.Flipped, includeTrades: true);
|
|
|
|
// Manually set up a position with multiple closing trades
|
|
position.Open.Status = TradeStatus.Filled;
|
|
position.StopLoss.Status = TradeStatus.Filled; // SL was filled
|
|
position.TakeProfit1.Status = TradeStatus.Filled; // TP1 was also filled
|
|
position.TakeProfit2 = new Trade( // Add TP2
|
|
date: TestDate.AddMinutes(15),
|
|
direction: TradeDirection.Short,
|
|
status: TradeStatus.Filled,
|
|
tradeType: TradeType.Market,
|
|
ticker: Ticker.BTC,
|
|
quantity: 0.05m, // Partial close
|
|
price: 53000m,
|
|
leverage: 1m,
|
|
exchangeOrderId: Guid.NewGuid().ToString(),
|
|
message: "Take Profit 2"
|
|
);
|
|
|
|
// Expected volume: Open + SL + TP1 + TP2
|
|
var expectedVolume = (50000m * 0.1m * 1m) + // Open: 5,000
|
|
(position.StopLoss.Price * 0.1m * 1m) + // SL: ~4,900
|
|
(position.TakeProfit1.Price * 0.1m * 1m) + // TP1: ~5,200
|
|
(53000m * 0.05m * 1m); // TP2: 2,650
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
result.TotalPlatformVolume.Should().BeGreaterThan(17000m); // Should include all trade volumes
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithMixedDirections_CalculatesSeparateDirectionCounts()
|
|
{
|
|
// Arrange
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFilledPosition(50000m, 0.1m, TradeDirection.Long, 1m), // Open long
|
|
CreateFilledPosition(50000m, 0.1m, TradeDirection.Long, 1m), // Another open long
|
|
CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m), // Open short
|
|
CreateFinishedPosition(30000m, 0.05m, TradeDirection.Short, 1m, 29000m, false) // Closed short
|
|
};
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalLifetimePositionCount.Should().Be(4);
|
|
result.PositionCountByDirection.Should().HaveCount(2);
|
|
result.PositionCountByDirection[TradeDirection.Long].Should().Be(2); // Two open longs
|
|
result.PositionCountByDirection[TradeDirection.Short].Should().Be(1); // One open short
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithSameAssetMultiplePositions_AggregatesCorrectly()
|
|
{
|
|
// Arrange - Multiple BTC positions
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false), // BTC position 1
|
|
CreateFinishedPosition(52000m, 0.05m, TradeDirection.Short, 2m, 51000m, false), // BTC position 2
|
|
CreateFinishedPosition(3000m, 1m, TradeDirection.Long, 1m, 3100m, false) // ETH position
|
|
};
|
|
|
|
// Set the third position to ETH
|
|
positions[2].Ticker = Ticker.ETH;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.VolumeByAsset.Should().HaveCount(2); // BTC and ETH
|
|
result.PositionCountByAsset.Should().HaveCount(2);
|
|
result.PositionCountByAsset["BTC"].Should().Be(2); // Two BTC positions
|
|
result.PositionCountByAsset["ETH"].Should().Be(1); // One ETH position
|
|
|
|
// BTC volume should be from both BTC positions using actual TP1 prices
|
|
var btcVolume1 = 50000m * 0.1m * 1m + positions[0].TakeProfit1.Price * 0.1m * 1m; // Long: 50000 + 52000
|
|
var btcVolume2 = 52000m * 0.05m * 2m + positions[1].TakeProfit1.Price * 0.05m * 2m; // Short: 5200 + (49920 * 0.05 * 2)
|
|
var expectedBtcVolume = btcVolume1 + btcVolume2;
|
|
|
|
result.VolumeByAsset["BTC"].Should().Be(expectedBtcVolume);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithZeroLeverage_HandlesGracefully()
|
|
{
|
|
// Arrange - Edge case with zero leverage (shouldn't happen in practice but test robustness)
|
|
var position = CreateTestPosition(50000m, 0.1m, TradeDirection.Long, 0m,
|
|
positionStatus: PositionStatus.Filled, includeTrades: true);
|
|
|
|
// Act & Assert - Should not throw and should handle zero leverage
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Should still work, just with zero volume
|
|
result.TotalPlatformVolume.Should().Be(0m);
|
|
result.OpenInterest.Should().Be(0m);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithExtremeLeverage_HandlesLargeNumbers()
|
|
{
|
|
// Arrange - Test with very high leverage to check for overflow issues
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 1m,
|
|
direction: TradeDirection.Long,
|
|
leverage: 1000m, // Extreme leverage
|
|
closePrice: 50001m, // Tiny profit
|
|
closedBySL: false
|
|
);
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert - Should handle large numbers without overflow
|
|
result.TotalPlatformVolume.Should().BeGreaterThan(100000000m); // 100M+ with 1000x leverage
|
|
result.TotalPlatformVolume.Should().Be(50000m * 1m * 1000m + position.TakeProfit1.Price * 1m * 1000m);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithNegativePnL_CalculatesCorrectly()
|
|
{
|
|
// Arrange - Create a position with loss
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 50000m,
|
|
quantity: 0.1m,
|
|
direction: TradeDirection.Long,
|
|
leverage: 1m,
|
|
closePrice: 49000m, // Lower than entry = loss
|
|
closedBySL: true // Closed by SL
|
|
);
|
|
|
|
// Manually set negative PnL (since positions don't calculate this automatically)
|
|
position.ProfitAndLoss = new ProfitAndLoss(new List<Tuple<decimal, decimal>>
|
|
{
|
|
new Tuple<decimal, decimal>(0.1m, 50000m), // Open
|
|
new Tuple<decimal, decimal>(-0.1m, position.StopLoss.Price) // Close at SL
|
|
}, TradeDirection.Long);
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert - Should handle negative PnL correctly
|
|
result.TotalPlatformPnL.Should().BeLessThan(0); // Negative PnL
|
|
result.NetPnL.Should().Be(result.TotalPlatformPnL - result.TotalPlatformFees);
|
|
result.NetPnL.Should().BeLessThan(0); // Net loss
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithOnlyOpenPositions_IncludesAllInOpenInterest()
|
|
{
|
|
// Arrange - Only open positions
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFilledPosition(50000m, 0.1m, TradeDirection.Long, 2m), // Open long
|
|
CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m), // Open short
|
|
CreateFilledPosition(60000m, 0.05m, TradeDirection.Long, 3m) // Another open long
|
|
};
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalLifetimePositionCount.Should().Be(3);
|
|
result.PositionCountByDirection.Should().HaveCount(2);
|
|
result.PositionCountByDirection[TradeDirection.Long].Should().Be(2);
|
|
result.PositionCountByDirection[TradeDirection.Short].Should().Be(1);
|
|
|
|
// Open interest should include all open positions
|
|
var expectedOpenInterest = (50000m * 0.1m * 2m) + (40000m * 0.2m * 1m) + (60000m * 0.05m * 3m);
|
|
result.OpenInterest.Should().Be(expectedOpenInterest);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithComplexScenario_CalculatesAllMetricsCorrectly()
|
|
{
|
|
// Arrange - Complex scenario with multiple positions, different statuses, assets, and directions
|
|
var positions = new List<Position>
|
|
{
|
|
// Closed positions
|
|
CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false), // BTC Long profit
|
|
CreateFinishedPosition(40000m, 0.2m, TradeDirection.Short, 2m, 41000m, true), // BTC Short loss (SL)
|
|
|
|
// Open positions
|
|
CreateFilledPosition(60000m, 0.05m, TradeDirection.Long, 3m), // BTC Long open
|
|
CreateFilledPosition(3000m, 1m, TradeDirection.Short, 1m), // ETH Short open
|
|
|
|
// Invalid positions (should be excluded)
|
|
CreateCanceledPosition(50000m, 0.01m, TradeDirection.Long, 1m), // Canceled
|
|
CreateRejectedPosition(40000m, 0.01m, TradeDirection.Short, 1m) // Rejected
|
|
};
|
|
|
|
// Set ETH ticker for the ETH position
|
|
positions[3].Ticker = Ticker.ETH;
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(positions);
|
|
|
|
// Assert
|
|
result.TotalLifetimePositionCount.Should().Be(4); // Only valid positions count
|
|
|
|
// Should have both BTC and ETH
|
|
result.VolumeByAsset.Should().HaveCount(2);
|
|
result.PositionCountByAsset.Should().HaveCount(2);
|
|
result.PositionCountByAsset["BTC"].Should().Be(3); // 2 closed + 1 open
|
|
result.PositionCountByAsset["ETH"].Should().Be(1); // 1 open
|
|
|
|
// Should have both directions in open positions
|
|
result.PositionCountByDirection.Should().HaveCount(2);
|
|
result.PositionCountByDirection[TradeDirection.Long].Should().Be(1); // 1 BTC long open
|
|
result.PositionCountByDirection[TradeDirection.Short].Should().Be(1); // 1 ETH short open
|
|
|
|
// Should have positive volume, some PnL, and open interest
|
|
result.TotalPlatformVolume.Should().BeGreaterThan(0);
|
|
result.OpenInterest.Should().BeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithVeryLargeNumbers_HandlesPrecisionCorrectly()
|
|
{
|
|
// Arrange - Test with large numbers to ensure decimal precision is maintained
|
|
var position = CreateFinishedPosition(
|
|
openPrice: 1000000m, // 1M price
|
|
quantity: 100m, // Large quantity
|
|
direction: TradeDirection.Long,
|
|
leverage: 10m, // Moderate leverage
|
|
closePrice: 1000100m,
|
|
closedBySL: false
|
|
);
|
|
|
|
// Calculate actual expected volume using the real TP1 price (not the closePrice parameter)
|
|
// TP1 price = 1,000,000 * (1 + 0.04) = 1,040,000 (default takeProfitPercentage)
|
|
var expectedOpenVolume = 1000000m * 100m * 10m; // 1,000,000,000
|
|
var expectedCloseVolume = position.TakeProfit1.Price * 100m * 10m; // 1,040,000 * 100 * 10 = 1,040,000,000
|
|
var expectedVolume = expectedOpenVolume + expectedCloseVolume; // 2,040,000,000
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert
|
|
result.TotalPlatformVolume.Should().Be(expectedVolume);
|
|
// Ensure no precision loss occurred
|
|
result.TotalPlatformVolume.Should().Be(2040000000m);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithZeroVolumePosition_ExcludesFromVolumeCalculation()
|
|
{
|
|
// Arrange - Create a position with zero quantity (edge case)
|
|
var position = CreateTestPosition(50000m, 0m, TradeDirection.Long, 1m,
|
|
positionStatus: PositionStatus.Filled, includeTrades: true);
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert - Should handle zero quantity gracefully
|
|
result.TotalPlatformVolume.Should().Be(0m);
|
|
result.TotalLifetimePositionCount.Should().Be(1); // Position still counts
|
|
result.OpenInterest.Should().Be(0m);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePlatformSummaryMetrics_WithInvalidTicker_HandlesGracefully()
|
|
{
|
|
// Arrange - Position with invalid ticker (edge case)
|
|
var position = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false);
|
|
|
|
// Manually set an invalid ticker (this shouldn't happen in practice)
|
|
position.Ticker = (Ticker)999; // Invalid enum value
|
|
|
|
// Act
|
|
var result = TradingBox.CalculatePlatformSummaryMetrics(new List<Position> { position });
|
|
|
|
// Assert - Should handle gracefully (use enum ToString)
|
|
result.VolumeByAsset.Should().HaveCount(1);
|
|
result.PositionCountByAsset.Should().HaveCount(1);
|
|
result.TotalPlatformVolume.Should().BeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateDailySnapshotFromPositions_WithDateFiltering_CalculatesCumulativeMetricsCorrectly()
|
|
{
|
|
// Arrange - Create positions with different dates spanning multiple days
|
|
var baseDate = TestDate.Date;
|
|
var positions = new List<Position>
|
|
{
|
|
// Day 1: Two positions (one finished, one filled)
|
|
CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false),
|
|
CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m),
|
|
|
|
// Day 2: One finished position
|
|
CreateFinishedPosition(60000m, 0.05m, TradeDirection.Long, 2m, 61200m, false),
|
|
|
|
// Day 3: Two positions (future dates - should be excluded)
|
|
};
|
|
|
|
// Set specific dates for positions
|
|
positions[0].Date = baseDate.AddHours(10); // Day 1, 10 AM
|
|
positions[1].Date = baseDate.AddHours(14); // Day 1, 2 PM
|
|
positions[2].Date = baseDate.AddDays(1).AddHours(11); // Day 2, 11 AM
|
|
|
|
// Future positions (should be excluded)
|
|
var futurePositions = new List<Position>
|
|
{
|
|
CreateFinishedPosition(30000m, 0.15m, TradeDirection.Short, 1m, 29100m, false),
|
|
CreateFilledPosition(70000m, 0.08m, TradeDirection.Long, 1m)
|
|
};
|
|
futurePositions[0].Date = baseDate.AddDays(2).AddHours(9); // Day 3, 9 AM
|
|
futurePositions[1].Date = baseDate.AddDays(2).AddHours(16); // Day 3, 4 PM
|
|
|
|
var allPositions = positions.Concat(futurePositions).ToList();
|
|
|
|
// Act - Calculate snapshot for Day 2 (should include Day 1 and Day 2 positions only)
|
|
var targetDate = baseDate.AddDays(1); // Day 2
|
|
var filteredPositions = allPositions.Where(p => p.Date.Date <= targetDate).ToList();
|
|
var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions);
|
|
|
|
// Assert - Should include positions from Day 1 and Day 2 only
|
|
metrics.TotalLifetimePositionCount.Should().Be(3); // 2 from Day 1 + 1 from Day 2
|
|
|
|
// Calculate expected volume: Day 1 positions + Day 2 position
|
|
// Default takeProfitPercentage is 0.04m (4%)
|
|
var day1Volume = (50000m * 0.1m * 1m + 52000m * 0.1m * 1m) + // Finished long: 5000 + 5200 = 10200
|
|
(40000m * 0.2m * 1m); // Open short: 8000 (not finished, no close volume)
|
|
var day2Volume = (60000m * 0.05m * 2m + 62400m * 0.05m * 2m); // Finished long with 2x leverage: 6000 + 6240 = 12240
|
|
var expectedTotalVolume = day1Volume + day2Volume; // 10200 + 8000 + 12240 = 30440
|
|
|
|
metrics.TotalPlatformVolume.Should().Be(expectedTotalVolume);
|
|
metrics.OpenInterest.Should().Be(40000m * 0.2m * 1m); // Only the open short position from Day 1
|
|
|
|
// Should have both BTC and position counts
|
|
metrics.VolumeByAsset.Should().ContainKey("BTC");
|
|
metrics.PositionCountByAsset["BTC"].Should().Be(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateDailySnapshotFromPositions_WithNoPositionsForDate_ReturnsEmptyMetrics()
|
|
{
|
|
// Arrange - Create positions all after the target date
|
|
var baseDate = TestDate.Date;
|
|
var positions = new List<Position>
|
|
{
|
|
CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false),
|
|
CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m)
|
|
};
|
|
|
|
// Set dates after target date
|
|
positions[0].Date = baseDate.AddDays(1).AddHours(10);
|
|
positions[1].Date = baseDate.AddDays(2).AddHours(14);
|
|
|
|
// Act - Calculate snapshot for a date before all positions
|
|
var targetDate = baseDate; // Today, before all positions
|
|
var filteredPositions = positions.Where(p => p.Date.Date <= targetDate).ToList();
|
|
var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions);
|
|
|
|
// Assert - Should have no positions for this date
|
|
metrics.TotalLifetimePositionCount.Should().Be(0);
|
|
metrics.TotalPlatformVolume.Should().Be(0);
|
|
metrics.TotalPlatformFees.Should().Be(0);
|
|
metrics.TotalPlatformPnL.Should().Be(0);
|
|
metrics.NetPnL.Should().Be(0);
|
|
metrics.OpenInterest.Should().Be(0);
|
|
metrics.VolumeByAsset.Should().BeEmpty();
|
|
metrics.PositionCountByAsset.Should().BeEmpty();
|
|
metrics.PositionCountByDirection.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateDailySnapshotFromPositions_WithMixedAssetsAndDirections_CalculatesAssetBreakdownsCorrectly()
|
|
{
|
|
// Arrange - Create positions with different assets and directions over multiple days
|
|
var baseDate = TestDate.Date;
|
|
var positions = new List<Position>
|
|
{
|
|
// Day 1: BTC Long (finished) and ETH Short (open)
|
|
CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false), // BTC finished
|
|
CreateFilledPosition(3000m, 1m, TradeDirection.Short, 1m), // ETH open
|
|
|
|
// Day 2: BTC Short (finished) and ETH Long (finished)
|
|
CreateFinishedPosition(40000m, 0.2m, TradeDirection.Short, 2m, 39200m, false), // BTC finished
|
|
CreateFinishedPosition(3100m, 0.5m, TradeDirection.Long, 1m, 3180m, false) // ETH finished
|
|
};
|
|
|
|
// Set dates
|
|
positions[0].Date = baseDate.AddHours(10); // Day 1 BTC
|
|
positions[1].Date = baseDate.AddHours(14); // Day 1 ETH
|
|
positions[2].Date = baseDate.AddDays(1).AddHours(11); // Day 2 BTC
|
|
positions[3].Date = baseDate.AddDays(1).AddHours(15); // Day 2 ETH
|
|
|
|
// Set ETH tickers for ETH positions
|
|
positions[1].Ticker = Ticker.ETH;
|
|
positions[3].Ticker = Ticker.ETH;
|
|
|
|
// Act - Calculate snapshot for Day 2 (includes all positions)
|
|
var targetDate = baseDate.AddDays(1);
|
|
var filteredPositions = positions.Where(p => p.Date.Date <= targetDate).ToList();
|
|
var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions);
|
|
|
|
// Assert - Should include all 4 positions
|
|
metrics.TotalLifetimePositionCount.Should().Be(4);
|
|
metrics.VolumeByAsset.Should().HaveCount(2); // BTC and ETH
|
|
metrics.PositionCountByAsset.Should().HaveCount(2);
|
|
metrics.PositionCountByAsset["BTC"].Should().Be(2);
|
|
metrics.PositionCountByAsset["ETH"].Should().Be(2);
|
|
|
|
// Should have only one direction (one open short ETH, rest are finished)
|
|
metrics.PositionCountByDirection.Should().HaveCount(1);
|
|
metrics.PositionCountByDirection[TradeDirection.Short].Should().Be(1); // Only the open ETH short position
|
|
|
|
// Open interest should only include the open ETH short position
|
|
metrics.OpenInterest.Should().Be(3000m * 1m * 1m); // ETH open short: 3000
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateDailySnapshotFromPositions_WithPositionSpanningMultipleDays_CalculatesVolumePerDayCorrectly()
|
|
{
|
|
// Arrange - Create positions representing the same position at different stages
|
|
var baseDate = TestDate.Date;
|
|
|
|
// Position as it appears on Day 1 (still open)
|
|
var positionDay1 = CreateFilledPosition(50000m, 0.1m, TradeDirection.Long, 1m);
|
|
positionDay1.Date = baseDate.AddHours(12); // Day 1, noon
|
|
|
|
// Position as it appears on Day 2 (now closed)
|
|
var positionDay2 = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 52000m, false);
|
|
positionDay2.Date = baseDate.AddHours(12); // Same open date
|
|
positionDay2.TakeProfit1.Date = baseDate.AddDays(1).AddHours(10); // Closed on Day 2
|
|
|
|
// Act - Calculate snapshot for Day 1 (position opened but not closed yet)
|
|
var day1Positions = new List<Position> { positionDay1 };
|
|
var day1Metrics = TradingBox.CalculatePlatformSummaryMetrics(day1Positions);
|
|
|
|
// Calculate snapshot for Day 2 (position opened and closed)
|
|
var day2Positions = new List<Position> { positionDay2 };
|
|
var day2Metrics = TradingBox.CalculatePlatformSummaryMetrics(day2Positions);
|
|
|
|
// Assert - Day 1: Only opening volume (position still open)
|
|
var expectedDay1Volume = 50000m * 0.1m * 1m; // Open: 5000
|
|
day1Metrics.TotalPlatformVolume.Should().Be(expectedDay1Volume);
|
|
day1Metrics.TotalLifetimePositionCount.Should().Be(1);
|
|
day1Metrics.OpenInterest.Should().Be(expectedDay1Volume); // Position still open
|
|
|
|
// Assert - Day 2: Opening volume + closing volume (position now closed)
|
|
var expectedDay2Volume = 50000m * 0.1m * 1m + 52000m * 0.1m * 1m; // Open: 5000 + Close: 5200 = 10200
|
|
day2Metrics.TotalPlatformVolume.Should().Be(expectedDay2Volume);
|
|
day2Metrics.TotalLifetimePositionCount.Should().Be(1);
|
|
day2Metrics.OpenInterest.Should().Be(0m); // Position now closed
|
|
|
|
// Assert - Volume increased from Day 1 to Day 2 (closing volume added)
|
|
day2Metrics.TotalPlatformVolume.Should().BeGreaterThan(day1Metrics.TotalPlatformVolume);
|
|
var volumeIncrease = day2Metrics.TotalPlatformVolume - day1Metrics.TotalPlatformVolume;
|
|
volumeIncrease.Should().Be(5200m); // The closing volume added on Day 2
|
|
}
|
|
}
|