From b928eac031084bdebb1b6927f20a5d3fb3734448 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 6 Jan 2026 16:36:02 +0700 Subject: [PATCH] Enhance SpotBot closing price calculation and logging - Implemented logic to calculate broker closing price from PNL when the price is zero or invalid, improving accuracy in trade reconciliation. - Added detailed logging for calculated closing prices, including entry price, PNL, and leverage, to enhance visibility into trading performance. - Updated handling of take profit and stop loss updates based on valid closing prices, ensuring more reliable position management. --- src/Managing.Application/Bots/SpotBot.cs | 130 +++++++++++++----- .../src/plugins/custom/gmx.ts | 35 ++++- 2 files changed, 126 insertions(+), 39 deletions(-) diff --git a/src/Managing.Application/Bots/SpotBot.cs b/src/Managing.Application/Bots/SpotBot.cs index e5171ce8..bb103fe1 100644 --- a/src/Managing.Application/Bots/SpotBot.cs +++ b/src/Managing.Application/Bots/SpotBot.cs @@ -1011,52 +1011,106 @@ public class SpotBot : TradingBotBase if (brokerPosition.Open != null) { var brokerClosingPrice = brokerPosition.Open.Price; - var isProfitable = position.OriginDirection == TradeDirection.Long - ? position.Open.Price < brokerClosingPrice - : position.Open.Price > brokerClosingPrice; - - if (isProfitable) + + // If brokerClosingPrice is 0 or invalid, calculate it from PNL + // This handles cases where very small prices (like PEPE) lose precision + if (brokerClosingPrice <= 0 && brokerPosition.ProfitAndLoss != null && + brokerPosition.ProfitAndLoss.Realized != 0 && position.Open != null) { - if (position.TakeProfit1 != null) + // Calculate closing price from PNL formula + // For LONG: PNL = (exitPrice - entryPrice) * quantity * leverage + // exitPrice = (PNL / (quantity * leverage)) + entryPrice + // For SHORT: PNL = (entryPrice - exitPrice) * quantity * leverage + // exitPrice = entryPrice - (PNL / (quantity * leverage)) + var realizedPnl = brokerPosition.ProfitAndLoss.Realized; + var entryPrice = position.Open.Price; + var quantity = position.Open.Quantity; + var leverage = position.Open.Leverage; + + if (quantity > 0 && leverage > 0) { - position.TakeProfit1.Price = brokerClosingPrice; - position.TakeProfit1.SetDate(brokerPosition.Open.Date); - position.TakeProfit1.SetStatus(TradeStatus.Filled); + var pnlPerUnit = realizedPnl / (quantity * leverage); + + if (position.OriginDirection == TradeDirection.Long) + { + brokerClosingPrice = entryPrice + pnlPerUnit; + } + else // Short + { + brokerClosingPrice = entryPrice - pnlPerUnit; + } + + await LogDebugAsync( + $"⚠️ Broker closing price was 0, calculated from PNL\n" + + $"Position: `{position.Identifier}`\n" + + $"Entry Price: `${entryPrice:F8}`\n" + + $"PNL: `${realizedPnl:F8}`\n" + + $"Quantity: `{quantity:F8}`\n" + + $"Leverage: `{leverage}`\n" + + $"Calculated Closing Price: `${brokerClosingPrice:F8}`"); + } + } + + // Only update TP/SL prices if we have a valid closing price + if (brokerClosingPrice > 0) + { + var isProfitable = position.OriginDirection == TradeDirection.Long + ? position.Open.Price < brokerClosingPrice + : position.Open.Price > brokerClosingPrice; + + if (isProfitable) + { + if (position.TakeProfit1 != null) + { + position.TakeProfit1.Price = brokerClosingPrice; + position.TakeProfit1.SetDate(brokerPosition.Open.Date); + position.TakeProfit1.SetStatus(TradeStatus.Filled); + } + + // Cancel SL trade when TP is hit + if (position.StopLoss != null) + { + position.StopLoss.SetStatus(TradeStatus.Cancelled); + } + } + else + { + if (position.StopLoss != null) + { + position.StopLoss.Price = brokerClosingPrice; + position.StopLoss.SetDate(brokerPosition.Open.Date); + position.StopLoss.SetStatus(TradeStatus.Filled); + } + + // Cancel TP trades when SL is hit + if (position.TakeProfit1 != null) + { + position.TakeProfit1.SetStatus(TradeStatus.Cancelled); + } + + if (position.TakeProfit2 != null) + { + position.TakeProfit2.SetStatus(TradeStatus.Cancelled); + } } - // Cancel SL trade when TP is hit - if (position.StopLoss != null) - { - position.StopLoss.SetStatus(TradeStatus.Cancelled); - } + await LogDebugAsync( + $"📊 Spot Position Reconciliation Complete\n" + + $"Position: `{position.Identifier}`\n" + + $"Closing Price: `${brokerClosingPrice:F8}`\n" + + $"Used: `{(isProfitable ? "Take Profit" : "Stop Loss")}`\n" + + $"PnL from broker: `${position.ProfitAndLoss.Realized:F8}`"); } else { - if (position.StopLoss != null) - { - position.StopLoss.Price = brokerClosingPrice; - position.StopLoss.SetDate(brokerPosition.Open.Date); - position.StopLoss.SetStatus(TradeStatus.Filled); - } - - // Cancel TP trades when SL is hit - if (position.TakeProfit1 != null) - { - position.TakeProfit1.SetStatus(TradeStatus.Cancelled); - } - - if (position.TakeProfit2 != null) - { - position.TakeProfit2.SetStatus(TradeStatus.Cancelled); - } + await LogWarningAsync( + $"⚠️ Cannot update closing trade price - broker price is invalid\n" + + $"Position: `{position.Identifier}`\n" + + $"Broker PNL: `{brokerPosition.ProfitAndLoss?.Realized:F8}`\n" + + $"Entry Price: `${position.Open?.Price:F8}`\n" + + $"Broker Closing Price: `{brokerPosition.Open.Price}`\n" + + $"Will skip updating TP/SL prices to avoid zero-price fills"); } - - await LogDebugAsync( - $"📊 Spot Position Reconciliation Complete\n" + - $"Position: `{position.Identifier}`\n" + - $"Closing Price: `${brokerClosingPrice:F2}`\n" + - $"Used: `{(isProfitable ? "Take Profit" : "Stop Loss")}`\n" + - $"PnL from broker: `${position.ProfitAndLoss.Realized:F2}`"); } return true; // Successfully reconciled, skip candle-based calculation diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index d597cc72..6e843e62 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -1922,7 +1922,40 @@ export const getSpotPositionHistoryImpl = async ( if (quantity > 0) { // Quote per base: input value / base qty (Long: USDC/BTC; Short: USDC/BTC) const numerator = direction === TradeDirection.Short ? outputQty : inputQty; - price = numerator / quantity; + const calculatedPrice = numerator / quantity; + + // For very small prices (like PEPE), ensure we preserve precision + // If calculated price is suspiciously small or zero, try alternative calculation + if (calculatedPrice > 0 && calculatedPrice < 1e-10) { + // For very small prices, try to get price from token data (more accurate) + try { + const tokenAddress = direction === TradeDirection.Short + ? initialToken?.address + : targetToken?.address; + + if (tokenAddress && tokensData[tokenAddress]?.prices?.minPrice) { + // Use GMX's price data directly (more accurate for very small prices) + price = Number(tokensData[tokenAddress].prices.minPrice) / Math.pow(10, 30); + } else { + price = calculatedPrice; // Fallback to calculated + } + } catch { + price = calculatedPrice; // Fallback to calculated + } + } else { + price = calculatedPrice; + } + + // Safety check: if price is still 0 or invalid, log warning + if (price <= 0 || !isFinite(price)) { + console.warn( + `⚠️ Invalid price calculated for spot position: ` + + `ticker=${tickerSymbol}, quantity=${quantity}, ` + + `numerator=${numerator}, calculated=${calculatedPrice}, ` + + `direction=${direction === TradeDirection.Short ? 'Short' : 'Long'}` + ); + // Keep price as 0 - will be handled by SpotBot reconciliation + } } const pnlUsd = (action as any).pnlUsd ? Number((action as any).pnlUsd) / 1e30 : 0;