diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 1bd68680..c9600c0b 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -14,6 +14,7 @@ using Managing.Domain.Bots; using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; +using Managing.Domain.Shared.Helpers; using Managing.Domain.Strategies; using Managing.Domain.Trades; using Managing.Domain.Users; @@ -955,13 +956,6 @@ public class BotController : BaseController } } - var tradingType = request.Config.TradingType switch - { - TradingType.BacktestFutures => TradingType.Futures, - TradingType.BacktestSpot => TradingType.Spot, - _ => TradingType.Futures - }; - // Map the request to the full TradingBotConfig var config = new TradingBotConfig { @@ -985,7 +979,7 @@ public class BotController : BaseController // Set computed/default properties FlipPosition = request.Config.FlipPosition, Name = request.Config.Name, - TradingType = tradingType + TradingType = TradingBox.GetLiveTradingType(request.Config.TradingType) }; return (config, user); diff --git a/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs b/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs index acfd954a..2a834f63 100644 --- a/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs +++ b/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs @@ -13,8 +13,8 @@ public interface ICandleStoreGrain : IGrainWithStringKey /// /// Gets the current list of historical candles (up to 500 most recent) /// - /// List of candles ordered by date - Task> GetCandlesAsync(); + /// Read-only list of candles ordered by date + Task> GetCandlesAsync(); /// /// Gets the X latest candles from the store /// diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index f931697d..d7c39374 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -68,7 +68,7 @@ public abstract class TradingBotBase : ITradingBot public virtual async Task Start(BotStatus previousStatus) { - if (Config.TradingType == TradingType.Futures) + if (TradingBox.IsLiveTrading(Config.TradingType)) { // Start async initialization in the background without blocking try @@ -111,8 +111,8 @@ public abstract class TradingBotBase : ITradingBot case BotStatus.Stopped: // If status was Stopped we log a message to inform the user that the bot is restarting await LogInformationAsync($"🔄 Bot Restarted\n" + - $"📊 Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" + - $"✅ Ready to continue trading"); + $"📊 Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" + + $"✅ Ready to continue trading"); break; default: @@ -235,7 +235,7 @@ public abstract class TradingBotBase : ITradingBot await ManagePositions(); UpdateWalletBalances(); - if (Config.TradingType == TradingType.Futures) + if (TradingBox.IsLiveTrading(Config.TradingType)) { ExecutionCount++; @@ -436,7 +436,9 @@ public abstract class TradingBotBase : ITradingBot protected void UpdateWalletBalances() { - var date = Config.TradingType == TradingType.BacktestFutures ? LastCandle?.Date ?? DateTime.UtcNow : DateTime.UtcNow; + var date = Config.TradingType == TradingType.BacktestFutures + ? LastCandle?.Date ?? DateTime.UtcNow + : DateTime.UtcNow; if (WalletBalances.Count == 0) { @@ -474,7 +476,7 @@ public abstract class TradingBotBase : ITradingBot // Common position status handling if (internalPosition.Status == PositionStatus.Finished || - internalPosition.Status == PositionStatus.Flipped) + internalPosition.Status == PositionStatus.Flipped) { await HandleClosedPosition(positionForSignal); } @@ -636,7 +638,7 @@ public abstract class TradingBotBase : ITradingBot } // Synth risk monitoring (only for live trading) - if (Config.UseSynthApi && Config.TradingType == TradingType.Futures && + if (Config.UseSynthApi && TradingBox.IsLiveTrading(Config.TradingType) && positionForSignal.Status == PositionStatus.Filled) { await MonitorSynthRisk(signal, positionForSignal); @@ -644,19 +646,22 @@ public abstract class TradingBotBase : ITradingBot } catch (Exception ex) { - await LogWarningAsync($"Cannot update position {positionForSignal.Identifier}: {ex.Message}, {ex.StackTrace}"); + await LogWarningAsync( + $"Cannot update position {positionForSignal.Identifier}: {ex.Message}, {ex.StackTrace}"); SentrySdk.CaptureException(ex); return; } } // Virtual methods for trading mode-specific behavior - protected virtual async Task SynchronizeWithBrokerPositions(Position internalPosition, Position positionForSignal, List brokerPositions) + protected virtual async Task SynchronizeWithBrokerPositions(Position internalPosition, Position positionForSignal, + List brokerPositions) { // Default implementation: do nothing (for backtest) } - protected virtual async Task HandleOrderManagementAndPositionStatus(LightSignal signal, Position internalPosition, Position positionForSignal) + protected virtual async Task HandleOrderManagementAndPositionStatus(LightSignal signal, Position internalPosition, + Position positionForSignal) { // Default implementation: do nothing (for backtest) } @@ -957,44 +962,44 @@ public abstract class TradingBotBase : ITradingBot LightSignal previousSignal, decimal lastPrice) { // Default implementation - subclasses should override - if (Config.FlipPosition) - { - var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0; - var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit; + if (Config.FlipPosition) + { + var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0; + var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit; - if (shouldFlip) - { - var flipReason = Config.FlipOnlyWhenInProfit - ? "current position is in profit" - : "FlipOnlyWhenInProfit is disabled"; + if (shouldFlip) + { + var flipReason = Config.FlipOnlyWhenInProfit + ? "current position is in profit" + : "FlipOnlyWhenInProfit is disabled"; - await LogInformation( - $"🔄 Position Flip Initiated\nFlipping position due to opposite signal\nReason: {flipReason}"); - await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true); - await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped); - var newPosition = await OpenPosition(signal); - await LogInformation( - $"✅ Position Flipped\nPosition: `{previousSignal.Identifier}` → `{signal.Identifier}`\nPrice: `${lastPrice}`"); - return newPosition; - } - else - { - var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0; - await LogInformation( - $"💸 Flip Blocked - Not Profitable\nPosition `{previousSignal.Identifier}` PnL: `${currentPnl:F2}`\nSignal `{signal.Identifier}` will wait for profitability"); - - SetSignalStatus(signal.Identifier, SignalStatus.Expired); - return null; - } - } - else - { - await LogInformation( - $"🚫 Flip Disabled\nPosition already open for: `{previousSignal.Identifier}`\nFlipping disabled, new signal expired"); - SetSignalStatus(signal.Identifier, SignalStatus.Expired); - return null; - } + await LogInformation( + $"🔄 Position Flip Initiated\nFlipping position due to opposite signal\nReason: {flipReason}"); + await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true); + await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped); + var newPosition = await OpenPosition(signal); + await LogInformation( + $"✅ Position Flipped\nPosition: `{previousSignal.Identifier}` → `{signal.Identifier}`\nPrice: `${lastPrice}`"); + return newPosition; } + else + { + var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0; + await LogInformation( + $"💸 Flip Blocked - Not Profitable\nPosition `{previousSignal.Identifier}` PnL: `${currentPnl:F2}`\nSignal `{signal.Identifier}` will wait for profitability"); + + SetSignalStatus(signal.Identifier, SignalStatus.Expired); + return null; + } + } + else + { + await LogInformation( + $"🚫 Flip Disabled\nPosition already open for: `{previousSignal.Identifier}`\nFlipping disabled, new signal expired"); + SetSignalStatus(signal.Identifier, SignalStatus.Expired); + return null; + } + } /// /// Executes the actual position opening logic. @@ -1003,34 +1008,34 @@ public abstract class TradingBotBase : ITradingBot protected virtual async Task ExecuteOpenPosition(LightSignal signal, decimal lastPrice) { // Default implementation - subclasses should override - // Verify actual balance before opening position + // Verify actual balance before opening position await VerifyAndUpdateBalanceAsync(); - var command = new OpenPositionRequest( - Config.AccountName, - Config.MoneyManagement, - signal.Direction, - Config.Ticker, - PositionInitiator.Bot, - signal.Date, - Account.User, - Config.BotTradingBalance, + var command = new OpenPositionRequest( + Config.AccountName, + Config.MoneyManagement, + signal.Direction, + Config.Ticker, + PositionInitiator.Bot, + signal.Date, + Account.User, + Config.BotTradingBalance, Config.TradingType == TradingType.BacktestFutures, - lastPrice, - signalIdentifier: signal.Identifier, + lastPrice, + signalIdentifier: signal.Identifier, initiatorIdentifier: Identifier, tradingType: Config.TradingType); - var position = await ServiceScopeHelpers - .WithScopedServices( - _scopeFactory, - async (exchangeService, accountService, tradingService) => - { - return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) - .Handle(command); - }); + var position = await ServiceScopeHelpers + .WithScopedServices( + _scopeFactory, + async (exchangeService, accountService, tradingService) => + { + return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) + .Handle(command); + }); - return position; + return position; } private async Task SendPositionToCopyTrading(Position position) @@ -1175,7 +1180,7 @@ public abstract class TradingBotBase : ITradingBot var brokerHistoryReconciled = await ReconcileWithBrokerHistory(position, currentCandle); if (brokerHistoryReconciled && !forceMarketClose) { - goto SkipCandleBasedCalculation; + goto SkipCandleBasedCalculation; } // Calculate position closing details using subclass-specific logic @@ -1217,7 +1222,7 @@ public abstract class TradingBotBase : ITradingBot $"Total Fees: `${position.GasFees + position.UiFees:F2}`\n" + $"Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`"; - if (Config.TradingType == TradingType.Futures) + if (TradingBox.IsLiveTrading(Config.TradingType)) { await LogDebugAsync(logMessage); } @@ -1227,7 +1232,7 @@ public abstract class TradingBotBase : ITradingBot await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished); // Update position in database with all trade changes - if (Config.TradingType == TradingType.Futures) + if (TradingBox.IsLiveTrading(Config.TradingType)) { position.Status = PositionStatus.Finished; await UpdatePositionDatabase(position); @@ -1240,7 +1245,9 @@ public abstract class TradingBotBase : ITradingBot // Update the last position closing time for cooldown period tracking // Only update if position was actually filled - LastPositionClosingTime = Config.TradingType == TradingType.BacktestFutures ? currentCandle.Date : DateTime.UtcNow; + LastPositionClosingTime = Config.TradingType == TradingType.BacktestFutures + ? currentCandle.Date + : DateTime.UtcNow; } else { @@ -1283,7 +1290,7 @@ public abstract class TradingBotBase : ITradingBot private async Task CancelAllOrders() { - if (Config.TradingType == TradingType.Futures && !Config.IsForWatchingOnly) + if (TradingBox.IsLiveTrading(Config.TradingType) && !Config.IsForWatchingOnly) { try { @@ -1465,7 +1472,7 @@ public abstract class TradingBotBase : ITradingBot try { // Set signal status based on configuration - if (Config.IsForWatchingOnly || (ExecutionCount < 1 && Config.TradingType == TradingType.Futures)) + if (Config.IsForWatchingOnly || (ExecutionCount < 1 && TradingBox.IsLiveTrading(Config.TradingType))) { signal.Status = SignalStatus.Expired; } @@ -1480,7 +1487,7 @@ public abstract class TradingBotBase : ITradingBot $"🆔 Signal ID: `{signal.Identifier}`"; // Apply Synth-based signal filtering if enabled - if (Config.UseSynthApi && Config.TradingType == TradingType.Futures && ExecutionCount > 0) + if (Config.UseSynthApi && TradingBox.IsLiveTrading(Config.TradingType) && ExecutionCount > 0) { await ServiceScopeHelpers.WithScopedServices(_scopeFactory, async (tradingService, exchangeService) => @@ -1513,7 +1520,7 @@ public abstract class TradingBotBase : ITradingBot await LogInformation(signalText); - if (Config.IsForWatchingOnly && Config.TradingType == TradingType.Futures && ExecutionCount > 0) + if (Config.IsForWatchingOnly && TradingBox.IsLiveTrading(Config.TradingType) && ExecutionCount > 0) { await ServiceScopeHelpers.WithScopedService(_scopeFactory, async messengerService => { @@ -1852,7 +1859,8 @@ public abstract class TradingBotBase : ITradingBot // Calculate cooldown end time based on last position closing time var cooldownEndTime = TradingBox.CalculateCooldownEndTime(LastPositionClosingTime.Value, Config.Timeframe, Config.CooldownPeriod); - var isInCooldown = (Config.TradingType == TradingType.BacktestFutures ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime; + var isInCooldown = (Config.TradingType == TradingType.BacktestFutures ? LastCandle.Date : DateTime.UtcNow) < + cooldownEndTime; if (isInCooldown) { @@ -1937,7 +1945,8 @@ public abstract class TradingBotBase : ITradingBot await agentGrain.OnPositionOpenedAsync(positionOpenEvent); await platformGrain.OnPositionOpenAsync(positionOpenEvent); - await LogDebugAsync($"Sent position opened event to both grains for position {position.Identifier}"); + await LogDebugAsync( + $"Sent position opened event to both grains for position {position.Identifier}"); break; case NotificationEventType.PositionClosed: @@ -1952,7 +1961,8 @@ public abstract class TradingBotBase : ITradingBot await agentGrain.OnPositionClosedAsync(positionClosedEvent); await platformGrain.OnPositionClosedAsync(positionClosedEvent); - await LogDebugAsync($"Sent position closed event to both grains for position {position.Identifier}"); + await LogDebugAsync( + $"Sent position closed event to both grains for position {position.Identifier}"); break; case NotificationEventType.PositionUpdated: @@ -2109,7 +2119,7 @@ public abstract class TradingBotBase : ITradingBot protected virtual async Task SendTradeMessageAsync(string message, bool isBadBehavior = false) { - if (Config.TradingType == TradingType.Futures) + if (TradingBox.IsLiveTrading(Config.TradingType)) { var user = Account.User; var messageWithBotName = $"🤖 {user.AgentName} - {Config.Name}\n{message}"; diff --git a/src/Managing.Application/Grains/CandleStoreGrain.cs b/src/Managing.Application/Grains/CandleStoreGrain.cs index a767ebee..45647a20 100644 --- a/src/Managing.Application/Grains/CandleStoreGrain.cs +++ b/src/Managing.Application/Grains/CandleStoreGrain.cs @@ -121,7 +121,7 @@ public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver await base.OnDeactivateAsync(reason, cancellationToken); } - public Task> GetCandlesAsync() + public Task> GetCandlesAsync() { try { @@ -130,15 +130,16 @@ public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver { _logger.LogWarning("State not initialized for grain {GrainKey}, returning empty list", this.GetPrimaryKeyString()); - return Task.FromResult(new HashSet()); + return Task.FromResult>(new List()); } - return Task.FromResult(_state.State.Candles.ToHashSet()); + // Return a readonly wrapper to preserve order and prevent external modifications + return Task.FromResult>(_state.State.Candles.AsReadOnly()); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving candles for grain {GrainKey}", this.GetPrimaryKeyString()); - return Task.FromResult(new HashSet()); + return Task.FromResult>(new List()); } } diff --git a/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs b/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs index 5e14b003..b872b5ca 100644 --- a/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs +++ b/src/Managing.Application/Scenarios/ScenarioRunnerGrain.cs @@ -32,11 +32,11 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain _scopeFactory = scopeFactory; } - private async Task> GetCandlesAsync(TradingExchanges tradingExchange, TradingBotConfig config) + private async Task> GetCandlesAsync(TradingExchanges tradingExchange, TradingBotConfig config) { try { - var newCandles = await ServiceScopeHelpers.WithScopedService>( + var newCandles = await ServiceScopeHelpers.WithScopedService>( _scopeFactory, async grainFactory => { var priceGrainKey = @@ -66,9 +66,8 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain return null; } - var candlesHashSet = await GetCandlesAsync(tradingExchanges, config); - // Convert to ordered List to preserve chronological order for indicators - var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList(); + // Get candles as ordered List (already ordered by date from CandleStoreGrain) + var candlesList = await GetCandlesAsync(tradingExchanges, config); if (candlesList.Count == 0) { diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index 35982d92..64454ea0 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -66,7 +66,7 @@ public class Backtest Timeframe = Config.Timeframe, IsForWatchingOnly = false, // Always start as active bot BotTradingBalance = initialTradingBalance, - TradingType = TradingType.Futures, // Always Futures for live bots + TradingType = Config.TradingType, CooldownPeriod = Config.CooldownPeriod, MaxLossStreak = Config.MaxLossStreak, MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 56a074df..9c181653 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -69,12 +69,6 @@ public static class TradingBox preCalculatedIndicatorValues); } - public static LightSignal GetSignal(IReadOnlyList newCandles, LightScenario lightScenario, - Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1) - { - return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null); - } - public static LightSignal GetSignal(IReadOnlyList newCandles, LightScenario lightScenario, Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod, Dictionary preCalculatedIndicatorValues) @@ -1343,4 +1337,23 @@ public static class TradingBox } #endregion + + public static bool IsLiveTrading(TradingType tradingType) + { + return tradingType switch + { + TradingType.Futures => true, + TradingType.Spot => true, + _ => false + }; + } + + public static TradingType GetLiveTradingType(TradingType tradingType) + { + return tradingType switch { + TradingType.BacktestFutures => TradingType.Futures, + TradingType.BacktestSpot => TradingType.Spot, + _ => throw new InvalidOperationException($"Unsupported TradingType for live trading: {tradingType}") + }; + } } \ No newline at end of file diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts index 65a2cc2b..0a039c9c 100644 --- a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts @@ -13,9 +13,9 @@ describe('swap tokens implementation', () => { console.log('Account', sdk.account) const result = await swapGmxTokensImpl( sdk, - Ticker.ETH, + Ticker.BTC, Ticker.USDC, - 0.0042 + 0.00006733 ) assert.strictEqual(typeof result, 'string')