diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 7f207ca3..b51454be 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -11,7 +11,6 @@ using Managing.Domain.Scenarios; using Managing.Domain.Statistics; using Managing.Domain.Strategies; using Managing.Domain.Strategies.Base; -using Managing.Domain.Trades; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -455,19 +454,14 @@ public class DataController : ControllerBase // Get all strategies for the specified user var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName)); - // Get all positions for all strategies in a single database call to avoid DbContext concurrency issues - var strategyIdentifiers = userStrategies.Select(s => s.Identifier).ToList(); - var allPositions = await _tradingService.GetPositionsByInitiatorIdentifiersAsync(strategyIdentifiers); - var positionsByIdentifier = allPositions.GroupBy(p => p.InitiatorIdentifier) - .ToDictionary(g => g.Key, g => g.ToList()); - // Get agent balance history for the last 30 days var startDate = DateTime.UtcNow.AddDays(-30); var endDate = DateTime.UtcNow; var agentBalanceHistory = await _agentService.GetAgentBalances(agentName, startDate, endDate); // Convert to detailed view model with additional information - var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy, positionsByIdentifier, agentBalanceHistory)) + var result = userStrategies + .Select(strategy => MapStrategyToViewModelAsync(strategy, agentBalanceHistory)) .ToList(); return Ok(result); @@ -511,78 +505,15 @@ public class DataController : ControllerBase return Ok(result); } - /// - /// Maps a trading bot to a strategy view model with detailed statistics using pre-fetched positions - /// - /// The trading bot to map - /// Pre-fetched positions grouped by initiator identifier - /// Agent balance history data - /// A view model with detailed strategy information - private UserStrategyDetailsViewModel MapStrategyToViewModel(Bot strategy, - Dictionary> positionsByIdentifier, AgentBalanceHistory agentBalanceHistory) - { - // Calculate ROI percentage based on PnL relative to account value - decimal pnl = strategy.Pnl; - - // If we had initial investment amount, we could calculate ROI like: - decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account - decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0; - - // Calculate volume statistics - decimal totalVolume = strategy.Volume; - decimal volumeLast24h = strategy.Volume; - - // Calculate win/loss statistics - (int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses); - - int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0; - // Calculate ROI for last 24h - decimal roiLast24h = strategy.Roi; - - // Get positions for this strategy from pre-fetched data - var positions = positionsByIdentifier.TryGetValue(strategy.Identifier, out var strategyPositions) - ? strategyPositions - : new List(); - - // Convert agent balance history to wallet balances dictionary - var walletBalances = agentBalanceHistory?.AgentBalances? - .ToDictionary(b => b.Time, b => b.TotalValue) ?? new Dictionary(); - - return new UserStrategyDetailsViewModel - { - Name = strategy.Name, - State = strategy.Status, - PnL = pnl, - ROIPercentage = roi, - ROILast24H = roiLast24h, - Runtime = strategy.StartupTime, - WinRate = winRate, - TotalVolumeTraded = totalVolume, - VolumeLast24H = volumeLast24h, - Wins = wins, - Losses = losses, - Positions = positions, - Identifier = strategy.Identifier, - WalletBalances = walletBalances, - Ticker = strategy.Ticker - }; - } - /// /// Maps a trading bot to a strategy view model with detailed statistics /// /// The trading bot to map /// Agent balance history data /// A view model with detailed strategy information - private async Task MapStrategyToViewModelAsync(Bot strategy, AgentBalanceHistory agentBalanceHistory) + private async Task MapStrategyToViewModelAsync(Bot strategy, + AgentBalanceHistory agentBalanceHistory) { - // Calculate ROI percentage based on PnL relative to account value - decimal pnl = strategy.Pnl; - - // If we had initial investment amount, we could calculate ROI like: - decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account - decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0; - // Calculate volume statistics decimal totalVolume = strategy.Volume; decimal volumeLast24h = strategy.Volume; @@ -591,8 +522,6 @@ public class DataController : ControllerBase (int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses); int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0; - // Calculate ROI for last 24h - decimal roiLast24h = strategy.Roi; // Fetch positions associated with this bot var positions = await _tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier); @@ -605,9 +534,8 @@ public class DataController : ControllerBase { Name = strategy.Name, State = strategy.Status, - PnL = pnl, - ROIPercentage = roi, - ROILast24H = roiLast24h, + PnL = strategy.Pnl, + ROIPercentage = strategy.Roi, Runtime = strategy.StartupTime, WinRate = winRate, TotalVolumeTraded = totalVolume, diff --git a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs index 25b82da5..398332a5 100644 --- a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs +++ b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs @@ -28,11 +28,6 @@ namespace Managing.Api.Models.Responses /// public decimal ROIPercentage { get; set; } - /// - /// Return on investment percentage in the last 24 hours - /// - public decimal ROILast24H { get; set; } - /// /// Date and time when the strategy was started /// diff --git a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs index f34a2e2d..6c90d00b 100644 --- a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs +++ b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs @@ -14,52 +14,54 @@ public interface IPlatformSummaryGrain : IGrainWithStringKey /// Gets the current platform summary data /// Task GetPlatformSummaryAsync(); - + /// /// Forces a refresh of all platform data /// Task RefreshDataAsync(); - + /// /// Gets the total volume traded across all strategies /// Task GetTotalVolumeAsync(); - + /// /// Gets the total PnL across all strategies /// Task GetTotalPnLAsync(); - + /// /// Gets the total open interest across all positions /// Task GetTotalOpenInterest(); - + /// /// Gets the total number of open positions /// Task GetTotalPositionCountAsync(); - + /// /// Gets the total platform fees /// Task GetTotalFeesAsync(); - + /// /// Gets the daily volume history for the last 30 days for chart visualization /// Task> GetVolumeHistoryAsync(); - + // Event handlers for immediate updates /// /// Updates the active strategy count /// [OneWay] Task UpdateActiveStrategyCountAsync(int newActiveCount); + [OneWay] Task OnPositionClosedAsync(PositionClosedEvent evt); + [OneWay] - Task OnTradeExecutedAsync(TradeExecutedEvent evt); + Task OnPositionOpenAsync(PositionOpenEvent evt); } /// @@ -68,59 +70,36 @@ public interface IPlatformSummaryGrain : IGrainWithStringKey [GenerateSerializer] public abstract class PlatformMetricsEvent { - [Id(0)] - public DateTime Timestamp { get; set; } = DateTime.UtcNow; + [Id(0)] public DateTime Timestamp { get; set; } = DateTime.UtcNow; } - - - /// /// Event fired when a position is closed /// [GenerateSerializer] public class PositionClosedEvent : PlatformMetricsEvent { - [Id(1)] - public Guid PositionId { get; set; } - - [Id(2)] - public Ticker Ticker { get; set; } - - [Id(3)] - public decimal RealizedPnL { get; set; } - - [Id(4)] - public decimal Volume { get; set; } + [Id(1)] public Guid PositionIdentifier { get; set; } + + [Id(2)] public Ticker Ticker { get; set; } + + [Id(3)] public decimal RealizedPnL { get; set; } + + [Id(4)] public decimal Volume { get; set; } } /// /// Event fired when a trade is executed /// [GenerateSerializer] -public class TradeExecutedEvent : PlatformMetricsEvent +public class PositionOpenEvent : PlatformMetricsEvent { - [Id(1)] - public Guid TradeId { get; set; } - - [Id(2)] - public Guid PositionId { get; set; } - - [Id(3)] - public Guid StrategyId { get; set; } - - [Id(4)] - public Ticker Ticker { get; set; } - - [Id(5)] - public decimal Volume { get; set; } - - [Id(6)] - public decimal PnL { get; set; } - - [Id(7)] - public decimal Fee { get; set; } - - [Id(8)] - public TradeDirection Direction { get; set; } -} + [Id(1)] public Ticker Ticker { get; set; } + + [Id(2)] public decimal Volume { get; set; } + + [Id(3)] public decimal Fee { get; set; } + + [Id(4)] public TradeDirection Direction { get; set; } + [Id(5)] public Guid PositionIdentifier { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs index 37a61869..b291a083 100644 --- a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs +++ b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs @@ -9,75 +9,53 @@ namespace Managing.Application.Abstractions.Grains; [GenerateSerializer] public class PlatformSummaryGrainState { - [Id(0)] - public DateTime LastUpdated { get; set; } - - [Id(1)] - public DateTime LastSnapshot { get; set; } - - [Id(2)] - public bool HasPendingChanges { get; set; } - - // Current metrics - [Id(3)] - public int TotalAgents { get; set; } - - [Id(4)] - public int TotalActiveStrategies { get; set; } - - [Id(5)] - public decimal TotalPlatformPnL { get; set; } - - [Id(6)] - public decimal TotalPlatformVolume { get; set; } - - [Id(7)] - public decimal TotalOpenInterest { get; set; } - - [Id(8)] - public int TotalPositionCount { get; set; } - - [Id(20)] - public decimal TotalPlatformFees { get; set; } - - // 24-hour ago values (for comparison) - [Id(9)] - public int TotalAgents24hAgo { get; set; } - - [Id(10)] - public int TotalActiveStrategies24hAgo { get; set; } - - [Id(11)] - public decimal TotalPlatformPnL24hAgo { get; set; } - - [Id(12)] - public decimal TotalPlatformVolume24hAgo { get; set; } - - [Id(13)] - public decimal TotalOpenInterest24hAgo { get; set; } - - [Id(14)] - public int TotalPositionCount24hAgo { get; set; } - - [Id(21)] - public decimal TotalPlatformFees24hAgo { get; set; } - - // Historical snapshots - [Id(15)] - public List DailySnapshots { get; set; } = new(); - - // Volume breakdown by asset - [Id(16)] - public Dictionary VolumeByAsset { get; set; } = new(); - - // Position count breakdown - [Id(17)] - public Dictionary PositionCountByAsset { get; set; } = new(); - - [Id(18)] - public Dictionary PositionCountByDirection { get; set; } = new(); -} + [Id(0)] public DateTime LastUpdated { get; set; } + [Id(1)] public DateTime LastSnapshot { get; set; } + + [Id(2)] public bool HasPendingChanges { get; set; } + + // Current metrics + [Id(3)] public int TotalAgents { get; set; } + + [Id(4)] public int TotalActiveStrategies { get; set; } + + [Id(5)] public decimal TotalPlatformPnL { get; set; } + + [Id(6)] public decimal TotalPlatformVolume { get; set; } + + [Id(7)] public decimal OpenInterest { get; set; } + + [Id(8)] public int TotalPositionCount { get; set; } + + [Id(20)] public decimal TotalPlatformFees { get; set; } + + // 24-hour ago values (for comparison) + [Id(9)] public int TotalAgents24hAgo { get; set; } + + [Id(10)] public int TotalActiveStrategies24hAgo { get; set; } + + [Id(11)] public decimal TotalPlatformPnL24hAgo { get; set; } + + [Id(12)] public decimal TotalPlatformVolume24hAgo { get; set; } + + [Id(13)] public decimal TotalOpenInterest24hAgo { get; set; } + + [Id(14)] public int TotalPositionCount24hAgo { get; set; } + + [Id(21)] public decimal TotalPlatformFees24hAgo { get; set; } + + // Historical snapshots + [Id(15)] public List DailySnapshots { get; set; } = new(); + + // Volume breakdown by asset + [Id(16)] public Dictionary VolumeByAsset { get; set; } = new(); + + // Position count breakdown + [Id(17)] public Dictionary PositionCountByAsset { get; set; } = new(); + + [Id(18)] public Dictionary PositionCountByDirection { get; set; } = new(); +} /// /// Daily snapshot of platform metrics @@ -85,29 +63,19 @@ public class PlatformSummaryGrainState [GenerateSerializer] public class DailySnapshot { - [Id(0)] - public DateTime Date { get; set; } - - [Id(1)] - public int TotalAgents { get; set; } - - [Id(2)] - public int TotalStrategies { get; set; } - - [Id(3)] - public decimal TotalVolume { get; set; } - - [Id(4)] - public decimal TotalPnL { get; set; } - - [Id(5)] - public decimal TotalOpenInterest { get; set; } - - [Id(6)] - public int TotalPositionCount { get; set; } - - [Id(7)] - public decimal TotalFees { get; set; } -} + [Id(0)] public DateTime Date { get; set; } + [Id(1)] public int TotalAgents { get; set; } + [Id(2)] public int TotalStrategies { get; set; } + + [Id(3)] public decimal TotalVolume { get; set; } + + [Id(4)] public decimal TotalPnL { get; set; } + + [Id(5)] public decimal TotalOpenInterest { get; set; } + + [Id(6)] public int TotalPositionCount { get; set; } + + [Id(7)] public decimal TotalFees { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs b/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs index 3f5f1acc..1a88b1a9 100644 --- a/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs +++ b/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs @@ -9,18 +9,11 @@ namespace Managing.Application.Abstractions.Models; [GenerateSerializer] public class AgentSummaryUpdateEvent { - [Id(0)] - public int UserId { get; set; } - - [Id(1)] - public Guid BotId { get; set; } - - [Id(2)] - public AgentSummaryEventType EventType { get; set; } - - [Id(3)] - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - [Id(4)] - public string? AdditionalData { get; set; } // Optional additional context -} + [Id(0)] public Guid BotId { get; set; } + + [Id(1)] public AgentSummaryEventType EventType { get; set; } + + [Id(2)] public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + [Id(3)] public string? AdditionalData { get; set; } // Optional additional context +} \ No newline at end of file diff --git a/src/Managing.Application.Tests/AgentGrainTests.cs b/src/Managing.Application.Tests/AgentGrainTests.cs index e5b4d7fa..5f59cfee 100644 --- a/src/Managing.Application.Tests/AgentGrainTests.cs +++ b/src/Managing.Application.Tests/AgentGrainTests.cs @@ -50,7 +50,6 @@ public class AgentGrainTests var botId = _mockState.Object.State.BotIds.First(); var updateEvent = new AgentSummaryUpdateEvent { - UserId = 1, BotId = botId, EventType = AgentSummaryEventType.PositionOpened, Timestamp = DateTime.UtcNow @@ -76,7 +75,6 @@ public class AgentGrainTests var agentGrain = CreateAgentGrain(); var updateEvent = new AgentSummaryUpdateEvent { - UserId = 1, BotId = Guid.NewGuid(), // Different bot ID EventType = AgentSummaryEventType.PositionOpened, Timestamp = DateTime.UtcNow diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index 85ad9cec..e1336673 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -350,17 +350,20 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable try { var agentGrain = GrainFactory.GetGrain(_state.State.User.Id); - var balanceCheckResult = await agentGrain.CheckAndEnsureEthBalanceAsync(_state.State.Identifier, _tradingBot.Account.Name); + var balanceCheckResult = + await agentGrain.CheckAndEnsureEthBalanceAsync(_state.State.Identifier, _tradingBot.Account.Name); if (!balanceCheckResult.IsSuccessful) { // Log the specific reason for the failure - await _tradingBot.LogWarning($"Balance check failed: {balanceCheckResult.Message} (Reason: {balanceCheckResult.FailureReason})"); - + await _tradingBot.LogWarning( + $"Balance check failed: {balanceCheckResult.Message} (Reason: {balanceCheckResult.FailureReason})"); + // Check if the bot should stop due to this failure if (balanceCheckResult.ShouldStopBot) { - await _tradingBot.LogWarning($"Stopping bot due to balance check failure: {balanceCheckResult.Message}"); + await _tradingBot.LogWarning( + $"Stopping bot due to balance check failure: {balanceCheckResult.Message}"); await StopAsync(); return; } @@ -371,10 +374,6 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable return; } } - else - { - await _tradingBot.LogInformation($"Balance check successful: {balanceCheckResult.Message}"); - } } catch (Exception ex) { diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index b1519667..1b6fbb9a 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -99,9 +99,6 @@ public class TradingBotBase : ITradingBot // Notify AgentGrain about bot startup await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.BotStarted, $"Bot: {Config.Name}, Ticker: {Config.Ticker}"); - - // Notify platform summary about active strategy count change - await NotifyPlatformSummaryAboutStrategyCount(); break; case BotStatus.Running: @@ -390,11 +387,9 @@ public class TradingBotBase : ITradingBot } else { - brokerPositions = await ServiceScopeHelpers.WithScopedService>(_scopeFactory, - async exchangeService => - { - return [.. await exchangeService.GetBrokerPositions(Account)]; - }); + brokerPositions = await ServiceScopeHelpers.WithScopedService>( + _scopeFactory, + async exchangeService => { return [.. await exchangeService.GetBrokerPositions(Account)]; }); } }); @@ -421,7 +416,7 @@ public class TradingBotBase : ITradingBot if (!internalPosition.Status.Equals(PositionStatus.New)) { internalPosition.Status = PositionStatus.Filled; - + // Update Open trade status when position becomes Filled if (internalPosition.Open != null) { @@ -434,8 +429,11 @@ public class TradingBotBase : ITradingBot if (internalPosition.Status == PositionStatus.New) { var orders = await ServiceScopeHelpers.WithScopedService>(_scopeFactory, - async exchangeService => { return [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]; }); - + async exchangeService => + { + return [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]; + }); + if (orders.Any()) { var ordersCount = orders.Count(); @@ -480,25 +478,27 @@ public class TradingBotBase : ITradingBot // Check if position is already open on broker with 2 orders await LogInformation( $"🔍 **Checking Broker Position**\nPosition has exactly `{orders.Count()}` open orders\nChecking if position is already open on broker..."); - + Position brokerPosition = null; - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => - { - var brokerPositions = await exchangeService.GetBrokerPositions(Account); - brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker); - }); + await ServiceScopeHelpers.WithScopedService(_scopeFactory, + async exchangeService => + { + var brokerPositions = await exchangeService.GetBrokerPositions(Account); + brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker); + }); if (brokerPosition != null) { await LogInformation( $"✅ **Position Found on Broker**\nPosition is already open on broker\nUpdating position status to Filled"); - + UpdatePositionPnl(positionForSignal.Identifier, brokerPosition.ProfitAndLoss.Realized); await SetPositionStatus(signal.Identifier, PositionStatus.Filled); - + // Notify platform summary about the executed trade await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened, - $"Position found on broker with 2 orders: {internalPosition.Identifier}", internalPosition); + $"Position found on broker with 2 orders: {internalPosition.Identifier}", + internalPosition); } else { @@ -519,11 +519,13 @@ public class TradingBotBase : ITradingBot await HandleClosedPosition(positionForSignal); } } - else if (internalPosition.Status == PositionStatus.Finished || internalPosition.Status == PositionStatus.Flipped) + else if (internalPosition.Status == PositionStatus.Finished || + internalPosition.Status == PositionStatus.Flipped) { await HandleClosedPosition(positionForSignal); } - else if (internalPosition.Status == PositionStatus.Filled || internalPosition.Status == PositionStatus.PartiallyFilled) + else if (internalPosition.Status == PositionStatus.Filled || + internalPosition.Status == PositionStatus.PartiallyFilled) { Candle lastCandle = null; await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => @@ -619,7 +621,8 @@ public class TradingBotBase : ITradingBot } } } - else if (internalPosition.Status == PositionStatus.Rejected || internalPosition.Status == PositionStatus.Canceled) + else if (internalPosition.Status == PositionStatus.Rejected || + internalPosition.Status == PositionStatus.Canceled) { await LogWarning($"Open position trade is rejected for signal {signal.Identifier}"); if (signal.Status == SignalStatus.PositionOpen) @@ -664,18 +667,18 @@ public class TradingBotBase : ITradingBot } } - if (!Config.IsForBacktest){ - // Update position in database with broker data + if (!Config.IsForBacktest) + { + // Update position in database with broker data await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => { // Update the internal position with broker data internalPosition.Status = PositionStatus.Filled; internalPosition.ProfitAndLoss = internalPosition.ProfitAndLoss; - + // Save updated position to database await tradingService.UpdatePositionAsync(internalPosition); }); - } } catch (Exception ex) @@ -807,9 +810,6 @@ public class TradingBotBase : ITradingBot await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened, $"Signal: {signal.Identifier}", position); - // Publish TradeExecutedEvent for the opening trade (this handles both position opening and trade execution) - await PublishTradeExecutedEventAsync(position.Open, position, signal.Identifier, 0); - Logger.LogInformation($"Position requested"); return position; // Return the created position without adding to list } @@ -828,10 +828,11 @@ public class TradingBotBase : ITradingBot // Handle insufficient funds errors with user-friendly messaging SetSignalStatus(signal.Identifier, SignalStatus.Expired); await LogWarning(ex.UserMessage); - + // Log the technical details for debugging - Logger.LogError(ex, "Insufficient funds error for signal {SignalId}: {ErrorMessage}", signal.Identifier, ex.Message); - + Logger.LogError(ex, "Insufficient funds error for signal {SignalId}: {ErrorMessage}", signal.Identifier, + ex.Message); + return null; } catch (Exception ex) @@ -1066,7 +1067,7 @@ public class TradingBotBase : ITradingBot if (currentCandle != null) { List recentCandles = null; - + if (Config.IsForBacktest) { recentCandles = LastCandle != null ? new List() { LastCandle } : new List(); @@ -1076,16 +1077,18 @@ public class TradingBotBase : ITradingBot // Use CandleStoreGrain to get recent candles instead of calling exchange service directly await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => { - var grainKey = CandleHelpers.GetCandleStoreGrainKey(Account.Exchange, Config.Ticker, Config.Timeframe); + var grainKey = + CandleHelpers.GetCandleStoreGrainKey(Account.Exchange, Config.Ticker, Config.Timeframe); var grain = grainFactory.GetGrain(grainKey); - + try { recentCandles = await grain.GetLastCandle(5); } catch (Exception ex) { - Logger.LogError(ex, "Error retrieving recent candles from CandleStoreGrain for {GrainKey}", grainKey); + Logger.LogError(ex, "Error retrieving recent candles from CandleStoreGrain for {GrainKey}", + grainKey); recentCandles = new List(); } }); @@ -1218,10 +1221,8 @@ public class TradingBotBase : ITradingBot // Update position in database with all trade changes if (!Config.IsForBacktest) { - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => - { - await tradingService.UpdatePositionAsync(position); - }); + await ServiceScopeHelpers.WithScopedService(_scopeFactory, + async tradingService => { await tradingService.UpdatePositionAsync(position); }); } // Update the last position closing time for cooldown period tracking @@ -1235,7 +1236,8 @@ public class TradingBotBase : ITradingBot Config.BotTradingBalance += position.ProfitAndLoss.Realized; Logger.LogInformation( - string.Format("💰 **Balance Updated**\nNew bot trading balance: `${0:F2}`", Config.BotTradingBalance)); + string.Format("💰 **Balance Updated**\nNew bot trading balance: `${0:F2}`", + Config.BotTradingBalance)); } // Notify AgentGrain about position closing @@ -1244,13 +1246,6 @@ public class TradingBotBase : ITradingBot : "PnL: Unknown"; await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionClosed, string.Format("Signal: {0}, {1}", position.SignalIdentifier, pnlInfo), position); - - // Publish TradeExecutedEvent for the closing trade - var closingTrade = GetClosingTrade(position); - if (closingTrade != null) - { - await PublishTradeExecutedEventAsync(closingTrade, position, position.SignalIdentifier, position.ProfitAndLoss?.Realized ?? 0); - } } else { @@ -1260,8 +1255,11 @@ public class TradingBotBase : ITradingBot if (!Config.IsForBacktest) { await ServiceScopeHelpers.WithScopedService(_scopeFactory, - messengerService => { messengerService.SendClosingPosition(position); - return Task.CompletedTask; }); + messengerService => + { + messengerService.SendClosingPosition(position); + return Task.CompletedTask; + }); } await CancelAllOrders(); @@ -1330,7 +1328,7 @@ public class TradingBotBase : ITradingBot Positions.Values.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus; await LogInformation( $"📊 **Position Status Change**\nPosition: `{signalIdentifier}`\nStatus: `{position.Status}` → `{positionStatus}`"); - + // Update Open trade status when position becomes Filled if (positionStatus == PositionStatus.Filled && position.Open != null) { @@ -1444,7 +1442,7 @@ public class TradingBotBase : ITradingBot // Network Fee: $0.50 for opening position only // Closing is handled by oracle, so no network fee for closing - var networkFeeForOpening = 0.50m; + var networkFeeForOpening = 0.15m; fees += networkFeeForOpening; return fees; @@ -1456,9 +1454,6 @@ public class TradingBotBase : ITradingBot Config.IsForWatchingOnly = !Config.IsForWatchingOnly; await LogInformation( $"🔄 **Watch Mode Toggle**\nBot: `{Config.Name}`\nWatch Only: `{(Config.IsForWatchingOnly ? "ON" : "OFF")}`"); - - // Notify platform summary about strategy count change - await NotifyPlatformSummaryAboutStrategyCount(); } /// @@ -1467,9 +1462,6 @@ public class TradingBotBase : ITradingBot public async Task StopBot() { await LogInformation($"🛑 **Bot Stopped**\nBot: `{Config.Name}`\nTicker: `{Config.Ticker}`"); - - // Notify platform summary about strategy count change - await NotifyPlatformSummaryAboutStrategyCount(); } public async Task LogInformation(string message) @@ -1953,46 +1945,6 @@ public class TradingBotBase : ITradingBot return isInCooldown; } - /// - /// Publishes a TradeExecutedEvent to the platform summary grain - /// - /// The trade that was executed - /// The position this trade belongs to - /// The signal identifier - /// The PnL for this trade - private async Task PublishTradeExecutedEventAsync(Trade trade, Position position, string signalIdentifier, decimal pnl) - { - if (Config.IsForBacktest) - { - return; // Skip notifications for backtest - } - - try - { - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => - { - var platformGrain = grainFactory.GetGrain("platform-summary"); - var tradeExecutedEvent = new TradeExecutedEvent - { - TradeId = Guid.NewGuid(), // Generate new ID for the event - PositionId = position.Identifier, - StrategyId = position.InitiatorIdentifier, - Ticker = position.Ticker, - Volume = trade.Price * trade.Quantity * trade.Leverage, - PnL = pnl, - Fee = trade.Fee, - Direction = trade.Direction - }; - await platformGrain.OnTradeExecutedAsync(tradeExecutedEvent); - Logger.LogDebug("Published TradeExecutedEvent for trade {TradeId} in position {PositionId}", tradeExecutedEvent.TradeId, position.Identifier); - }); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to publish TradeExecutedEvent for position {PositionId}", position.Identifier); - } - } - /// /// Gets the trade that was used to close the position /// @@ -2013,12 +1965,14 @@ public class TradingBotBase : ITradingBot { return position.TakeProfit2; } - + // If no specific closing trade is found, create a synthetic one based on the position // This handles cases where the position was closed manually or by the exchange if (position.ProfitAndLoss?.Realized != null) { - var closeDirection = position.OriginDirection == TradeDirection.Long ? TradeDirection.Short : TradeDirection.Long; + var closeDirection = position.OriginDirection == TradeDirection.Long + ? TradeDirection.Short + : TradeDirection.Long; return new Trade( DateTime.UtcNow, closeDirection, @@ -2032,49 +1986,18 @@ public class TradingBotBase : ITradingBot "Position closed" ); } - + return null; } - /// - /// Notifies the platform summary grain about active strategy count changes - /// - private async Task NotifyPlatformSummaryAboutStrategyCount() - { - if (Config.IsForBacktest) - { - return; // Skip notifications for backtest - } - - try - { - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => - { - var platformGrain = grainFactory.GetGrain("platform-summary"); - - // Get current active strategy count from the platform - var currentSummary = await platformGrain.GetPlatformSummaryAsync(); - var currentActiveCount = currentSummary.TotalActiveStrategies; - - // Update the count (this will trigger a refresh if needed) - await platformGrain.UpdateActiveStrategyCountAsync(currentActiveCount); - - Logger.LogDebug("Notified platform summary about strategy count: {Count}", currentActiveCount); - }); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to notify platform summary about strategy count"); - } - } - /// /// Notifies both AgentGrain and PlatformSummaryGrain about bot events /// /// The type of event (e.g., PositionOpened, PositionClosed) /// Optional additional context data /// Optional position data for platform summary events - private async Task NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType eventType, string additionalData = null, Position position = null) + private async Task NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType eventType, string additionalData = null, + Position position = null) { if (Config.IsForBacktest) { @@ -2092,7 +2015,6 @@ public class TradingBotBase : ITradingBot var updateEvent = new AgentSummaryUpdateEvent { - UserId = Account.User.Id, BotId = Identifier, EventType = eventType, Timestamp = DateTime.UtcNow, @@ -2105,24 +2027,35 @@ public class TradingBotBase : ITradingBot // Notify PlatformSummaryGrain (platform-wide metrics) var platformGrain = grainFactory.GetGrain("platform-summary"); - + switch (eventType) { case AgentSummaryEventType.PositionOpened when position != null: // Position opening is now handled by TradeExecutedEvent in PublishTradeExecutedEventAsync - Logger.LogDebug("Position opened notification sent via TradeExecutedEvent for position {PositionId}", position.Identifier); + Logger.LogDebug( + "Position opened notification sent via TradeExecutedEvent for position {PositionId}", + position.Identifier); + var positionOpenEvent = new PositionOpenEvent + { + PositionIdentifier = position.Identifier, + Ticker = position.Ticker, + Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage, + Fee = position.Open.Fee + }; + await platformGrain.OnPositionOpenAsync(positionOpenEvent); break; case AgentSummaryEventType.PositionClosed when position != null: var positionClosedEvent = new PositionClosedEvent { - PositionId = position.Identifier, + PositionIdentifier = position.Identifier, Ticker = position.Ticker, RealizedPnL = position.ProfitAndLoss?.Realized ?? 0, Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage, }; await platformGrain.OnPositionClosedAsync(positionClosedEvent); - Logger.LogDebug("Sent platform position closed notification for position {PositionId}", position.Identifier); + Logger.LogDebug("Sent platform position closed notification for position {PositionId}", + position.Identifier); break; } }); @@ -2132,5 +2065,4 @@ public class TradingBotBase : ITradingBot Logger.LogError(ex, "Failed to send notifications: {EventType} for bot {BotId}", eventType, Identifier); } } - } \ No newline at end of file diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index 780ec88d..7e4a93d8 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -46,7 +46,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Set up reminder for daily snapshots using precise timing var now = DateTime.UtcNow; - + // Daily reminder - runs at midnight (00:00 UTC) var nextDailyTime = CandleHelpers.GetNextExpectedCandleTime(Timeframe.OneDay, now); var timeUntilNextDay = nextDailyTime - now; @@ -98,7 +98,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _state.State.TotalActiveStrategies = totalActiveStrategies; _state.State.TotalPlatformVolume = totalVolume; _state.State.TotalPlatformPnL = totalPnL; - _state.State.TotalOpenInterest = totalOpenInterest; + _state.State.OpenInterest = totalOpenInterest; _state.State.TotalPositionCount = totalPositionCount; _state.State.LastUpdated = DateTime.UtcNow; _state.State.HasPendingChanges = false; @@ -225,7 +225,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable public Task GetTotalOpenInterest() { - return Task.FromResult(_state.State.TotalOpenInterest); + return Task.FromResult(_state.State.OpenInterest); } public Task GetTotalPositionCountAsync() @@ -282,21 +282,15 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable try { _logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}", - evt.PositionId, evt.Ticker, evt.RealizedPnL); + evt.PositionIdentifier, evt.Ticker, evt.RealizedPnL); // Validate event data - if (evt == null || evt.PositionId == Guid.Empty || evt.Ticker == Ticker.Unknown) + if (evt == null || evt.PositionIdentifier == Guid.Empty || evt.Ticker == Ticker.Unknown) { _logger.LogWarning("Invalid PositionClosedEvent received: {Event}", evt); return; } - // Ensure position count doesn't go negative - if (_state.State.TotalPositionCount > 0) - { - _state.State.TotalPositionCount--; - } - _state.State.TotalPlatformVolume += evt.Volume; _state.State.TotalPlatformPnL += evt.RealizedPnL; @@ -308,9 +302,9 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } _state.State.VolumeByAsset[asset] += evt.Volume; - + // Update open interest (subtract the closed position's volume) - _state.State.TotalOpenInterest = Math.Max(0, _state.State.TotalOpenInterest - evt.Volume); + _state.State.OpenInterest = Math.Max(0, _state.State.OpenInterest - evt.Volume); _state.State.HasPendingChanges = true; await _state.WriteStateAsync(); @@ -321,17 +315,17 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } } - public async Task OnTradeExecutedAsync(TradeExecutedEvent evt) + public async Task OnPositionOpenAsync(PositionOpenEvent evt) { try { - _logger.LogInformation("Trade executed: {TradeId} for {Ticker} with volume: {Volume}", - evt.TradeId, evt.Ticker, evt.Volume); + _logger.LogInformation("Position opened: {PositionIdentifier} for {Ticker} with volume: {Volume}", + evt.PositionIdentifier, evt.Ticker, evt.Volume); // Validate event data if (evt == null || evt.Ticker == Ticker.Unknown || evt.Volume <= 0) { - _logger.LogWarning("Invalid TradeExecutedEvent received: {Event}", evt); + _logger.LogWarning("Invalid PositionOpenEvent received: {Event}", evt); return; } @@ -344,18 +338,20 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { _state.State.VolumeByAsset[asset] = 0; } + _state.State.VolumeByAsset[asset] += evt.Volume; // Update open interest and position count // Since this is called only when position is fully open on broker, we always increase counts _state.State.TotalPositionCount++; - _state.State.TotalOpenInterest += evt.Volume; + _state.State.OpenInterest += evt.Volume; // Update position count by asset if (!_state.State.PositionCountByAsset.ContainsKey(asset)) { _state.State.PositionCountByAsset[asset] = 0; } + _state.State.PositionCountByAsset[asset]++; // Update position count by direction @@ -363,13 +359,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { _state.State.PositionCountByDirection[evt.Direction] = 0; } - _state.State.PositionCountByDirection[evt.Direction]++; - // Update PnL if provided - if (evt.PnL != 0) - { - _state.State.TotalPlatformPnL += evt.PnL; - } + _state.State.PositionCountByDirection[evt.Direction]++; // Update fees if provided if (evt.Fee > 0) @@ -409,7 +400,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _state.State.TotalActiveStrategies24hAgo = _state.State.TotalActiveStrategies; _state.State.TotalPlatformPnL24hAgo = _state.State.TotalPlatformPnL; _state.State.TotalPlatformVolume24hAgo = _state.State.TotalPlatformVolume; - _state.State.TotalOpenInterest24hAgo = _state.State.TotalOpenInterest; + _state.State.TotalOpenInterest24hAgo = _state.State.OpenInterest; _state.State.TotalPositionCount24hAgo = _state.State.TotalPositionCount; _state.State.TotalPlatformFees24hAgo = _state.State.TotalPlatformFees; @@ -421,7 +412,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable TotalStrategies = _state.State.TotalActiveStrategies, TotalVolume = _state.State.TotalPlatformVolume, TotalPnL = _state.State.TotalPlatformPnL, - TotalOpenInterest = _state.State.TotalOpenInterest, + TotalOpenInterest = _state.State.OpenInterest, TotalPositionCount = _state.State.TotalPositionCount, TotalFees = _state.State.TotalPlatformFees }; @@ -462,7 +453,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable TotalPlatformPnL = state.TotalPlatformPnL, TotalPlatformVolume = state.TotalPlatformVolume, TotalPlatformVolumeLast24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo, - TotalOpenInterest = state.TotalOpenInterest, + TotalOpenInterest = state.OpenInterest, TotalPositionCount = state.TotalPositionCount, TotalPlatformFees = state.TotalPlatformFees, @@ -471,7 +462,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable StrategiesChange24h = state.TotalActiveStrategies - state.TotalActiveStrategies24hAgo, PnLChange24h = state.TotalPlatformPnL - state.TotalPlatformPnL24hAgo, VolumeChange24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo, - OpenInterestChange24h = state.TotalOpenInterest - state.TotalOpenInterest24hAgo, + OpenInterestChange24h = state.OpenInterest - state.TotalOpenInterest24hAgo, PositionCountChange24h = state.TotalPositionCount - state.TotalPositionCount24hAgo, FeesChange24h = state.TotalPlatformFees - state.TotalPlatformFees24hAgo,