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,