Add test for dailysnapshot
This commit is contained in:
@@ -51,10 +51,386 @@ public class TradingBoxAgentSummaryMetricsTests
|
|||||||
Assert.Equal(1, metrics.Losses);
|
Assert.Equal(1, metrics.Losses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateAgentSummaryMetrics_WithEmptyPositions_ReturnsZeroMetrics()
|
||||||
|
{
|
||||||
|
var positions = new List<Position>();
|
||||||
|
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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<Position>
|
||||||
|
{
|
||||||
|
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,
|
private static Position CreatePosition(decimal openPrice, decimal quantity, TradeDirection direction,
|
||||||
decimal realizedPnL, decimal netPnL, decimal uiFees, decimal gasFees,
|
decimal realizedPnL, decimal netPnL, decimal uiFees, decimal gasFees,
|
||||||
decimal stopLossPrice, TradeStatus stopLossStatus, decimal takeProfitPrice,
|
decimal stopLossPrice, TradeStatus stopLossStatus, decimal takeProfitPrice,
|
||||||
TradeStatus takeProfitStatus)
|
TradeStatus takeProfitStatus, decimal leverage = 1m)
|
||||||
{
|
{
|
||||||
var position = new Position(
|
var position = new Position(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
@@ -74,11 +450,11 @@ public class TradingBoxAgentSummaryMetricsTests
|
|||||||
user: new User { Id = 1, Name = "tester" });
|
user: new User { Id = 1, Name = "tester" });
|
||||||
|
|
||||||
position.Status = PositionStatus.Finished;
|
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,
|
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,
|
position.TakeProfit1 = BuildTrade(direction == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long,
|
||||||
takeProfitStatus, takeProfitPrice, quantity);
|
takeProfitStatus, takeProfitPrice, quantity, leverage);
|
||||||
position.ProfitAndLoss = new ProfitAndLoss
|
position.ProfitAndLoss = new ProfitAndLoss
|
||||||
{
|
{
|
||||||
Realized = realizedPnL,
|
Realized = realizedPnL,
|
||||||
@@ -90,7 +466,7 @@ public class TradingBoxAgentSummaryMetricsTests
|
|||||||
return position;
|
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(
|
return new Trade(
|
||||||
date: DateTime.UtcNow,
|
date: DateTime.UtcNow,
|
||||||
@@ -100,9 +476,317 @@ public class TradingBoxAgentSummaryMetricsTests
|
|||||||
ticker: Ticker.BTC,
|
ticker: Ticker.BTC,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
price: price,
|
price: price,
|
||||||
leverage: 1m,
|
leverage: leverage,
|
||||||
exchangeOrderId: Guid.NewGuid().ToString(),
|
exchangeOrderId: Guid.NewGuid().ToString(),
|
||||||
message: "unit-trade");
|
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<Position> { 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -849,17 +849,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
.Where(p => p.IsValidForMetrics()).ToList();
|
.Where(p => p.IsValidForMetrics()).ToList();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate statistics using TradingBox helpers
|
// Calculate statistics using TradingBox.CalculateAgentSummaryMetrics
|
||||||
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(positionForMetrics);
|
var agentMetrics = TradingBox.CalculateAgentSummaryMetrics(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 long and short position counts
|
// Calculate long and short position counts
|
||||||
var longPositionCount = positionForMetrics
|
var longPositionCount = positionForMetrics
|
||||||
@@ -880,13 +871,13 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
LastStopTime = _state.State.LastStopTime,
|
LastStopTime = _state.State.LastStopTime,
|
||||||
AccumulatedRunTimeSeconds = _state.State.AccumulatedRunTimeSeconds,
|
AccumulatedRunTimeSeconds = _state.State.AccumulatedRunTimeSeconds,
|
||||||
CreateDate = _state.State.CreateDate,
|
CreateDate = _state.State.CreateDate,
|
||||||
TradeWins = tradeWins,
|
TradeWins = agentMetrics.Wins,
|
||||||
TradeLosses = tradeLosses,
|
TradeLosses = agentMetrics.Losses,
|
||||||
Pnl = pnl, // Gross PnL before fees
|
Pnl = agentMetrics.TotalPnL, // Gross PnL before fees
|
||||||
NetPnL = netPnl, // Net PnL after fees
|
NetPnL = agentMetrics.NetPnL, // Net PnL after fees
|
||||||
Roi = roi,
|
Roi = agentMetrics.TotalROI,
|
||||||
Volume = volume,
|
Volume = agentMetrics.TotalVolume,
|
||||||
Fees = fees,
|
Fees = agentMetrics.TotalFees,
|
||||||
LongPositionCount = longPositionCount,
|
LongPositionCount = longPositionCount,
|
||||||
ShortPositionCount = shortPositionCount
|
ShortPositionCount = shortPositionCount
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -540,12 +540,12 @@ public static class TradingBox
|
|||||||
totalVolume += position.Open.Quantity * position.Open.Price * position.Open.Leverage;
|
totalVolume += position.Open.Quantity * position.Open.Price * position.Open.Leverage;
|
||||||
|
|
||||||
// Add exit volumes from stop loss or take profits if they were executed
|
// 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;
|
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 *
|
totalVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price *
|
||||||
position.TakeProfit1.Leverage;
|
position.TakeProfit1.Leverage;
|
||||||
|
|||||||
Reference in New Issue
Block a user