using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Trading.Commands; using Managing.Application.Trading.Handlers; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Indicators; using Managing.Domain.Shared.Helpers; using Managing.Domain.Strategies.Base; using Managing.Domain.Trades; using Managing.Domain.Users; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Orleans.Streams; using static Managing.Common.Enums; namespace Managing.Application.Bots; public class BacktestSpotBot : TradingBotBase, ITradingBot { public BacktestSpotBot( ILogger logger, IServiceScopeFactory scopeFactory, TradingBotConfig config, IStreamProvider? streamProvider = null ) : base(logger, scopeFactory, config, streamProvider) { // Backtest-specific initialization Config.TradingType = TradingType.BacktestSpot; } public override async Task Start(BotStatus previousStatus) { // Backtest mode: Skip account loading and broker initialization // Just log basic startup info await LogInformation($"šŸ”¬ Backtest Spot Bot Started\n" + $"šŸ“Š Testing Setup:\n" + $"šŸŽÆ Ticker: `{Config.Ticker}`\n" + $"ā° Timeframe: `{Config.Timeframe}`\n" + $"šŸŽ® Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" + $"šŸ’° Initial Balance: `${Config.BotTradingBalance:F2}`\n" + $"āœ… Ready to run spot backtest simulation"); } public override async Task Run() { // Backtest signal update is handled in BacktestExecutor loop // No need to call UpdateSignals() here if (!Config.IsForWatchingOnly) await ManagePositions(); UpdateWalletBalances(); // Backtest logging - simplified, no account dependency ExecutionCount++; Logger.LogInformation( "[BacktestSpot][{BotName}] Execution {ExecutionCount} - LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Positions: {PositionCount}", Config.Name, ExecutionCount, LastCandle?.Date, Signals.Count, Positions.Count); } protected override async Task GetInternalPositionForUpdate(Position position) { // In backtest mode, return the position as-is (no database lookup needed) return position; } protected override async Task UpdatePositionWithBrokerData(Position position, List brokerPositions) { // In backtest mode, skip broker synchronization return; } protected override async Task GetCurrentCandleForPositionClose(Account account, string ticker) { // In backtest mode, use LastCandle return LastCandle; } protected override async Task CanOpenPositionWithBrokerChecks(LightSignal signal) { // In backtest mode, skip broker position checks return await CanOpenPosition(signal); } protected override async Task LoadAccountAsync() { // In backtest mode, skip account loading return; } protected override async Task VerifyAndUpdateBalanceAsync() { // In backtest mode, skip balance verification return; } protected override async Task SendPositionToCopyTradingStream(Position position) { // In backtest mode, skip copy trading stream return; } protected override async Task NotifyAgentAndPlatformAsync(NotificationEventType eventType, Position position) { // In backtest mode, skip notifications return; } protected override async Task UpdatePositionInDatabaseAsync(Position position) { // In backtest mode, skip database updates return; } protected override async Task SendClosedPositionToMessenger(Position position, User user) { // In backtest mode, skip messenger updates return; } protected override async Task CancelAllOrdersAsync() { // In backtest mode, no orders to cancel return; } protected override async Task LogInformationAsync(string message) { // In backtest mode, skip user notifications, just log to system if (Config.TradingType == TradingType.BacktestSpot) return; await base.LogInformationAsync(message); } protected override async Task LogWarningAsync(string message) { // In backtest mode, skip user notifications, just log to system if (Config.TradingType == TradingType.BacktestSpot) return; await base.LogWarningAsync(message); } protected override async Task LogDebugAsync(string message) { // In backtest mode, skip messenger debug logs if (Config.TradingType == TradingType.BacktestSpot) return; await base.LogDebugAsync(message); } protected override async Task SendTradeMessageAsync(string message, bool isBadBehavior) { // In backtest mode, skip trade messages return; } protected override async Task UpdateSignalsCore(IReadOnlyList candles, Dictionary preCalculatedIndicatorValues = null) { // Call base implementation for common optimizations (flip check, cooldown check) // This will return early if: // - FlipPosition is disabled AND there's an open position // - Bot is in cooldown period await base.UpdateSignalsCore(candles, preCalculatedIndicatorValues); // For backtest, if no candles provided (called from Run()), skip signal generation // Signals are generated in BacktestExecutor with rolling window candles if (candles == null || candles.Count == 0) return; if (Config.Scenario == null) throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null"); // Use TradingBox.GetSignal for backtest with pre-calculated indicators var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LookbackPeriod, preCalculatedIndicatorValues); if (backtestSignal == null) return; await AddSignal(backtestSignal); } protected override async Task CanOpenPosition(LightSignal signal) { // For spot trading, only LONG signals can open positions if (signal.Direction != TradeDirection.Long) { await LogInformationAsync( $"🚫 Short Signal Ignored\nShort signals cannot open positions in spot trading\nSignal: `{signal.Identifier}` will be ignored"); return false; } // Backtest-specific logic: only check cooldown and loss streak // No broker checks, no synth risk assessment, no startup cycle check needed return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal); } protected override async Task HandleFlipPosition(LightSignal signal, Position openedPosition, LightSignal previousSignal, decimal lastPrice) { // For spot trading, SHORT signals should close the open LONG position // LONG signals should not flip (they would be same direction) if (signal.Direction == TradeDirection.Short && openedPosition.OriginDirection == TradeDirection.Long) { // SHORT signal closes the open LONG position await LogInformationAsync( $"šŸ”» Short Signal - Closing Long Position\nClosing position `{openedPosition.Identifier}` due to SHORT signal\nSignal: `{signal.Identifier}`"); await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true); await SetPositionStatus(previousSignal.Identifier, PositionStatus.Finished); SetSignalStatus(signal.Identifier, SignalStatus.Expired); return null; // No new position opened for SHORT signals } else if (signal.Direction == TradeDirection.Long && openedPosition.OriginDirection == TradeDirection.Long) { // Same direction LONG signal - ignore it await LogInformationAsync( $"šŸ“ Same Direction Signal\nLONG signal `{signal.Identifier}` ignored\nPosition `{openedPosition.Identifier}` already open for LONG"); SetSignalStatus(signal.Identifier, SignalStatus.Expired); return null; } else { // This shouldn't happen in spot trading, but handle it gracefully await LogInformationAsync( $"āš ļø Unexpected Signal Direction\nSignal: `{signal.Identifier}` Direction: `{signal.Direction}`\nPosition: `{openedPosition.Identifier}` Direction: `{openedPosition.OriginDirection}`\nSignal ignored"); SetSignalStatus(signal.Identifier, SignalStatus.Expired); return null; } } protected override async Task ExecuteOpenPosition(LightSignal signal, decimal lastPrice) { // Backtest-specific position opening: no balance verification, no exchange calls // Only LONG signals should reach here (SHORT signals are filtered out earlier) if (signal.Direction != TradeDirection.Long) { throw new InvalidOperationException( $"Only LONG signals can open positions in spot trading. Received: {signal.Direction}"); } if (Account == null || Account.User == null) { throw new InvalidOperationException("Account and Account.User must be set before opening a position"); } var command = new OpenSpotPositionRequest( Config.AccountName, Config.MoneyManagement, signal.Direction, Config.Ticker, PositionInitiator.Bot, signal.Date, Account.User, Config.BotTradingBalance, isForPaperTrading: true, // Backtest is always paper trading lastPrice, signalIdentifier: signal.Identifier, initiatorIdentifier: Identifier, tradingType: Config.TradingType); var position = await ServiceScopeHelpers .WithScopedServices( _scopeFactory, async (exchangeService, accountService, tradingService) => { return await new OpenSpotPositionCommandHandler(exchangeService, accountService, tradingService) .Handle(command); }); return position; } public override async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, bool tradeClosingPosition = false, bool forceMarketClose = false) { await LogInformationAsync( $"šŸ”§ Closing {position.OriginDirection} Spot Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\nšŸ“‹ Type: `{tradeToClose.TradeType}`\nšŸ“Š Quantity: `{tradeToClose.Quantity:F5}`"); // Backtest-specific: no exchange quantity check, no grace period, direct close var command = new CloseSpotPositionCommand(position, position.AccountId, lastPrice); try { Position closedPosition = null; await ServiceScopeHelpers.WithScopedServices( _scopeFactory, async (exchangeService, accountService, tradingService) => { closedPosition = await new CloseSpotPositionCommandHandler(exchangeService, accountService, tradingService) .Handle(command); }); if (closedPosition.Status == PositionStatus.Finished || closedPosition.Status == PositionStatus.Flipped) { if (tradeClosingPosition) { await SetPositionStatus(signal.Identifier, PositionStatus.Finished); } await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); } else { throw new Exception($"Wrong position status : {closedPosition.Status}"); } } catch (Exception ex) { await LogWarningAsync($"Position {signal.Identifier} not closed : {ex.Message}"); if (position.Status == PositionStatus.Canceled || position.Status == PositionStatus.Rejected) { // Trade close on exchange => Should close trade manually await SetPositionStatus(signal.Identifier, PositionStatus.Finished); // Ensure trade dates are properly updated even for canceled/rejected positions await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); } } } }