Add reconcilliation for cancelled position if needed

This commit is contained in:
2025-12-11 18:35:25 +07:00
parent 1426f0b560
commit 65d00c0b9a
3 changed files with 138 additions and 7 deletions

View File

@@ -423,11 +423,135 @@ public class SpotBot : TradingBotBase
return Task.FromResult(false);
}
protected override Task<bool> ReconcileWithBrokerHistory(Position position, Candle currentCandle)
protected override async Task<bool> 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<IExchangeService, List<Position>>(
_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(

View File

@@ -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)
{