Implement orphaned position recovery in SpotBot; enhance logging for recovery attempts and failures, and ensure position synchronization with token balance checks.
This commit is contained in:
@@ -182,14 +182,24 @@ public class SpotBot : TradingBotBase
|
||||
|
||||
if (tokenBalance is { Value: > 1m })
|
||||
{
|
||||
// We have a token balance but no internal position - orphaned position
|
||||
// We have a token balance but no internal position - attempt to recover orphaned position
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Orphaned Token Balance Detected\n" +
|
||||
$"Ticker: {Config.Ticker}\n" +
|
||||
$"Token balance: `{tokenBalance.Amount:F5}`\n" +
|
||||
$"But no internal position tracked\n" +
|
||||
$"This may require manual cleanup");
|
||||
return false; // Don't allow opening new position until resolved
|
||||
$"Attempting to recover position from database...");
|
||||
|
||||
var recovered = await TryRecoverOrphanedPosition(tokenBalance.Amount);
|
||||
if (!recovered)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"❌ Position Recovery Failed\n" +
|
||||
$"Could not recover orphaned position\n" +
|
||||
$"Manual cleanup may be required");
|
||||
}
|
||||
|
||||
return false; // Don't allow opening new position until next cycle
|
||||
}
|
||||
|
||||
// No position and no balance - safe to open
|
||||
@@ -202,6 +212,135 @@ public class SpotBot : TradingBotBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryRecoverOrphanedPosition(decimal tokenBalance)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all positions for this bot from database
|
||||
var allPositions = await ServiceScopeHelpers.WithScopedService<ITradingService, IEnumerable<Position>>(
|
||||
_scopeFactory,
|
||||
async tradingService => await tradingService.GetPositionsByInitiatorIdentifierAsync(Identifier));
|
||||
|
||||
// Find the last position for this account and ticker (regardless of status)
|
||||
// The orphaned token balance indicates this position should actually be open
|
||||
var lastPosition = allPositions
|
||||
.Where(p => p.AccountId == Account.Id &&
|
||||
p.Ticker == Config.Ticker)
|
||||
.OrderByDescending(p => p.Date)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (lastPosition == null)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"🔍 No Recoverable Position Found\n" +
|
||||
$"No unfinished position found in database for this ticker\n" +
|
||||
$"Token balance: `{tokenBalance:F5}` may be from external source");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that the Open trade exists
|
||||
if (lastPosition.Open == null)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Invalid Position Data\n" +
|
||||
$"Position `{lastPosition.Identifier}` has no Open trade\n" +
|
||||
$"Cannot recover position");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that the token balance matches the position quantity with tolerance
|
||||
var positionQuantity = lastPosition.Open.Quantity;
|
||||
if (positionQuantity == 0)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Invalid Position Data\n" +
|
||||
$"Position `{lastPosition.Identifier}` has zero quantity\n" +
|
||||
$"Cannot recover position");
|
||||
return false;
|
||||
}
|
||||
|
||||
var tolerance = positionQuantity * 0.006m; // 0.6% tolerance for slippage
|
||||
var difference = Math.Abs(tokenBalance - positionQuantity);
|
||||
|
||||
if (difference > tolerance)
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Token Balance Mismatch\n" +
|
||||
$"Position: `{lastPosition.Identifier}`\n" +
|
||||
$"Position Quantity: `{positionQuantity:F5}`\n" +
|
||||
$"Token Balance: `{tokenBalance:F5}`\n" +
|
||||
$"Difference: `{difference:F5}` exceeds tolerance `{tolerance:F5}`\n" +
|
||||
$"Cannot recover - amounts don't match");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recover the position by setting status to Filled
|
||||
var previousStatus = lastPosition.Status;
|
||||
lastPosition.Status = PositionStatus.Filled;
|
||||
lastPosition.Open.SetStatus(TradeStatus.Filled);
|
||||
|
||||
// Update quantity to match actual token balance
|
||||
lastPosition.Open.Quantity = tokenBalance;
|
||||
|
||||
// Calculate current PnL
|
||||
var currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(
|
||||
_scopeFactory,
|
||||
async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker));
|
||||
|
||||
if (currentPrice > 0)
|
||||
{
|
||||
var openPrice = lastPosition.Open.Price;
|
||||
var pnlBeforeFees = TradingBox.CalculatePnL(openPrice, currentPrice, tokenBalance, 1,
|
||||
TradeDirection.Long);
|
||||
|
||||
var totalFees = lastPosition.GasFees + lastPosition.UiFees;
|
||||
var netPnl = pnlBeforeFees - totalFees;
|
||||
|
||||
lastPosition.ProfitAndLoss = new ProfitAndLoss
|
||||
{
|
||||
Realized = pnlBeforeFees,
|
||||
Net = netPnl
|
||||
};
|
||||
}
|
||||
|
||||
// Update position in database
|
||||
await UpdatePositionInDatabaseAsync(lastPosition);
|
||||
|
||||
// Add to internal positions collection
|
||||
Positions[lastPosition.Identifier] = lastPosition;
|
||||
|
||||
// Update signal status if signal already exists
|
||||
if (Signals.ContainsKey(lastPosition.SignalIdentifier))
|
||||
{
|
||||
SetSignalStatus(lastPosition.SignalIdentifier, SignalStatus.PositionOpen);
|
||||
}
|
||||
|
||||
await LogWarningAsync(
|
||||
$"✅ Orphaned Position Recovered\n" +
|
||||
$"Position: `{lastPosition.Identifier}`\n" +
|
||||
$"Signal: `{lastPosition.SignalIdentifier}`\n" +
|
||||
$"Ticker: {Config.Ticker}\n" +
|
||||
$"Previous Status: `{previousStatus}` → `Filled`\n" +
|
||||
$"Token Balance: `{tokenBalance:F5}`\n" +
|
||||
$"Open Price: `${lastPosition.Open.Price:F2}`\n" +
|
||||
$"Open Date: `{lastPosition.Open.Date}`\n" +
|
||||
$"Current Price: `${currentPrice:F2}`\n" +
|
||||
$"Current PnL: `${lastPosition.ProfitAndLoss?.Realized ?? 0:F2}`\n" +
|
||||
$"Position is now being tracked and will be managed on next cycle");
|
||||
|
||||
// Send notification
|
||||
await NotifyAgentAndPlatformAsync(NotificationEventType.PositionOpened, lastPosition);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error recovering orphaned position");
|
||||
await LogWarningAsync($"❌ Position Recovery Error\n{ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task LoadAccountAsync()
|
||||
{
|
||||
// Live trading: load real account from database
|
||||
|
||||
Reference in New Issue
Block a user