From 65d00c0b9afa21d7d8da7575a97fa215ed8e32a4 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Thu, 11 Dec 2025 18:35:25 +0700 Subject: [PATCH] Add reconcilliation for cancelled position if needed --- src/Managing.Application/Bots/SpotBot.cs | 132 +++++++++++++++++- .../Bots/TradingBotBase.cs | 2 +- .../plugins/get-spot-position-history.test.ts | 11 +- 3 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/Managing.Application/Bots/SpotBot.cs b/src/Managing.Application/Bots/SpotBot.cs index f7fdc658..67b35ba8 100644 --- a/src/Managing.Application/Bots/SpotBot.cs +++ b/src/Managing.Application/Bots/SpotBot.cs @@ -423,11 +423,135 @@ public class SpotBot : TradingBotBase return Task.FromResult(false); } - protected override Task ReconcileWithBrokerHistory(Position position, Candle currentCandle) + protected override async Task ReconcileWithBrokerHistory(Position position, Candle currentCandle) { - // Spot trading doesn't have broker position history like futures - // Return false to continue with candle-based calculation - return Task.FromResult(false); + // Spot-specific: reconcile with spot position history + try + { + await LogDebugAsync( + $"šŸ” Fetching Spot Position History\nPosition: `{position.Identifier}`\nTicker: `{Config.Ticker}`"); + + var positionHistory = await ServiceScopeHelpers.WithScopedService>( + _scopeFactory, + async exchangeService => + { + // Get position history from the last 24 hours for better coverage + var fromDate = DateTime.UtcNow.AddHours(-24); + var toDate = DateTime.UtcNow; + return await exchangeService.GetSpotPositionHistory(Account, Config.Ticker, fromDate, toDate); + }); + + // Find the most recent position in history + if (positionHistory != null && positionHistory.Any()) + { + // Get the most recent position from spot history (ordered by date) + var brokerPosition = positionHistory + .OrderByDescending(p => p.Open?.Date ?? p.Date) + .FirstOrDefault(); + + // For spot trading, SHORT direction means the spot was sold/closed + // We need to verify the last position is SHORT to confirm the position was correctly closed + if (brokerPosition != null && brokerPosition.OriginDirection == TradeDirection.Short) + { + if (brokerPosition.ProfitAndLoss != null) + { + await LogDebugAsync( + $"āœ… Spot Position History Found\n" + + $"Position: `{position.Identifier}`\n" + + $"Last Position Direction: `{brokerPosition.OriginDirection}` (SHORT = Sold/Closed) āœ…\n" + + $"Realized PnL (after fees): `${brokerPosition.ProfitAndLoss.Realized:F2}`\n" + + $"Bot's UI Fees: `${position.UiFees:F2}`\n" + + $"Bot's Gas Fees: `${position.GasFees:F2}`"); + + // Use the actual spot PnL data from broker history + // For spot, fees are simpler (no leverage, no closing UI fees) + var totalBotFees = position.GasFees + position.UiFees; + var brokerRealizedPnl = brokerPosition.ProfitAndLoss.Realized; + + position.ProfitAndLoss = new ProfitAndLoss + { + Realized = brokerRealizedPnl, + Net = brokerRealizedPnl - totalBotFees + }; + + // Update the closing trade price if available + 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 (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); + } + } + + 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 + } + } + else if (brokerPosition != null) + { + await LogDebugAsync( + $"āš ļø Last Position Not SHORT\n" + + $"Position: `{position.Identifier}`\n" + + $"Last Position Direction: `{brokerPosition.OriginDirection}`\n" + + $"Expected SHORT to confirm spot was sold/closed\n" + + $"Will continue with candle-based calculation"); + } + } + + return false; // No matching position found or not reconciled, continue with candle-based calculation + } + catch (Exception ex) + { + Logger.LogError(ex, "Error reconciling spot position with broker history for position {PositionId}", position.Identifier); + await LogWarningAsync( + $"āš ļø Error During Spot Position History Reconciliation\n" + + $"Position: `{position.Identifier}`\n" + + $"Error: {ex.Message}\n" + + $"Will continue with candle-based calculation"); + return false; // On error, continue with candle-based calculation + } } protected override Task<(decimal closingPrice, bool pnlCalculated)> CalculatePositionClosingFromCandles( diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index f4fd65c3..8545f436 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -1175,7 +1175,7 @@ public abstract class TradingBotBase : ITradingBot { Candle currentCandle = await GetCurrentCandleForPositionClose(Account, Config.Ticker.ToString()); - // Try broker history reconciliation first (futures-specific) + // Try broker history reconciliation first var brokerHistoryReconciled = await ReconcileWithBrokerHistory(position, currentCandle); if (brokerHistoryReconciled && !forceMarketClose) { diff --git a/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts b/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts index f4c45c0d..fb6fbc53 100644 --- a/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts @@ -7,13 +7,20 @@ test('GMX get spot position history - Market swaps', async (t) => { await t.test('should get spot swap executions', async () => { const sdk = await getClientForAddress('0x932167388dD9aad41149b3cA23eBD489E2E2DD78') + // Get today's date range (start and end of today) + const today = new Date() + const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0) + const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999) + const fromDateTime = startOfDay.toISOString() + const toDateTime = endOfDay.toISOString() + const result = await getSpotPositionHistoryImpl( sdk, 0, // pageIndex 100, // pageSize Ticker.BTC, // ticker - '2025-12-09T00:00:00.000Z', // fromDateTime - '2025-12-11T00:00:00.000Z' // toDateTime + fromDateTime, // fromDateTime (today's start) + toDateTime // toDateTime (today's end) ) console.log('\nšŸ“Š Spot Swap History Summary:')