Enhance TradingBotBase with recovery logic for recently canceled positions and improved error handling for Web3Proxy. Updated CheckPositionInExchangeHistory to return error status, ensuring robust position verification and cancellation processes.

This commit is contained in:
2025-11-12 00:41:39 +07:00
parent 583b35d209
commit 2057c233e5

View File

@@ -400,6 +400,9 @@ public class TradingBotBase : ITradingBot
if (!hasOpenPositions && !hasWaitingSignals) if (!hasOpenPositions && !hasWaitingSignals)
return; return;
// Recovery Logic: Check for recently canceled positions that might need recovery
await RecoverRecentlyCanceledPositions();
// First, process all existing positions that are not finished // First, process all existing positions that are not finished
// Optimized: Inline the filter to avoid LINQ overhead // Optimized: Inline the filter to avoid LINQ overhead
foreach (var position in Positions.Values) foreach (var position in Positions.Values)
@@ -598,9 +601,20 @@ public class TradingBotBase : ITradingBot
$"Checking position history before marking as closed..."); $"Checking position history before marking as closed...");
// Verify in exchange history before assuming it's closed // Verify in exchange history before assuming it's closed
bool existsInHistory = await CheckPositionInExchangeHistory(positionForSignal); var (existsInHistory, hadWeb3ProxyError) = await CheckPositionInExchangeHistory(positionForSignal);
if (existsInHistory) if (hadWeb3ProxyError)
{
// Web3Proxy error - don't assume position is closed, wait for next cycle
await LogWarning(
$"⏳ Web3Proxy Error During Position Verification\n" +
$"Position: `{positionForSignal.Identifier}`\n" +
$"Cannot verify if position is closed\n" +
$"Will retry on next execution cycle");
// Don't change position status, wait for next cycle
return;
}
else if (existsInHistory)
{ {
// Position was actually filled and closed by the exchange // Position was actually filled and closed by the exchange
Logger.LogInformation( Logger.LogInformation(
@@ -767,9 +781,20 @@ public class TradingBotBase : ITradingBot
// Position might be canceled by the broker // Position might be canceled by the broker
// Check if position exists in exchange history with PnL before canceling // Check if position exists in exchange history with PnL before canceling
bool positionFoundInHistory = await CheckPositionInExchangeHistory(positionForSignal); var (positionFoundInHistory, hadWeb3ProxyError) = await CheckPositionInExchangeHistory(positionForSignal);
if (positionFoundInHistory) if (hadWeb3ProxyError)
{
// Web3Proxy error occurred - don't mark as cancelled, wait for next cycle
await LogWarning(
$"⏳ Web3Proxy Error - Skipping Position Cancellation\n" +
$"Position: `{positionForSignal.Identifier}`\n" +
$"Status remains: `{positionForSignal.Status}`\n" +
$"Will retry position verification on next execution cycle");
// Don't change signal status to Expired, let it continue
return;
}
else if (positionFoundInHistory)
{ {
// Position was actually filled and closed, process it properly // Position was actually filled and closed, process it properly
await HandleClosedPosition(positionForSignal); await HandleClosedPosition(positionForSignal);
@@ -2824,12 +2849,12 @@ public class TradingBotBase : ITradingBot
/// </summary> /// </summary>
/// <param name="position">The position to check</param> /// <param name="position">The position to check</param>
/// <returns>True if position found in exchange history with PnL, false otherwise</returns> /// <returns>True if position found in exchange history with PnL, false otherwise</returns>
private async Task<bool> CheckPositionInExchangeHistory(Position position) private async Task<(bool found, bool hadError)> CheckPositionInExchangeHistory(Position position)
{ {
if (Config.IsForBacktest) if (Config.IsForBacktest)
{ {
// For backtests, we don't have exchange history, so return false // For backtests, we don't have exchange history, so return false
return false; return (false, false);
} }
try try
@@ -2864,7 +2889,7 @@ public class TradingBotBase : ITradingBot
$"Direction: `{position.OriginDirection}` (Matched: ✅)\n" + $"Direction: `{position.OriginDirection}` (Matched: ✅)\n" +
$"Exchange PnL: `${recentPosition.ProfitAndLoss.Realized:F2}`\n" + $"Exchange PnL: `${recentPosition.ProfitAndLoss.Realized:F2}`\n" +
$"Position was actually filled and closed"); $"Position was actually filled and closed");
return true; return (true, false);
} }
else else
{ {
@@ -2880,11 +2905,192 @@ public class TradingBotBase : ITradingBot
await LogDebug( await LogDebug(
$"❌ No Position Found in Exchange History\nPosition: `{position.Identifier}`\nPosition was never filled"); $"❌ No Position Found in Exchange History\nPosition: `{position.Identifier}`\nPosition was never filled");
return false; return (false, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error checking position history for position {PositionId}", position.Identifier); Logger.LogError(ex, "Error checking position history for position {PositionId}", position.Identifier);
await LogWarning(
$"⚠️ Web3Proxy Error During Position History Check\n" +
$"Position: `{position.Identifier}`\n" +
$"Error: {ex.Message}\n" +
$"Will retry on next execution cycle");
return (false, true); // found=false, hadError=true
}
}
private async Task RecoverRecentlyCanceledPositions()
{
if (Config.IsForBacktest)
{
// For backtests, we don't have broker positions, so skip recovery
return;
}
try
{
// Get the last (most recent) position from all positions
var lastPosition = Positions.Values.LastOrDefault();
if (lastPosition == null)
{
return; // No positions at all
}
// Only attempt recovery if the last position is cancelled
if (lastPosition.Status != PositionStatus.Canceled)
{
await LogDebug(
$"❌ Recovery Skipped\nLast position `{lastPosition.Identifier}` is not cancelled (Status: {lastPosition.Status})\nNo recovery needed");
return;
}
// Also get count of cancelled positions for logging
var canceledPositionsCount = Positions.Values.Count(p => p.Status == PositionStatus.Canceled);
await LogDebug(
$"🔄 Position Recovery Check\nFound `{canceledPositionsCount}` canceled positions\nLast position `{lastPosition.Identifier}` is cancelled\nAttempting recovery from broker...");
// Get the signal for the last position
if (!Signals.TryGetValue(lastPosition.SignalIdentifier, out var signal))
{
await LogWarning(
$"⚠️ Signal Not Found for Recovery\nPosition: `{lastPosition.Identifier}`\nSignal: `{lastPosition.SignalIdentifier}`\nCannot recover without signal");
return;
}
// Attempt recovery for the last position only
bool recovered = await RecoverOpenPositionFromBroker(signal, lastPosition);
if (recovered)
{
await LogInformation(
$"🎉 Position Recovery Successful\nPosition `{lastPosition.Identifier}` recovered from broker\nStatus restored to Filled\nWill continue normal processing");
}
else
{
await LogDebug(
$"❌ Recovery Not Needed\nPosition `{lastPosition.Identifier}` confirmed canceled\nNo open position found on broker");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error during recently canceled positions recovery");
await LogWarning($"Position recovery check failed due to exception: {ex.Message}");
}
}
private async Task<bool> RecoverOpenPositionFromBroker(LightSignal signal, Position positionForSignal)
{
if (Config.IsForBacktest)
{
// For backtests, we don't have broker positions, so return false
return false;
}
try
{
await LogWarning(
$"🔄 Attempting Position Recovery\n" +
$"Signal: `{signal.Identifier}`\n" +
$"Position: `{positionForSignal.Identifier}`\n" +
$"Direction: `{positionForSignal.OriginDirection}`\n" +
$"Ticker: `{Config.Ticker}`\n" +
$"Checking broker for open position...");
Position brokerPosition = null;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
async exchangeService =>
{
var brokerPositions = await exchangeService.GetBrokerPositions(Account);
brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker);
});
if (brokerPosition != null)
{
// Check if the broker position matches our expected direction
if (brokerPosition.OriginDirection == positionForSignal.OriginDirection)
{
await LogInformation(
$"✅ Position Recovered from Broker\n" +
$"Position: `{positionForSignal.Identifier}`\n" +
$"Direction: `{positionForSignal.OriginDirection}` (Matched: ✅)\n" +
$"Broker Position Size: `{brokerPosition.Open?.Quantity ?? 0}`\n" +
$"Broker Position Price: `${brokerPosition.Open?.Price ?? 0:F2}`\n" +
$"Restoring position status to Filled");
// Update position status back to Filled (from Canceled)
positionForSignal.Status = PositionStatus.Filled;
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
// Update signal status back to PositionOpen since position is recovered
SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen);
// Update PnL from broker position
var brokerNetPnL = brokerPosition.GetPnLBeforeFees();
UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL);
// Update trade details if available
if (positionForSignal.Open != null && brokerPosition.Open != null)
{
positionForSignal.Open.SetStatus(TradeStatus.Filled);
if (brokerPosition.Open.ExchangeOrderId != null)
{
positionForSignal.Open.SetExchangeOrderId(brokerPosition.Open.ExchangeOrderId);
}
}
// Update stop loss and take profit trades if available
if (positionForSignal.StopLoss != null && brokerPosition.StopLoss != null)
{
positionForSignal.StopLoss.SetExchangeOrderId(brokerPosition.StopLoss.ExchangeOrderId);
}
if (positionForSignal.TakeProfit1 != null && brokerPosition.TakeProfit1 != null)
{
positionForSignal.TakeProfit1.SetExchangeOrderId(brokerPosition.TakeProfit1.ExchangeOrderId);
}
if (positionForSignal.TakeProfit2 != null && brokerPosition.TakeProfit2 != null)
{
positionForSignal.TakeProfit2.SetExchangeOrderId(brokerPosition.TakeProfit2.ExchangeOrderId);
}
// Update database
await UpdatePositionDatabase(positionForSignal);
// Notify about position recovery
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionUpdated, positionForSignal);
await LogInformation(
$"🎉 Position Recovery Complete\n" +
$"Position `{positionForSignal.Identifier}` successfully recovered\n" +
$"Status restored to Filled\n" +
$"Database and internal state updated");
return true;
}
else
{
await LogWarning(
$"⚠️ Direction Mismatch During Recovery\n" +
$"Expected: `{positionForSignal.OriginDirection}`\n" +
$"Broker Position: `{brokerPosition.OriginDirection}`\n" +
$"Cannot recover - directions don't match");
}
}
else
{
await LogDebug(
$"❌ No Open Position Found on Broker\n" +
$"Ticker: `{Config.Ticker}`\n" +
$"Position recovery not possible");
}
return false;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error during position recovery for position {PositionId}", positionForSignal.Identifier);
await LogWarning($"Position recovery failed due to exception: {ex.Message}");
return false; return false;
} }
} }