From 3f1d102452670bc3ab3cd4a6101c01f67b98c691 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Thu, 8 Jan 2026 05:40:06 +0700 Subject: [PATCH] Enhance SpotBot to verify opening swaps and handle failed transactions - Added logic to confirm the existence of opening swaps in exchange history before marking positions as filled, addressing potential failures in on-chain transactions. - Implemented checks for very low token balances and adjusted position statuses accordingly, ensuring accurate tracking and management of positions. - Improved logging for failed swaps to provide clearer insights into transaction issues and position management. --- src/Managing.Application/Bots/SpotBot.cs | 103 ++++++++++++++++++++--- 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/src/Managing.Application/Bots/SpotBot.cs b/src/Managing.Application/Bots/SpotBot.cs index d1704c2e..38ab72dd 100644 --- a/src/Managing.Application/Bots/SpotBot.cs +++ b/src/Managing.Application/Bots/SpotBot.cs @@ -463,11 +463,43 @@ public class SpotBot : TradingBotBase var isDustAmount = tokenBalanceAmount <= dustThreshold; var balanceIsVeryLow = tokenBalanceAmount < positionQuantity * 0.1m; // Less than 10% of expected - // If balance is dust or very low, check if position was closed in history + // If balance is dust or very low, check if opening swap failed or position was closed if (isDustAmount || balanceIsVeryLow) { if (internalPosition.Status == PositionStatus.Filled) { + // First, check if the opening swap actually succeeded (exists in history) + // If not, the swap likely failed and position should be marked as Canceled + var openingSwapConfirmed = await VerifyOpeningSwapInHistory(internalPosition); + + if (!openingSwapConfirmed) + { + // Opening swap not found in history - likely failed + // Mark position as Canceled since it never actually opened + var previousStatus = internalPosition.Status; + internalPosition.Status = PositionStatus.Canceled; + internalPosition.Open.SetStatus(TradeStatus.Cancelled); + positionForSignal.Open.SetStatus(TradeStatus.Cancelled); + await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Canceled); + await UpdatePositionInDatabaseAsync(internalPosition); + + await LogWarningAsync( + $"❌ Position Opening Failed - Swap Not Found in History\n" + + $"Position: `{internalPosition.Identifier}`\n" + + $"Signal: `{internalPosition.SignalIdentifier}`\n" + + $"Ticker: {Config.Ticker}\n" + + $"Expected Quantity: `{positionQuantity:F5}`\n" + + $"Token Balance: `{tokenBalanceAmount:F5}` (very low)\n" + + $"Status Changed: `{previousStatus}` → `Canceled`\n" + + $"The opening swap (USDC → {Config.Ticker}) was not found in exchange history\n" + + $"This indicates the swap failed and position never opened\n" + + $"Position will not be tracked or managed"); + + await NotifyAgentAndPlatformAsync(NotificationEventType.PositionClosed, internalPosition); + return; // Exit - position failed to open + } + + // Opening swap exists, so position did open - check if it was closed var (positionFoundInHistory, hadWeb3ProxyError) = await CheckSpotPositionInExchangeHistory(internalPosition); @@ -531,25 +563,68 @@ public class SpotBot : TradingBotBase } } - // Token balance exists - verify the opening swap actually succeeded in history before marking as Filled + // Token balance exists - ALWAYS verify the opening swap actually succeeded in history + // This is critical because Web3Proxy may return success when transaction is sent, + // but the swap might fail on-chain. We must verify it actually executed. var previousPositionStatus = internalPosition.Status; - // Only check history if position is not already Filled - if (internalPosition.Status != PositionStatus.Filled) + // Always verify the opening swap exists in history, even if position is already marked as Filled + // This catches cases where the position was prematurely marked as Filled by the command handler + bool swapConfirmedInHistory = await VerifyOpeningSwapInHistory(internalPosition); + + if (!swapConfirmedInHistory) { - // Verify the opening swap actually executed successfully - bool swapConfirmedInHistory = await VerifyOpeningSwapInHistory(internalPosition); + // Swap not found in history - check if this is a failed swap or just delayed + // If balance is very low, the swap likely failed + var dustThreshold = Config.Ticker == Ticker.ETH + ? 0.01m // ETH: 0.01 ETH is likely gas reserve or dust + : 0.0001m; // Other tokens: very small amount is dust - if (!swapConfirmedInHistory) + var isDustAmount = tokenBalanceAmount <= dustThreshold; + var balanceIsVeryLow = tokenBalanceAmount < positionQuantity * 0.1m; // Less than 10% of expected + + if (isDustAmount || balanceIsVeryLow) { - await LogDebugAsync( - $"⏳ Opening Swap Not Yet Confirmed in History\n" + + // Balance is very low and swap not in history - swap likely failed + // Mark position as Canceled since swap failed on-chain + var previousStatus = internalPosition.Status; + internalPosition.Status = PositionStatus.Canceled; + internalPosition.Open.SetStatus(TradeStatus.Cancelled); + positionForSignal.Open.SetStatus(TradeStatus.Cancelled); + await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Canceled); + await UpdatePositionInDatabaseAsync(internalPosition); + + await LogWarningAsync( + $"❌ Position Opening Failed - Swap Not Found in History\n" + $"Position: `{internalPosition.Identifier}`\n" + - $"Token Balance: `{tokenBalanceAmount:F5}`\n" + - $"Status: `{internalPosition.Status}`\n" + - $"Waiting for swap to appear in exchange history...\n" + - $"Will retry on next cycle"); - return; // Don't mark as Filled yet, wait for history confirmation + $"Signal: `{internalPosition.SignalIdentifier}`\n" + + $"Ticker: {Config.Ticker}\n" + + $"Expected Quantity: `{positionQuantity:F5}`\n" + + $"Token Balance: `{tokenBalanceAmount:F5}` (very low)\n" + + $"Status Changed: `{previousStatus}` → `Canceled`\n" + + $"The opening swap (USDC → {Config.Ticker}) was not found in exchange history\n" + + $"This indicates the swap transaction failed on-chain even though it was sent\n" + + $"Position will not be tracked or managed"); + + await NotifyAgentAndPlatformAsync(NotificationEventType.PositionClosed, internalPosition); + return; // Exit - swap failed + } + else + { + // Balance exists but swap not yet in history - might be delayed + // Only wait if position is not already Filled (to avoid repeated checks) + if (internalPosition.Status != PositionStatus.Filled) + { + await LogDebugAsync( + $"⏳ Opening Swap Not Yet Confirmed in History\n" + + $"Position: `{internalPosition.Identifier}`\n" + + $"Token Balance: `{tokenBalanceAmount:F5}`\n" + + $"Status: `{internalPosition.Status}`\n" + + $"Waiting for swap to appear in exchange history...\n" + + $"Will retry on next cycle"); + return; // Don't mark as Filled yet, wait for history confirmation + } + // If already Filled, continue - might be a timing issue with history indexing } }