Add test for dailysnapshot
This commit is contained in:
@@ -51,10 +51,386 @@ public class TradingBoxAgentSummaryMetricsTests
|
||||
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,
|
||||
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<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();
|
||||
});
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user