diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index 77ede2fe..88749b73 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -28,9 +28,6 @@ namespace Managing.Application.Abstractions Task LoadLastCandle(); Task CreateManualSignal(TradeDirection direction); - Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, - bool tradeClosingPosition = false); - /// /// Gets the current trading bot configuration. /// diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index f4b8c56f..073fe3fb 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -760,7 +760,8 @@ public class TradingBotBase : ITradingBot 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}%`)"); - 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; } } @@ -1294,7 +1295,7 @@ public class TradingBotBase : ITradingBot } public async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, - bool tradeClosingPosition = false) + bool tradeClosingPosition = false, bool forceMarketClose = false) { await LogInformation( $"šŸ”§ 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) { await LogDebug($"āœ… Trade already closed on exchange for position: `{position.Identifier}`"); - await HandleClosedPosition(position); + await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); } else { @@ -1340,7 +1341,7 @@ public class TradingBotBase : ITradingBot await SetPositionStatus(signal.Identifier, PositionStatus.Finished); } - await HandleClosedPosition(closedPosition); + await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); } else { @@ -1356,13 +1357,13 @@ public class TradingBotBase : ITradingBot // Trade close on exchange => Should close trade manually await SetPositionStatus(signal.Identifier, PositionStatus.Finished); // 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)) { @@ -1375,7 +1376,7 @@ public class TradingBotBase : ITradingBot }); // For live trading on GMX, fetch the actual position history to get real PnL data - if (!Config.IsForBacktest) + if (!Config.IsForBacktest && !forceMarketClose) { try { @@ -1503,6 +1504,52 @@ public class TradingBotBase : ITradingBot decimal closingPrice = 0; 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) { List recentCandles = null;