diff --git a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs
index f724b459..38d828a6 100644
--- a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs
+++ b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs
@@ -19,31 +19,6 @@ public interface IPlatformSummaryGrain : IGrainWithStringKey
///
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();
-
// Event handlers for immediate updates
///
/// Updates the active strategy count
@@ -96,4 +71,10 @@ public class PositionOpenEvent : PlatformMetricsEvent
[Id(4)] public TradeDirection Direction { get; set; }
[Id(5)] public Guid PositionIdentifier { get; set; }
+}
+
+[GenerateSerializer]
+public class PositionUpdatedEvent : PlatformMetricsEvent
+{
+ [Id(1)] public Guid PositionIdentifier { 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 1a88b1a9..74807e7e 100644
--- a/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs
+++ b/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs
@@ -11,7 +11,7 @@ public class AgentSummaryUpdateEvent
{
[Id(0)] public Guid BotId { get; set; }
- [Id(1)] public AgentSummaryEventType EventType { get; set; }
+ [Id(1)] public NotificationEventType EventType { get; set; }
[Id(2)] public DateTime Timestamp { get; set; } = DateTime.UtcNow;
diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs
index 2deba19c..cc7d8c30 100644
--- a/src/Managing.Application.Abstractions/Services/ITradingService.cs
+++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs
@@ -19,7 +19,6 @@ public interface ITradingService
Task GetScenarioByNameAsync(string scenario);
Task InsertPositionAsync(Position position);
Task UpdatePositionAsync(Position position);
- Task UpdateTradeAsync(Trade trade);
Task GetIndicatorByNameAsync(string strategy);
Task InsertScenarioAsync(Scenario scenario);
Task InsertIndicatorAsync(IndicatorBase indicatorBase);
diff --git a/src/Managing.Application.Tests/AgentGrainTests.cs b/src/Managing.Application.Tests/AgentGrainTests.cs
index 5f59cfee..70790e37 100644
--- a/src/Managing.Application.Tests/AgentGrainTests.cs
+++ b/src/Managing.Application.Tests/AgentGrainTests.cs
@@ -51,7 +51,7 @@ public class AgentGrainTests
var updateEvent = new AgentSummaryUpdateEvent
{
BotId = botId,
- EventType = AgentSummaryEventType.PositionOpened,
+ EventType = NotificationEventType.PositionOpened,
Timestamp = DateTime.UtcNow
};
@@ -76,7 +76,7 @@ public class AgentGrainTests
var updateEvent = new AgentSummaryUpdateEvent
{
BotId = Guid.NewGuid(), // Different bot ID
- EventType = AgentSummaryEventType.PositionOpened,
+ EventType = NotificationEventType.PositionOpened,
Timestamp = DateTime.UtcNow
};
diff --git a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
index d27b5e7f..5f8cdb7e 100644
--- a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
+++ b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
@@ -1,4 +1,3 @@
-using Managing.Application.Abstractions.Models;
using Managing.Application.Bots.Models;
using Orleans.Concurrency;
@@ -20,28 +19,38 @@ namespace Managing.Application.Abstractions.Grains
[OneWay]
Task UpdateAgentNameAsync(string agentName);
- ///
- /// Generates a summary of the agent's stats for the AgentRegistryGrain.
- ///
- [OneWay]
- Task UpdateSummary();
-
///
/// Registers a new bot with this agent.
///
+ [OneWay]
Task RegisterBotAsync(Guid botId);
///
/// Unregisters a bot from this agent.
///
+ [OneWay]
Task UnregisterBotAsync(Guid botId);
///
- /// Handles stream notifications for agent summary updates.
+ /// Handles position opened events for real-time agent summary updates.
///
- /// The update event from the stream]
+ /// The position opened event
[OneWay]
- Task OnAgentSummaryUpdateAsync(AgentSummaryUpdateEvent updateEvent);
+ Task OnPositionOpenedAsync(PositionOpenEvent evt);
+
+ ///
+ /// Handles position closed events for real-time agent summary updates.
+ ///
+ /// The position closed event
+ [OneWay]
+ Task OnPositionClosedAsync(PositionClosedEvent evt);
+
+ ///
+ /// Handles position update events for real-time PnL and status updates.
+ ///
+ /// The position update event
+ [OneWay]
+ Task OnPositionUpdatedAsync(PositionUpdatedEvent evt);
///
/// Coordinates ETH balance checking and swapping for all bots under this agent.
diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs
index b0ac92d6..32c78061 100644
--- a/src/Managing.Application/Bots/Grains/AgentGrain.cs
+++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs
@@ -1,6 +1,6 @@
+#nullable enable
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
-using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots.Models;
using Managing.Application.Orleans;
@@ -91,40 +91,107 @@ public class AgentGrain : Grain, IAgentGrain
_logger.LogInformation("Agent {UserId} updated with name {AgentName}", this.GetPrimaryKeyLong(), agentName);
}
- public async Task OnAgentSummaryUpdateAsync(AgentSummaryUpdateEvent updateEvent)
+ public async Task OnPositionOpenedAsync(PositionOpenEvent evt)
{
try
{
- _logger.LogInformation("Received agent summary update event for user {UserId}, event type: {EventType}",
- this.GetPrimaryKeyLong(), updateEvent.EventType);
+ _logger.LogInformation("Position opened event received for user {UserId}, position: {PositionId}",
+ this.GetPrimaryKeyLong(), evt.PositionIdentifier);
- // Only update summary if the event is for this agent's bots
- if (_state.State.BotIds.Contains(updateEvent.BotId))
- {
- await UpdateSummary();
- }
+ // Update event-driven metrics
+ _state.State.TotalVolume += evt.Volume;
+ _state.State.TotalFees += evt.Fee;
+ _state.State.LastSummaryUpdate = DateTime.UtcNow;
+
+ await _state.WriteStateAsync();
+
+ // Update the agent summary with the new data
+ await UpdateSummary();
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error processing agent summary update event for user {UserId}",
+ _logger.LogError(ex, "Error processing position opened event for user {UserId}",
this.GetPrimaryKeyLong());
}
}
- public async Task UpdateSummary()
+ public async Task OnPositionClosedAsync(PositionClosedEvent evt)
{
try
{
- // Get all bots for this agent
- var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
+ _logger.LogInformation("Position closed event received for user {UserId}, position: {PositionId}, PnL: {PnL}",
+ this.GetPrimaryKeyLong(), evt.PositionIdentifier, evt.RealizedPnL);
- // Calculate aggregated statistics from bot data
- var totalPnL = bots.Sum(b => b.Pnl);
- var totalWins = bots.Sum(b => b.TradeWins);
- var totalLosses = bots.Sum(b => b.TradeLosses);
+ // Update event-driven metrics
+ _state.State.TotalPnL += evt.RealizedPnL;
+ _state.State.TotalVolume += evt.Volume;
+
+ // Update wins/losses count
+ if (evt.RealizedPnL > 0)
+ {
+ _state.State.Wins++;
+ }
+ else if (evt.RealizedPnL < 0)
+ {
+ _state.State.Losses++;
+ }
+
+ _state.State.LastSummaryUpdate = DateTime.UtcNow;
+
+ await _state.WriteStateAsync();
+
+ // Update the agent summary with the new data
+ await UpdateSummary();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing position closed event for user {UserId}",
+ this.GetPrimaryKeyLong());
+ }
+ }
+
+ public async Task OnPositionUpdatedAsync(PositionUpdatedEvent evt)
+ {
+ try
+ {
+ _logger.LogInformation("Position updated event received for user {UserId}, position: {PositionId}",
+ this.GetPrimaryKeyLong(), evt.PositionIdentifier);
+
+ // Update event-driven metrics for PnL changes
+ // Note: This is for real-time PnL updates, not cumulative like closed positions
+ _state.State.LastSummaryUpdate = DateTime.UtcNow;
+
+ await _state.WriteStateAsync();
+
+ // Update the agent summary with the new data
+ await UpdateSummary();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing position updated event for user {UserId}",
+ this.GetPrimaryKeyLong());
+ }
+ }
+
+ ///
+ /// Updates the agent summary by recalculating from position data (used for initialization or manual refresh)
+ ///
+ private async Task UpdateSummary()
+ {
+ try
+ {
+ // Get all positions for this agent's bots as initiator
+ var positions = await _tradingService.GetPositionsByInitiatorIdentifiersAsync(_state.State.BotIds);
+
+ // Calculate aggregated statistics from position data
+ var totalPnL = positions.Sum(p => p.ProfitAndLoss?.Realized ?? 0);
+ var totalVolume = positions.Sum(p => p.Open.Price * p.Open.Quantity);
+ var totalFees = positions.Sum(p => p.CalculateTotalFees());
+
+ // Calculate wins/losses from position PnL
+ var totalWins = positions.Count(p => (p.ProfitAndLoss?.Realized ?? 0) > 0);
+ var totalLosses = positions.Count(p => (p.ProfitAndLoss?.Realized ?? 0) < 0);
- // Calculate ROI based on total volume traded with proper division by zero handling
- var totalVolume = bots.Sum(b => b.Volume);
decimal totalROI;
if (totalVolume > 0)
@@ -148,11 +215,11 @@ public class AgentGrain : Grain, IAgentGrain
totalROI = 0;
}
- // Calculate Runtime based on the farthest date from bot startup times
+ // Calculate Runtime based on the earliest position date
DateTime? runtime = null;
- if (bots.Any())
+ if (positions.Any())
{
- runtime = bots.Max(b => b.StartupTime);
+ runtime = positions.Min(p => p.Date);
}
// Calculate total balance (USDC + open positions value)
@@ -192,6 +259,10 @@ public class AgentGrain : Grain, IAgentGrain
totalBalance = 0; // Set to 0 if calculation fails
}
+ // Get active strategies count from bot data (this is still needed for running bots)
+ var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
+ var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running);
+
var summary = new AgentSummary
{
UserId = (int)this.GetPrimaryKeyLong(),
@@ -201,13 +272,16 @@ public class AgentGrain : Grain, IAgentGrain
Losses = totalLosses,
TotalROI = totalROI,
Runtime = runtime,
- ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Running),
+ ActiveStrategiesCount = activeStrategiesCount,
TotalVolume = totalVolume,
TotalBalance = totalBalance,
};
// Save summary to database
await _agentService.SaveOrUpdateAgentSummary(summary);
+
+ _logger.LogDebug("Updated agent summary from position data for user {UserId}: PnL={PnL}, Volume={Volume}, Wins={Wins}, Losses={Losses}",
+ this.GetPrimaryKeyLong(), totalPnL, totalVolume, totalWins, totalLosses);
}
catch (Exception ex)
{
@@ -219,6 +293,10 @@ public class AgentGrain : Grain, IAgentGrain
{
if (_state.State.BotIds.Add(botId))
{
+ // Update active strategies count
+ _state.State.ActiveStrategiesCount = _state.State.BotIds.Count;
+ _state.State.LastSummaryUpdate = DateTime.UtcNow;
+
await _state.WriteStateAsync();
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
@@ -231,6 +309,10 @@ public class AgentGrain : Grain, IAgentGrain
{
if (_state.State.BotIds.Remove(botId))
{
+ // Update active strategies count
+ _state.State.ActiveStrategiesCount = _state.State.BotIds.Count;
+ _state.State.LastSummaryUpdate = DateTime.UtcNow;
+
await _state.WriteStateAsync();
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
diff --git a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
index d00c3dd5..56b558aa 100644
--- a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
+++ b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs
@@ -1,4 +1,3 @@
-using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Orleans;
using Microsoft.Extensions.Logging;
@@ -17,17 +16,14 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
{
private readonly IPersistentState _state;
private readonly ILogger _logger;
- private readonly IBotService _botService;
public LiveBotRegistryGrain(
[PersistentState("bot-registry", "registry-store")]
IPersistentState state,
- ILogger logger,
- IBotService botService)
+ ILogger logger)
{
_state = state;
_logger = logger;
- _botService = botService;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
@@ -196,8 +192,8 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
{
var platformGrain = GrainFactory.GetGrain("platform-summary");
await platformGrain.UpdateActiveStrategyCountAsync(_state.State.ActiveBotsCount);
-
- _logger.LogDebug("Notified platform summary about active strategy count change. New count: {ActiveCount}",
+
+ _logger.LogDebug("Notified platform summary about active strategy count change. New count: {ActiveCount}",
_state.State.ActiveBotsCount);
}
catch (Exception ex)
diff --git a/src/Managing.Application/Bots/Models/AgentGrainState.cs b/src/Managing.Application/Bots/Models/AgentGrainState.cs
index 568499f8..e44565af 100644
--- a/src/Managing.Application/Bots/Models/AgentGrainState.cs
+++ b/src/Managing.Application/Bots/Models/AgentGrainState.cs
@@ -24,6 +24,49 @@ namespace Managing.Application.Bots.Models
///
[Id(4)]
public CachedBalanceData? CachedBalanceData { get; set; } = null;
+
+ // Event-driven metrics for real-time updates
+ ///
+ /// Total PnL calculated from position events
+ ///
+ [Id(5)]
+ public decimal TotalPnL { get; set; } = 0;
+
+ ///
+ /// Total volume calculated from position events
+ ///
+ [Id(6)]
+ public decimal TotalVolume { get; set; } = 0;
+
+ ///
+ /// Total wins count from closed positions
+ ///
+ [Id(7)]
+ public int Wins { get; set; } = 0;
+
+ ///
+ /// Total losses count from closed positions
+ ///
+ [Id(8)]
+ public int Losses { get; set; } = 0;
+
+ ///
+ /// Total fees paid from position events
+ ///
+ [Id(9)]
+ public decimal TotalFees { get; set; } = 0;
+
+ ///
+ /// Active strategies count (updated by bot registration/unregistration)
+ ///
+ [Id(10)]
+ public int ActiveStrategiesCount { get; set; } = 0;
+
+ ///
+ /// Last time the summary was updated
+ ///
+ [Id(11)]
+ public DateTime LastSummaryUpdate { get; set; } = DateTime.UtcNow;
}
///
diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs
index 754d9f92..4bf61b9b 100644
--- a/src/Managing.Application/Bots/TradingBotBase.cs
+++ b/src/Managing.Application/Bots/TradingBotBase.cs
@@ -1,6 +1,5 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
-using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Application.Trading.Handlers;
@@ -95,10 +94,6 @@ public class TradingBotBase : ITradingBot
$"📢 I'll notify you when signals are triggered.";
await LogInformation(startupMessage);
-
- // Notify AgentGrain about bot startup
- await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.BotStarted,
- $"Bot: {Config.Name}, Ticker: {Config.Ticker}");
break;
case BotStatus.Running:
@@ -393,36 +388,29 @@ public class TradingBotBase : ITradingBot
}
});
+ NotificationEventType eventType = NotificationEventType.PositionUpdated;
if (!Config.IsForBacktest)
{
var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker);
if (brokerPosition != null)
{
- // Calculate net PnL after fees for broker position
+ var previousPositionStatus = internalPosition.Status;
+ // Position found on the broker, means the position is filled
var brokerNetPnL = brokerPosition.GetNetPnL();
UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL);
internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerNetPnL };
internalPosition.Status = PositionStatus.Filled;
+ await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Filled);
- // Update Open trade status when position is found on broker
- if (internalPosition.Open != null)
+ internalPosition.Open.SetStatus(TradeStatus.Filled);
+ positionForSignal.Open.SetStatus(TradeStatus.Filled);
+ eventType = NotificationEventType.PositionOpened;
+
+ await UpdatePositionDatabase(internalPosition);
+
+ if (previousPositionStatus != PositionStatus.Filled && internalPosition.Status == PositionStatus.Filled)
{
- internalPosition.Open.SetStatus(TradeStatus.Filled);
- }
-
- // Also update the position in the bot's positions dictionary
- if (positionForSignal.Open != null)
- {
- positionForSignal.Open.SetStatus(TradeStatus.Filled);
- }
-
- if (internalPosition.Status.Equals(PositionStatus.New))
- {
- await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Filled);
-
- // Notify platform summary about the executed trade
- await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened,
- $"Position found on broker: {internalPosition.Identifier}", internalPosition);
+ await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionUpdated, internalPosition);
}
}
else
@@ -431,18 +419,6 @@ public class TradingBotBase : ITradingBot
if (internalPosition.Status.Equals(PositionStatus.Filled))
{
internalPosition.Status = PositionStatus.Finished;
-
- // Update Open trade status when position becomes Finished
- if (internalPosition.Open != null)
- {
- internalPosition.Open.SetStatus(TradeStatus.Filled);
- }
-
- // Also update the position in the bot's positions dictionary
- if (positionForSignal.Open != null)
- {
- positionForSignal.Open.SetStatus(TradeStatus.Filled);
- }
}
}
}
@@ -496,6 +472,7 @@ public class TradingBotBase : ITradingBot
}
else if (ordersCount == 2)
{
+ // TODO: This should never happen, but just in case
// 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...");
@@ -532,8 +509,7 @@ public class TradingBotBase : ITradingBot
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}",
+ await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened,
internalPosition);
}
else
@@ -560,8 +536,7 @@ public class TradingBotBase : ITradingBot
{
await HandleClosedPosition(positionForSignal);
}
- else if (internalPosition.Status == PositionStatus.Filled ||
- internalPosition.Status == PositionStatus.PartiallyFilled)
+ else if (internalPosition.Status == PositionStatus.Filled)
{
Candle lastCandle = null;
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService =>
@@ -600,6 +575,7 @@ public class TradingBotBase : ITradingBot
}
}
+ // For backtest and to make sure position is closed based on SL and TP
if (positionForSignal.OriginDirection == TradeDirection.Long)
{
if (positionForSignal.StopLoss.Price >= lastCandle.Low)
@@ -779,36 +755,7 @@ public class TradingBotBase : ITradingBot
}
}
- 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;
- // Apply fees to the internal position PnL before saving
- if (internalPosition.ProfitAndLoss != null)
- {
- var totalFees = internalPosition.CalculateTotalFees();
- internalPosition.ProfitAndLoss.Realized = internalPosition.ProfitAndLoss.Realized - totalFees;
- }
-
- // Update Open trade status when position is updated to Filled
- if (internalPosition.Open != null)
- {
- internalPosition.Open.SetStatus(TradeStatus.Filled);
- }
-
- // Also update the position in the bot's positions dictionary
- if (positionForSignal.Open != null)
- {
- positionForSignal.Open.SetStatus(TradeStatus.Filled);
- }
-
- // Save updated position to database
- await tradingService.UpdatePositionAsync(internalPosition);
- });
- }
+
}
catch (Exception ex)
{
@@ -818,6 +765,14 @@ public class TradingBotBase : ITradingBot
}
}
+ private async Task UpdatePositionDatabase(Position position)
+ {
+ await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService =>
+ {
+ await tradingService.UpdatePositionAsync(position);
+ });
+ }
+
private async Task OpenPosition(LightSignal signal)
{
Logger.LogInformation($"Opening position for {signal.Identifier}");
@@ -936,8 +891,8 @@ public class TradingBotBase : ITradingBot
}
// Notify AgentGrain about position opening
- await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened,
- $"Signal: {signal.Identifier}", position);
+ await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened,
+ position);
Logger.LogInformation($"Position requested");
return position; // Return the created position without adding to list
@@ -1418,13 +1373,6 @@ public class TradingBotBase : ITradingBot
string.Format("💰 **Balance Updated**\nNew bot trading balance: `${0:F2}`",
Config.BotTradingBalance));
}
-
- // Notify AgentGrain about position closing
- var pnlInfo = position.ProfitAndLoss?.Realized != null
- ? string.Format("PnL: {0:F2}", position.ProfitAndLoss.Realized)
- : "PnL: Unknown";
- await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionClosed,
- string.Format("Signal: {0}, {1}", position.SignalIdentifier, pnlInfo), position);
}
else
{
@@ -1555,7 +1503,7 @@ public class TradingBotBase : ITradingBot
public decimal GetProfitAndLoss()
{
// Calculate net PnL after deducting fees for each position
- var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null && p.IsFinished())
+ var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null)
.Sum(p => p.GetNetPnL());
return netPnl;
}
@@ -2120,13 +2068,12 @@ public class TradingBotBase : ITradingBot
}
///
- /// Notifies both AgentGrain and PlatformSummaryGrain about bot events
+ /// Notifies both AgentGrain and PlatformSummaryGrain about bot events using unified event data
///
- /// The type of event (e.g., PositionOpened, PositionClosed)
- /// Optional additional context data
+ /// The type of event (e.g., PositionOpened, PositionClosed, PositionUpdated)
/// Optional position data for platform summary events
- private async Task NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType eventType, string additionalData = null,
- Position position = null)
+ private async Task NotifyAgentAndPlatformGrainAsync(NotificationEventType eventType,
+ Position position)
{
if (Config.IsForBacktest)
{
@@ -2137,33 +2084,13 @@ public class TradingBotBase : ITradingBot
{
await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory =>
{
- // Notify AgentGrain (user-specific metrics)
- if (Account?.User != null)
- {
- var agentGrain = grainFactory.GetGrain(Account.User.Id);
-
- var updateEvent = new AgentSummaryUpdateEvent
- {
- BotId = Identifier,
- EventType = eventType,
- Timestamp = DateTime.UtcNow,
- AdditionalData = additionalData
- };
-
- await agentGrain.OnAgentSummaryUpdateAsync(updateEvent);
- Logger.LogDebug("Sent agent notification: {EventType} for bot {BotId}", eventType, Identifier);
- }
-
- // Notify PlatformSummaryGrain (platform-wide metrics)
+ var agentGrain = grainFactory.GetGrain(Account.User.Id);
var platformGrain = grainFactory.GetGrain("platform-summary");
+ // Create unified event objects based on event type
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);
+ case NotificationEventType.PositionOpened:
var positionOpenEvent = new PositionOpenEvent
{
PositionIdentifier = position.Identifier,
@@ -2172,10 +2099,15 @@ public class TradingBotBase : ITradingBot
Fee = position.GasFees + position.UiFees,
Direction = position.OriginDirection
};
+
+ await agentGrain.OnPositionOpenedAsync(positionOpenEvent);
await platformGrain.OnPositionOpenAsync(positionOpenEvent);
+
+ Logger.LogDebug("Sent position opened event to both grains for position {PositionId}",
+ position.Identifier);
break;
- case AgentSummaryEventType.PositionClosed when position != null:
+ case NotificationEventType.PositionClosed:
var positionClosedEvent = new PositionClosedEvent
{
PositionIdentifier = position.Identifier,
@@ -2183,8 +2115,23 @@ public class TradingBotBase : ITradingBot
RealizedPnL = position.ProfitAndLoss?.Realized ?? 0,
Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage,
};
+
+ await agentGrain.OnPositionClosedAsync(positionClosedEvent);
await platformGrain.OnPositionClosedAsync(positionClosedEvent);
- Logger.LogDebug("Sent platform position closed notification for position {PositionId}",
+
+ Logger.LogDebug("Sent position closed event to both grains for position {PositionId}",
+ position.Identifier);
+ break;
+
+ case NotificationEventType.PositionUpdated:
+ var positionUpdatedEvent = new PositionUpdatedEvent
+ {
+ PositionIdentifier = position.Identifier,
+ };
+
+ await agentGrain.OnPositionUpdatedAsync(positionUpdatedEvent);
+
+ Logger.LogDebug("Sent position updated event to both grains for position {PositionId}",
position.Identifier);
break;
}
diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs
index 8bb7624a..8f6bd6e6 100644
--- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs
+++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs
@@ -71,13 +71,13 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
TotalOpenInterest = 0,
TotalPositionCount = 0,
};
-
+
_state.State.DailySnapshots.Add(initialSnapshot);
_state.State.LastSnapshot = today;
_state.State.LastUpdated = today;
_logger.LogInformation("Created initial empty daily snapshot for {Date}", today);
}
-
+
await RefreshDataAsync();
}
}
@@ -108,7 +108,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
// Calculate volume from strategies
var totalVolume = strategies.Sum(s => s.Volume);
-
+
// Calculate PnL directly from database positions (closed positions only)
var totalPnL = await _tradingService.GetGlobalPnLFromPositionsAsync();
@@ -118,7 +118,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
// Update state
_state.State.TotalAgents = totalAgents;
_state.State.TotalActiveStrategies = totalActiveStrategies;
-
+
// Only update volume if it hasn't been updated by events recently
// This preserves real-time volume updates from position events
if (!_state.State.VolumeUpdatedByEvents)
@@ -130,7 +130,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{
_logger.LogDebug("Preserving event-updated volume: {Volume}", _state.State.TotalPlatformVolume);
}
-
+
_state.State.TotalPlatformPnL = totalPnL;
_state.State.OpenInterest = totalOpenInterest;
_state.State.TotalPositionCount = totalPositionCount;
@@ -254,31 +254,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
}
}
- public Task GetTotalVolumeAsync()
- {
- return Task.FromResult(_state.State.TotalPlatformVolume);
- }
-
- public Task GetTotalPnLAsync()
- {
- return Task.FromResult(_state.State.TotalPlatformPnL);
- }
-
- public Task GetTotalOpenInterest()
- {
- return Task.FromResult(_state.State.OpenInterest);
- }
-
- public Task GetTotalPositionCountAsync()
- {
- return Task.FromResult(_state.State.TotalPositionCount);
- }
-
- public Task GetTotalFeesAsync()
- {
- return Task.FromResult(_state.State.TotalPlatformFees);
- }
-
// Event handlers for immediate updates
public async Task UpdateActiveStrategyCountAsync(int newActiveCount)
{
@@ -303,7 +278,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
}
}
-
public async Task OnPositionClosedAsync(PositionClosedEvent evt)
{
try
@@ -319,7 +293,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
}
_state.State.TotalPlatformVolume += evt.Volume;
-
+
// PnL is now calculated directly from database positions, not from events
// This ensures accuracy and prevents double-counting issues
// Refresh PnL from database to get the latest accurate value
@@ -427,7 +401,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
}
}
-
private async Task TakeDailySnapshotAsync()
{
_logger.LogInformation("Taking daily snapshot");
diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs
index 939905e5..bc48989c 100644
--- a/src/Managing.Application/Trading/TradingService.cs
+++ b/src/Managing.Application/Trading/TradingService.cs
@@ -178,27 +178,11 @@ public class TradingService : ITradingService
return position;
}
-
public async Task UpdatePositionAsync(Position position)
{
await _tradingRepository.UpdatePositionAsync(position);
}
- public async Task UpdateTradeAsync(Trade trade)
- {
- await _tradingRepository.UpdateTradeAsync(trade);
- }
-
- public async Task> GetPositionsAsync()
- {
- var positions = new List();
- positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.New));
- positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.Filled));
- positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.PartiallyFilled));
- return positions;
- }
-
-
public async Task WatchTrader()
{
var availableTickers = new List { Ticker.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK };
@@ -268,7 +252,8 @@ public class TradingService : ITradingService
return await _tradingRepository.GetPositionsByInitiatorIdentifierAsync(initiatorIdentifier);
}
- public async Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers)
+ public async Task> GetPositionsByInitiatorIdentifiersAsync(
+ IEnumerable initiatorIdentifiers)
{
return await _tradingRepository.GetPositionsByInitiatorIdentifiersAsync(initiatorIdentifiers);
}
@@ -385,10 +370,11 @@ public class TradingService : ITradingService
var account = await _accountRepository.GetAccountByKeyAsync(publicAddress);
if (account != null && account.IsGmxInitialized)
{
- _logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX", publicAddress);
- return new PrivyInitAddressResponse
- {
- Success = true,
+ _logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX",
+ publicAddress);
+ return new PrivyInitAddressResponse
+ {
+ Success = true,
Address = publicAddress,
IsAlreadyInitialized = true
};
diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs
index a1dee9d4..4ea695d3 100644
--- a/src/Managing.Common/Enums.cs
+++ b/src/Managing.Common/Enums.cs
@@ -205,10 +205,9 @@ public static class Enums
Canceled = 1,
Rejected = 2,
Updating = 3,
- PartiallyFilled = 4,
- Filled = 5,
- Flipped = 6,
- Finished = 7
+ Filled = 4,
+ Flipped = 5,
+ Finished = 6
}
public enum PositionInitiator
@@ -492,10 +491,11 @@ public static class Enums
///
/// Event types for agent summary updates
///
- public enum AgentSummaryEventType
+ public enum NotificationEventType
{
BotStarted,
PositionOpened,
PositionClosed,
+ PositionUpdated
}
}
\ No newline at end of file
diff --git a/src/Managing.Domain/Trades/Position.cs b/src/Managing.Domain/Trades/Position.cs
index c8cdb64f..2304f155 100644
--- a/src/Managing.Domain/Trades/Position.cs
+++ b/src/Managing.Domain/Trades/Position.cs
@@ -105,7 +105,7 @@ namespace Managing.Domain.Trades
return 0;
}
- return ProfitAndLoss.Realized - CalculateTotalFees();
+ return ProfitAndLoss.Realized;
}
///