@@ -1,4 +1,5 @@
using Managing.Application.Abstraction s;
using System.Diagnostic s;
using Managing.Application.Abstractions ;
using Managing.Application.Abstractions.Grains ;
using Managing.Application.Abstractions.Services ;
using Managing.Application.Trading.Commands ;
@@ -26,6 +27,7 @@ public class TradingBotBase : ITradingBot
{
public readonly ILogger < TradingBotBase > Logger ;
private readonly IServiceScopeFactory _scopeFactory ;
private const int NEW_POSITION_GRACE_SECONDS = 45 ; // grace window before evaluating missing orders
public TradingBotConfig Config { get ; set ; }
public Account Account { get ; set ; }
@@ -492,8 +494,18 @@ public class TradingBotBase : ITradingBot
}
}
if ( internalPosition . Status = = PositionStatus . New )
if ( internalPosition . Status = = PositionStatus . New )
{
// Grace period: give the broker time to register open orders before we evaluate
var now = Config . IsForBacktest ? ( LastCandle ? . Date ? ? DateTime . UtcNow ) : DateTime . UtcNow ;
var secondsSinceOpenRequest = ( now - positionForSignal . Open . Date ) . TotalSeconds ;
if ( secondsSinceOpenRequest < NEW_POSITION_GRACE_SECONDS )
{
var remaining = NEW_POSITION_GRACE_SECONDS - secondsSinceOpenRequest ;
await LogInformation ( $"⏳ Waiting for broker confirmation\nElapsed: `{secondsSinceOpenRequest:F0}s`\nGrace left: `{remaining:F0}s`" ) ;
return ; // skip early checks until grace period elapses
}
var orders = await ServiceScopeHelpers . WithScopedService < IExchangeService , List < Trade > > ( _scopeFactory ,
async exchangeService = >
{
@@ -609,11 +621,30 @@ public class TradingBotBase : ITradingBot
else
{
await LogWarning (
$"❌ Position Never Filled\nNo position on exchange and no orders\nPosition was never filled and will b e marked as canceled." ) ;
$"❌ Position Never Filled\nNo position on exchange and no orders\nChecking position history befor e marking as canceled." ) ;
// Check if position exists in exchange history with PnL before canceling
bool positionFoundInHistory = await CheckPositionInExchangeHistory ( positionForSignal ) ;
if ( positionFoundInHistory )
{
// Position was actually filled and closed, process it properly
await HandleClosedPosition ( positionForSignal ) ;
await LogInformation (
$"✅ Position Found in Exchange History\n" +
$"Position was actually filled and closed\n" +
$"Processing with HandleClosedPosition" ) ;
}
else
{
// Position was never filled, just mark as canceled without processing PnL
positionForSignal . Status = PositionStatus . Canceled ;
await SetPositionStatus ( signal . Identifier , PositionStatus . Canceled ) ;
await UpdatePositionDatabase ( positionForSignal ) ;
await LogWarning (
$"❌ Position Confirmed Never Filled\nNo position in exchange history\nMarking as canceled without PnL processing" ) ;
}
// Position was never filled (still in New status), so just mark it as canceled
// Don't call HandleClosedPosition as that would incorrectly add volume/PnL
await SetPositionStatus ( signal . Identifier , PositionStatus . Canceled ) ;
SetSignalStatus ( signal . Identifier , SignalStatus . Expired ) ;
}
}
@@ -1346,6 +1377,8 @@ public class TradingBotBase : ITradingBot
// Skip the candle-based PnL calculation since we have actual GMX data
goto SkipCandleBasedCalculation ;
} else {
}
}
@@ -1728,6 +1761,24 @@ public class TradingBotBase : ITradingBot
try
{
var position = Positions . Values . First ( p = > p . SignalIdentifier = = signalIdentifier ) ;
if ( positionStatus . Equals ( PositionStatus . Canceled ) ) {
var stackTrace = new StackTrace ( true ) ;
var callingMethod = stackTrace . GetFrame ( 1 ) ? . GetMethod ( ) ;
var callingMethodName = callingMethod ? . DeclaringType ? . Name + "." + callingMethod ? . Name ;
var exception = new InvalidOperationException ( $"Position {signalIdentifier} is already canceled for User {Account.User.Name}" ) ;
exception . Data [ "SignalIdentifier" ] = signalIdentifier ;
exception . Data [ "PositionId" ] = position . Identifier ;
exception . Data [ "CurrentStatus" ] = position . Status . ToString ( ) ;
exception . Data [ "RequestedStatus" ] = positionStatus . ToString ( ) ;
exception . Data [ "AccountName" ] = Account . Name ;
exception . Data [ "BotName" ] = Config . Name ;
exception . Data [ "CallingMethod" ] = callingMethodName ;
exception . Data [ "CallStack" ] = Environment . StackTrace ;
SentrySdk . CaptureException ( exception ) ;
}
if ( ! position . Status . Equals ( positionStatus ) )
{
Positions . Values . First ( p = > p . SignalIdentifier = = signalIdentifier ) . Status = positionStatus ;
@@ -2480,4 +2531,64 @@ public class TradingBotBase : ITradingBot
Logger . LogError ( ex , "Failed to send notifications: {EventType} for bot {BotId}" , eventType , Identifier ) ;
}
}
/// <summary>
/// Checks if a position exists in the exchange history with PnL data.
/// This helps determine if a position was actually filled and closed on the exchange
/// even if the bot's internal tracking shows it as never filled.
/// </summary>
/// <param name="position">The position to check</param>
/// <returns>True if position found in exchange history with PnL, false otherwise</returns>
private async Task < bool > CheckPositionInExchangeHistory ( Position position )
{
if ( Config . IsForBacktest )
{
// For backtests, we don't have exchange history, so return false
return false ;
}
try
{
Logger . LogDebug (
$"🔍 Checking Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`" ) ;
List < Position > positionHistory = null ;
await ServiceScopeHelpers . WithScopedService < IExchangeService > ( _scopeFactory ,
async exchangeService = >
{
// Get position history from the last 24 hours
var fromDate = DateTime . UtcNow . AddHours ( - 24 ) ;
var toDate = DateTime . UtcNow ;
positionHistory =
await exchangeService . GetPositionHistory ( Account , Config . Ticker , fromDate , toDate ) ;
} ) ;
// Check if there's a recent position with PnL data
if ( positionHistory ! = null & & positionHistory . Any ( ) )
{
var recentPosition = positionHistory
. OrderByDescending ( p = > p . Open ? . Date ? ? DateTime . MinValue )
. FirstOrDefault ( ) ;
if ( recentPosition ! = null & & recentPosition . ProfitAndLoss ! = null )
{
Logger . LogDebug (
$"✅ Position Found in Exchange History\n" +
$"Position: `{position.Identifier}`\n" +
$"Exchange PnL: `${recentPosition.ProfitAndLoss.Realized:F2}`\n" +
$"Position was actually filled and closed" ) ;
return true ;
}
}
Logger . LogDebug (
$"❌ No Position Found in Exchange History\nPosition: `{position.Identifier}`\nPosition was never filled" ) ;
return false ;
}
catch ( Exception ex )
{
Logger . LogError ( ex , "Error checking position history for position {PositionId}" , position . Identifier ) ;
return false ;
}
}
}