Fix pnl calculation when force closed

This commit is contained in:
2025-11-03 19:26:48 +07:00
parent b8c6f05805
commit 60035ca299
2 changed files with 54 additions and 10 deletions

View File

@@ -28,9 +28,6 @@ namespace Managing.Application.Abstractions
Task LoadLastCandle(); Task LoadLastCandle();
Task<LightSignal> CreateManualSignal(TradeDirection direction); Task<LightSignal> CreateManualSignal(TradeDirection direction);
Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,
bool tradeClosingPosition = false);
/// <summary> /// <summary>
/// Gets the current trading bot configuration. /// Gets the current trading bot configuration.
/// </summary> /// </summary>

View File

@@ -760,7 +760,8 @@ public class TradingBotBase : ITradingBot
await LogInformation( await LogInformation(
$"⏰ Time Limit Close\nClosing position due to time limit: `{Config.MaxPositionTimeHours}h` exceeded\n📈 Position Status: {profitStatus}\n💰 Entry: `${positionForSignal.Open.Price}` → Current: `${lastCandle.Close}`\n📊 Realized PNL: `${currentPnl:F2}` (`{pnlPercentage:F2}%`)"); $"⏰ Time Limit Close\nClosing position due to time limit: `{Config.MaxPositionTimeHours}h` exceeded\n📈 Position Status: {profitStatus}\n💰 Entry: `${positionForSignal.Open.Price}` → Current: `${lastCandle.Close}`\n📊 Realized PNL: `${currentPnl:F2}` (`{pnlPercentage:F2}%`)");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true); // Force a market close: compute PnL based on current price instead of SL/TP
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true, true);
return; return;
} }
} }
@@ -1294,7 +1295,7 @@ public class TradingBotBase : ITradingBot
} }
public async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, public async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,
bool tradeClosingPosition = false) bool tradeClosingPosition = false, bool forceMarketClose = false)
{ {
await LogInformation( await LogInformation(
$"🔧 Closing {position.OriginDirection} Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\n📋 Type: `{tradeToClose.TradeType}`\n📊 Quantity: `{tradeToClose.Quantity:F5}`"); $"🔧 Closing {position.OriginDirection} Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\n📋 Type: `{tradeToClose.TradeType}`\n📊 Quantity: `{tradeToClose.Quantity:F5}`");
@@ -1315,7 +1316,7 @@ public class TradingBotBase : ITradingBot
if (!Config.IsForBacktest && quantity == 0) if (!Config.IsForBacktest && quantity == 0)
{ {
await LogDebug($"✅ Trade already closed on exchange for position: `{position.Identifier}`"); await LogDebug($"✅ Trade already closed on exchange for position: `{position.Identifier}`");
await HandleClosedPosition(position); await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose);
} }
else else
{ {
@@ -1340,7 +1341,7 @@ public class TradingBotBase : ITradingBot
await SetPositionStatus(signal.Identifier, PositionStatus.Finished); await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
} }
await HandleClosedPosition(closedPosition); await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose);
} }
else else
{ {
@@ -1356,13 +1357,13 @@ public class TradingBotBase : ITradingBot
// Trade close on exchange => Should close trade manually // Trade close on exchange => Should close trade manually
await SetPositionStatus(signal.Identifier, PositionStatus.Finished); await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
// Ensure trade dates are properly updated even for canceled/rejected positions // Ensure trade dates are properly updated even for canceled/rejected positions
await HandleClosedPosition(position); await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose);
} }
} }
} }
} }
private async Task HandleClosedPosition(Position position) private async Task HandleClosedPosition(Position position, decimal? forcedClosingPrice = null, bool forceMarketClose = false)
{ {
if (Positions.ContainsKey(position.Identifier)) if (Positions.ContainsKey(position.Identifier))
{ {
@@ -1375,7 +1376,7 @@ public class TradingBotBase : ITradingBot
}); });
// For live trading on GMX, fetch the actual position history to get real PnL data // For live trading on GMX, fetch the actual position history to get real PnL data
if (!Config.IsForBacktest) if (!Config.IsForBacktest && !forceMarketClose)
{ {
try try
{ {
@@ -1503,6 +1504,52 @@ public class TradingBotBase : ITradingBot
decimal closingPrice = 0; decimal closingPrice = 0;
bool pnlCalculated = false; bool pnlCalculated = false;
// If we are forcing a market close (e.g., time limit), use the provided closing price
if (forceMarketClose && forcedClosingPrice.HasValue)
{
closingPrice = forcedClosingPrice.Value;
bool isManualCloseProfitable = position.OriginDirection == TradeDirection.Long
? closingPrice > position.Open.Price
: closingPrice < position.Open.Price;
if (isManualCloseProfitable)
{
if (position.TakeProfit1 != null)
{
position.TakeProfit1.Price = closingPrice;
position.TakeProfit1.SetDate(currentCandle?.Date ?? DateTime.UtcNow);
position.TakeProfit1.SetStatus(TradeStatus.Filled);
}
if (position.StopLoss != null)
{
position.StopLoss.SetStatus(TradeStatus.Cancelled);
}
}
else
{
if (position.StopLoss != null)
{
position.StopLoss.Price = closingPrice;
position.StopLoss.SetDate(currentCandle?.Date ?? DateTime.UtcNow);
position.StopLoss.SetStatus(TradeStatus.Filled);
}
if (position.TakeProfit1 != null)
{
position.TakeProfit1.SetStatus(TradeStatus.Cancelled);
}
if (position.TakeProfit2 != null)
{
position.TakeProfit2.SetStatus(TradeStatus.Cancelled);
}
}
pnlCalculated = true;
}
if (currentCandle != null) if (currentCandle != null)
{ {
List<Candle> recentCandles = null; List<Candle> recentCandles = null;