From a3d6dd1238a79a3a712f527d66ae1f78236d38f3 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 10 Oct 2025 01:35:10 +0700 Subject: [PATCH] Remove warning for backtest when signal is expired --- .../Accounts/AccountService.cs | 2 +- .../Bots/TradingBotBase.cs | 61 ++++++++++++++++--- .../Handlers/ClosePositionCommandHandler.cs | 24 +++++--- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/Managing.Application/Accounts/AccountService.cs b/src/Managing.Application/Accounts/AccountService.cs index d83a9634..7e11d0ee 100644 --- a/src/Managing.Application/Accounts/AccountService.cs +++ b/src/Managing.Application/Accounts/AccountService.cs @@ -160,7 +160,7 @@ public class AccountService : IAccountService await ManagePropertiesAsync(hideSecrets, getBalance, account); - if (account.User != null) + if (account.User == null) { account.User = await _userRepository.GetUserByNameAsync(account.User.Name); } diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 42085a7b..065d2490 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -329,9 +329,10 @@ public class TradingBotBase : ITradingBot } catch (Exception ex) { - await LogWarning($"Failed to update finished position {position.Identifier} in database: {ex.Message}"); + await LogWarning( + $"Failed to update finished position {position.Identifier} in database: {ex.Message}"); + } } - } // Then, open positions for signals waiting for a position open // But first, check if we already have a position for any of these signals @@ -1294,7 +1295,7 @@ public class TradingBotBase : ITradingBot // Update the closing trade price if available if (gmxPosition.Open != null) { - var closingPrice = gmxPosition.Open.Price; + var gmxClosingPrice = gmxPosition.Open.Price; // Determine which trade was the closing trade based on profitability bool isProfitable = position.ProfitAndLoss.Realized > 0; @@ -1303,7 +1304,7 @@ public class TradingBotBase : ITradingBot { if (position.TakeProfit1 != null) { - position.TakeProfit1.SetPrice(closingPrice, 2); + position.TakeProfit1.SetPrice(gmxClosingPrice, 2); position.TakeProfit1.SetDate(gmxPosition.Open.Date); position.TakeProfit1.SetStatus(TradeStatus.Filled); } @@ -1318,7 +1319,7 @@ public class TradingBotBase : ITradingBot { if (position.StopLoss != null) { - position.StopLoss.SetPrice(closingPrice, 2); + position.StopLoss.SetPrice(gmxClosingPrice, 2); position.StopLoss.SetDate(gmxPosition.Open.Date); position.StopLoss.SetStatus(TradeStatus.Filled); } @@ -1338,7 +1339,7 @@ public class TradingBotBase : ITradingBot Logger.LogInformation( $"📊 Position Reconciliation Complete\n" + $"Position: `{position.Identifier}`\n" + - $"Closing Price: `${closingPrice:F2}`\n" + + $"Closing Price: `${gmxClosingPrice:F2}`\n" + $"Used: `{(isProfitable ? "Take Profit" : "Stop Loss")}`\n" + $"PnL from GMX: `${position.ProfitAndLoss.Realized:F2}`"); } @@ -1359,6 +1360,10 @@ public class TradingBotBase : ITradingBot } } + // Calculate P&L for backtests even if currentCandle is null + decimal closingPrice = 0; + bool pnlCalculated = false; + if (currentCandle != null) { List recentCandles = null; @@ -1428,8 +1433,6 @@ public class TradingBotBase : ITradingBot wasTakeProfitHit = minPriceRecent <= position.TakeProfit1.Price; } - decimal closingPrice; - if (wasStopLossHit) { // Use actual execution price based on direction @@ -1534,6 +1537,40 @@ public class TradingBotBase : ITradingBot $"Closing at market price: `${closingPrice:F2}`"); } + pnlCalculated = true; + } + else if (Config.IsForBacktest) + { + // For backtests when currentCandle is null, use a fallback closing price + // This ensures P&L calculation always happens for backtests + Logger.LogWarning( + $"⚠️ Backtest: No current candle available for position {position.Identifier}. Using fallback closing price calculation."); + + // Use the position's stop loss or take profit price as closing price + if (position.StopLoss != null && position.StopLoss.Price > 0) + { + closingPrice = position.StopLoss.Price; + position.StopLoss.SetStatus(TradeStatus.Filled); + } + else if (position.TakeProfit1 != null && position.TakeProfit1.Price > 0) + { + closingPrice = position.TakeProfit1.Price; + position.TakeProfit1.SetStatus(TradeStatus.Filled); + } + else + { + // Last resort: use entry price (no profit/loss) + closingPrice = position.Open.Price; + Logger.LogWarning( + $"⚠️ Backtest: Using entry price as closing price for position {position.Identifier}"); + } + + pnlCalculated = true; + } + + // Calculate P&L if we have a closing price + if (pnlCalculated && closingPrice > 0) + { var entryPrice = position.Open.Price; var positionSize = position.Open.Quantity * position.Open.Leverage; @@ -1553,7 +1590,7 @@ public class TradingBotBase : ITradingBot var netPnl = pnl - totalFees; position.ProfitAndLoss = new ProfitAndLoss { Realized = pnl, Net = netPnl }; } - else if (position.ProfitAndLoss.Realized == 0) + else if (position.ProfitAndLoss.Realized == 0 || position.ProfitAndLoss.Net == 0) { var totalFees = position.GasFees + position.UiFees; var netPnl = pnl - totalFees; @@ -1561,6 +1598,12 @@ public class TradingBotBase : ITradingBot position.ProfitAndLoss.Net = netPnl; } + Logger.LogInformation( + $"💰 P&L Calculated for Position {position.Identifier}\n" + + $"Entry: `${entryPrice:F2}` | Exit: `${closingPrice:F2}`\n" + + $"Realized P&L: `${pnl:F2}` | Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`\n" + + $"Total Fees: `${position.GasFees + position.UiFees:F2}`"); + // Fees are now tracked separately in UiFees and GasFees properties // No need to subtract fees from PnL as they're tracked separately } diff --git a/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs b/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs index adf91655..f595e277 100644 --- a/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs +++ b/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs @@ -1,6 +1,8 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Trading.Commands; +using Managing.Common; +using Managing.Domain.Accounts; using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; using Microsoft.Extensions.DependencyInjection; @@ -21,12 +23,15 @@ public class ClosePositionCommandHandler( { try { - // Get Trade - var account = await accountService.GetAccountById(request.AccountId, false, false); - if (request.Position == null) + Account account = null; + if (!request.IsForBacktest) { - _ = await exchangeService.CancelOrder(account, request.Position.Ticker); - return request.Position; + account = await accountService.GetAccountById(request.AccountId, false, false); + if (request.Position == null) + { + _ = await exchangeService.CancelOrder(account, request.Position.Ticker); + return request.Position; + } } var isForPaperTrading = request.IsForBacktest; @@ -48,12 +53,14 @@ public class ClosePositionCommandHandler( request.Position.ProfitAndLoss = TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice, request.Position.Open.Leverage); - + // Add UI fees for closing the position (broker closed it) - var closingPositionSizeUsd = (lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage; + var closingPositionSizeUsd = + (lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage; var closingUiFees = TradingHelpers.CalculateClosingUiFees(closingPositionSizeUsd); request.Position.AddUiFees(closingUiFees); - + request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction); + await tradingService.UpdatePositionAsync(request.Position); return request.Position; } @@ -78,6 +85,7 @@ public class ClosePositionCommandHandler( var closingPositionSizeUsd = (lastPrice * closedPosition.Quantity) * request.Position.Open.Leverage; var closingUiFees = TradingHelpers.CalculateClosingUiFees(closingPositionSizeUsd); request.Position.AddUiFees(closingUiFees); + request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction); if (!request.IsForBacktest) await tradingService.UpdatePositionAsync(request.Position);