From 47bea1b9b7b7a76358dbd16d1491817c5d8f5039 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 23 Nov 2025 23:31:34 +0700 Subject: [PATCH] Update closing position on BotStop --- .../Bots/Grains/LiveTradingBotGrain.cs | 42 +--------- .../ManageBot/StopBotCommandHandler.cs | 77 ++++++++++++++++++- 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index f936f98b..013ac583 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -317,44 +317,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable private async Task StopAsyncInternal(bool isRestarting, string? reason = null) { - // Only check for open positions if this is not part of a restart operation - if (!isRestarting) - { - await CloseAllOpenPositionsAsync(); - - // Verify positions are actually closed (even if CloseAllOpenPositionsAsync had timeouts/exceptions) - // This ensures we don't report failure if positions were successfully closed despite timeouts - try - { - var botId = this.GetPrimaryKey(); - var positions = await ServiceScopeHelpers.WithScopedService>( - _scopeFactory, - async tradingService => await tradingService.GetPositionsByInitiatorIdentifierAsync(botId)); - - var stillOpenPositions = - positions?.Where(p => p.IsOpen() || p.Status.Equals(PositionStatus.New)).ToList() ?? - new List(); - - if (stillOpenPositions.Any()) - { - _logger.LogWarning( - "Bot {GrainId} still has {Count} open positions after closure attempt: {Positions}", - botId, stillOpenPositions.Count, - string.Join(", ", stillOpenPositions.Select(p => p.Identifier))); - } - else - { - _logger.LogInformation("Bot {GrainId} - all positions verified as closed", botId); - } - } - catch (Exception ex) - { - // Don't fail the stop operation if we can't verify positions - _logger.LogWarning(ex, - "Could not verify position closure status for bot {GrainId}, continuing with stop", - this.GetPrimaryKey()); - } - } + // Note: Position closing is now handled outside the grain in StopBotCommandHandler + // to avoid Orleans timeout. This method only handles fast grain operations. // The check is now against the registry status var botRegistry = GrainFactory.GetGrain(0); @@ -607,7 +571,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable if (_state.State.Config.IsForCopyTrading && _state.State.Config.MasterBotIdentifier.HasValue) { _logger.LogInformation("Checking copy trading authorization for bot {GrainId}", this.GetPrimaryKey()); - + // Check if copy trading validation should be bypassed (for testing) var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")? .Equals("true", StringComparison.OrdinalIgnoreCase) ?? true; diff --git a/src/Managing.Application/ManageBot/StopBotCommandHandler.cs b/src/Managing.Application/ManageBot/StopBotCommandHandler.cs index 59db3f93..a087ea26 100644 --- a/src/Managing.Application/ManageBot/StopBotCommandHandler.cs +++ b/src/Managing.Application/ManageBot/StopBotCommandHandler.cs @@ -4,8 +4,10 @@ using Managing.Application.Abstractions.Services; using Managing.Application.ManageBot.Commands; using Managing.Core; using Managing.Domain.Accounts; +using Managing.Domain.Trades; using MediatR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Application.ManageBot @@ -15,16 +17,27 @@ namespace Managing.Application.ManageBot private readonly IBotService _botService; private readonly IServiceScopeFactory _scopeFactory; private readonly IGrainFactory _grainFactory; + private readonly ILogger _logger; - public StopBotCommandHandler(IBotService botService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory) + public StopBotCommandHandler( + IBotService botService, + IServiceScopeFactory scopeFactory, + IGrainFactory grainFactory, + ILogger logger) { _botService = botService; _scopeFactory = scopeFactory; _grainFactory = grainFactory; + _logger = logger; } public async Task Handle(StopBotCommand request, CancellationToken cancellationToken) { + // Close open positions BEFORE stopping the grain to avoid Orleans timeout + // This runs outside the grain context, so it won't cause grain timeouts + await CloseAllOpenPositionsAsync(request.Identifier); + + // Now stop the grain (this is fast - just stops timer and updates registry) var result = await _botService.StopBot(request.Identifier); try @@ -49,10 +62,70 @@ namespace Managing.Application.ManageBot { // Log the error but don't fail the stop operation // The bot was successfully stopped, we just couldn't update the summary - Console.WriteLine($"Failed to update agent summary after stopping bot {request.Identifier}: {ex.Message}"); + _logger.LogWarning(ex, "Failed to update agent summary after stopping bot {Identifier}", request.Identifier); } return result; } + + /// + /// Closes all open positions for a bot. This runs outside the Orleans grain context + /// to avoid grain timeouts when closing positions takes a long time. + /// + private async Task CloseAllOpenPositionsAsync(Guid botId) + { + try + { + var positions = await ServiceScopeHelpers.WithScopedService>( + _scopeFactory, + async tradingService => await tradingService.GetPositionsByInitiatorIdentifierAsync(botId)); + + var openPositions = positions?.Where(p => p.IsOpen() || p.Status.Equals(PositionStatus.New)).ToList() ?? + new List(); + + if (!openPositions.Any()) + { + _logger.LogDebug("Bot {BotId} has no open positions to close", botId); + return; + } + + _logger.LogInformation( + "Closing {Count} open positions for bot {BotId}: {Positions}", + openPositions.Count, botId, string.Join(", ", openPositions.Select(p => p.Identifier))); + + foreach (var position in openPositions) + { + try + { + _logger.LogInformation("Closing position {PositionId} for bot {BotId}", position.Identifier, botId); + await ClosePositionAsync(botId, position.Identifier); + _logger.LogInformation("Successfully closed position {PositionId} for bot {BotId}", + position.Identifier, botId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to close position {PositionId} for bot {BotId}", + position.Identifier, botId); + // Continue with other positions even if one fails + } + } + + _logger.LogInformation("Finished closing positions for bot {BotId}", botId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error closing open positions for bot {BotId}", botId); + // Don't throw - we want to continue with the stop process even if position closing fails + } + } + + /// + /// Closes a single position using the grain's ClosePositionAsync method + /// + private async Task ClosePositionAsync(Guid botId, Guid positionId) + { + var grain = _grainFactory.GetGrain(botId); + await grain.ClosePositionAsync(positionId); + } } } \ No newline at end of file