From 4a45d6c9702d399874680352b7160b1b6b8aa46e Mon Sep 17 00:00:00 2001 From: cryptooda Date: Thu, 14 Aug 2025 19:44:33 +0700 Subject: [PATCH] Add platform grain --- .../Controllers/DataController.cs | 79 +- .../Extensions/PlatformSummaryExtensions.cs | 40 + .../Models/Responses/AgentSummaryViewModel.cs | 68 + .../Grains/IPlatformSummaryGrain.cs | 118 ++ .../Grains/PlatformSummaryGrainState.cs | 68 + .../Models/PlatformSummaryViewModel.cs | 102 ++ .../Bots/Grains/LiveTradingBotGrain.cs | 14 +- .../Grains/PlatformSummaryGrain.cs | 416 +++++ src/Managing.Domain/Bots/Bot.cs | 2 + ...814123925_AddBotPositionCounts.Designer.cs | 1428 +++++++++++++++++ .../20250814123925_AddBotPositionCounts.cs | 40 + .../PostgreSql/Entities/BotEntity.cs | 2 + .../PostgreSql/PostgreSqlMappers.cs | 6 +- 13 files changed, 2324 insertions(+), 59 deletions(-) create mode 100644 src/Managing.Api/Extensions/PlatformSummaryExtensions.cs create mode 100644 src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs create mode 100644 src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs create mode 100644 src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs create mode 100644 src/Managing.Application/Grains/PlatformSummaryGrain.cs create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.Designer.cs create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.cs diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 41d713c..6332add 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -1,5 +1,7 @@ -using Managing.Api.Models.Requests; +using Managing.Api.Extensions; +using Managing.Api.Models.Requests; using Managing.Api.Models.Responses; +using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; @@ -36,6 +38,7 @@ public class DataController : ControllerBase private readonly IHubContext _hubContext; private readonly IMediator _mediator; private readonly ITradingService _tradingService; + private readonly IGrainFactory _grainFactory; /// /// Initializes a new instance of the class. @@ -47,6 +50,7 @@ public class DataController : ControllerBase /// SignalR hub context for real-time communication. /// Mediator for handling commands and queries. /// Service for trading operations. + /// Orleans grain factory for accessing grains. public DataController( IExchangeService exchangeService, IAccountService accountService, @@ -55,7 +59,8 @@ public class DataController : ControllerBase IAgentService agentService, IHubContext hubContext, IMediator mediator, - ITradingService tradingService) + ITradingService tradingService, + IGrainFactory grainFactory) { _exchangeService = exchangeService; _accountService = accountService; @@ -65,6 +70,7 @@ public class DataController : ControllerBase _hubContext = hubContext; _mediator = mediator; _tradingService = tradingService; + _grainFactory = grainFactory; } /// @@ -458,68 +464,31 @@ public class DataController : ControllerBase /// /// Retrieves a summary of platform activity across all agents (platform-level data only) + /// Uses Orleans grain for efficient caching and real-time updates /// /// A summary of platform activity without individual agent details [HttpGet("GetPlatformSummary")] public async Task> GetPlatformSummary() { - const string cacheKey = "PlatformSummary"; - - // Check if the platform summary is already cached - var cachedSummary = _cacheService.GetValue(cacheKey); - - if (cachedSummary != null) + try { - return Ok(cachedSummary); + // Get the platform summary grain + var platformSummaryGrain = _grainFactory.GetGrain("platform-summary"); + + // Get the platform summary from the grain (handles caching and real-time updates) + var abstractionsSummary = await platformSummaryGrain.GetPlatformSummaryAsync(); + + // Convert to API ViewModel + var summary = abstractionsSummary.ToApiViewModel(); + + return Ok(summary); } - - // Get all agents and their strategies (without time filter) - var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand()); - - // Create the platform summary - var summary = new PlatformSummaryViewModel + catch (Exception ex) { - TotalAgents = agentsWithStrategies.Count, - TotalActiveStrategies = agentsWithStrategies.Values.Sum(list => list.Count) - }; - - // Calculate total platform metrics - decimal totalPlatformPnL = 0; - decimal totalPlatformVolume = 0; - decimal totalPlatformVolumeLast24h = 0; - - // Calculate totals from all agents - foreach (var agent in agentsWithStrategies) - { - var strategies = agent.Value; - - if (strategies.Count == 0) - { - continue; // Skip agents with no strategies - } - - // TODO: Add this calculation into repository for better performance - - var globalPnL = strategies.Sum(s => s.Pnl); - var globalVolume = strategies.Sum(s => s.Volume); - var globalVolumeLast24h = strategies.Sum(s => s.Volume); - - // Calculate agent metrics for platform totals - // Add to platform totals - totalPlatformPnL += globalPnL; - totalPlatformVolume += globalVolume; - totalPlatformVolumeLast24h += globalVolumeLast24h; + // Log the error and return a fallback response + // In production, you might want to return cached data or partial data + return StatusCode(500, $"Error retrieving platform summary: {ex.Message}"); } - - // Set the platform totals - summary.TotalPlatformPnL = totalPlatformPnL; - summary.TotalPlatformVolume = totalPlatformVolume; - summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h; - - // Cache the results for 5 minutes - _cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5)); - - return Ok(summary); } diff --git a/src/Managing.Api/Extensions/PlatformSummaryExtensions.cs b/src/Managing.Api/Extensions/PlatformSummaryExtensions.cs new file mode 100644 index 0000000..060fc19 --- /dev/null +++ b/src/Managing.Api/Extensions/PlatformSummaryExtensions.cs @@ -0,0 +1,40 @@ +using Managing.Api.Models.Responses; +using AbstractionsPlatformSummaryViewModel = Managing.Application.Abstractions.Models.PlatformSummaryViewModel; + +namespace Managing.Api.Extensions; + +/// +/// Extension methods for converting between Platform Summary ViewModels +/// +public static class PlatformSummaryExtensions +{ + /// + /// Converts from the Abstractions PlatformSummaryViewModel to the API PlatformSummaryViewModel + /// + public static PlatformSummaryViewModel ToApiViewModel(this AbstractionsPlatformSummaryViewModel abstractionsModel) + { + return new PlatformSummaryViewModel + { + TotalAgents = abstractionsModel.TotalAgents, + TotalActiveStrategies = abstractionsModel.TotalActiveStrategies, + TotalPlatformPnL = abstractionsModel.TotalPlatformPnL, + TotalPlatformVolume = abstractionsModel.TotalPlatformVolume, + TotalPlatformVolumeLast24h = abstractionsModel.TotalPlatformVolumeLast24h, + TotalOpenInterest = abstractionsModel.TotalOpenInterest, + TotalPositionCount = abstractionsModel.TotalPositionCount, + AgentsChange24h = abstractionsModel.AgentsChange24h, + StrategiesChange24h = abstractionsModel.StrategiesChange24h, + PnLChange24h = abstractionsModel.PnLChange24h, + VolumeChange24h = abstractionsModel.VolumeChange24h, + OpenInterestChange24h = abstractionsModel.OpenInterestChange24h, + PositionCountChange24h = abstractionsModel.PositionCountChange24h, + VolumeByAsset = abstractionsModel.VolumeByAsset, + PositionCountByAsset = abstractionsModel.PositionCountByAsset, + PositionCountByDirection = abstractionsModel.PositionCountByDirection.ToDictionary( + kvp => kvp.Key.ToString(), + kvp => kvp.Value), + LastUpdated = abstractionsModel.LastUpdated, + Last24HourSnapshot = abstractionsModel.Last24HourSnapshot + }; + } +} diff --git a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs index 0e46709..96dd8ed 100644 --- a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs +++ b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs @@ -71,6 +71,74 @@ namespace Managing.Api.Models.Responses /// Total volume traded across all agents in the last 24 hours in USD /// public decimal TotalPlatformVolumeLast24h { get; set; } + + /// + /// Total open interest across all positions in USD + /// + public decimal TotalOpenInterest { get; set; } + + /// + /// Total number of open positions across all strategies + /// + public int TotalPositionCount { get; set; } + + // 24-hour changes + /// + /// Change in agent count over the last 24 hours + /// + public int AgentsChange24h { get; set; } + + /// + /// Change in strategy count over the last 24 hours + /// + public int StrategiesChange24h { get; set; } + + /// + /// Change in PnL over the last 24 hours + /// + public decimal PnLChange24h { get; set; } + + /// + /// Change in volume over the last 24 hours + /// + public decimal VolumeChange24h { get; set; } + + /// + /// Change in open interest over the last 24 hours + /// + public decimal OpenInterestChange24h { get; set; } + + /// + /// Change in position count over the last 24 hours + /// + public int PositionCountChange24h { get; set; } + + // Breakdowns + /// + /// Volume breakdown by asset/ticker + /// + public Dictionary VolumeByAsset { get; set; } = new(); + + /// + /// Position count breakdown by asset/ticker + /// + public Dictionary PositionCountByAsset { get; set; } = new(); + + /// + /// Position count breakdown by direction (Long/Short) + /// + public Dictionary PositionCountByDirection { get; set; } = new(); + + // Metadata + /// + /// When the data was last updated + /// + public DateTime LastUpdated { get; set; } + + /// + /// When the last 24-hour snapshot was taken + /// + public DateTime Last24HourSnapshot { get; set; } } /// diff --git a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs new file mode 100644 index 0000000..0162e41 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs @@ -0,0 +1,118 @@ +using Managing.Application.Abstractions.Models; +using Orleans; +using static Managing.Common.Enums; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Grain interface for managing platform-wide summary metrics +/// +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(); + + // Event handlers for immediate updates + Task OnStrategyDeployedAsync(StrategyDeployedEvent evt); + Task OnStrategyStoppedAsync(StrategyStoppedEvent evt); + Task OnPositionOpenedAsync(PositionOpenedEvent evt); + Task OnPositionClosedAsync(PositionClosedEvent evt); + Task OnTradeExecutedAsync(TradeExecutedEvent evt); +} + +/// +/// Base class for platform metrics events +/// +public abstract class PlatformMetricsEvent +{ + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} + +/// +/// Event fired when a new strategy is deployed +/// +public class StrategyDeployedEvent : PlatformMetricsEvent +{ + public Guid StrategyId { get; set; } + public string AgentName { get; set; } = string.Empty; + public string StrategyName { get; set; } = string.Empty; + public decimal InitialVolume { get; set; } + public decimal InitialPnL { get; set; } +} + +/// +/// Event fired when a strategy is stopped +/// +public class StrategyStoppedEvent : PlatformMetricsEvent +{ + public Guid StrategyId { get; set; } + public string AgentName { get; set; } = string.Empty; + public string StrategyName { get; set; } = string.Empty; +} + +/// +/// Event fired when a new position is opened +/// +public class PositionOpenedEvent : PlatformMetricsEvent +{ + public Guid PositionId { get; set; } + public Guid StrategyId { get; set; } + public string Ticker { get; set; } = string.Empty; + public decimal Size { get; set; } + public decimal NotionalValue { get; set; } + public TradeDirection Direction { get; set; } +} + +/// +/// Event fired when a position is closed +/// +public class PositionClosedEvent : PlatformMetricsEvent +{ + public Guid PositionId { get; set; } + public Guid StrategyId { get; set; } + public string Ticker { get; set; } = string.Empty; + public decimal RealizedPnL { get; set; } + public decimal Volume { get; set; } +} + +/// +/// Event fired when a trade is executed +/// +public class TradeExecutedEvent : PlatformMetricsEvent +{ + public Guid TradeId { get; set; } + public Guid PositionId { get; set; } + public Guid StrategyId { get; set; } + public string Ticker { get; set; } = string.Empty; + public decimal Volume { get; set; } + public decimal PnL { get; set; } + public decimal Fee { get; set; } + public TradeDirection Direction { get; set; } +} diff --git a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs new file mode 100644 index 0000000..e733364 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs @@ -0,0 +1,68 @@ +using static Managing.Common.Enums; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// State model for Platform Summary Grain +/// +public class PlatformSummaryGrainState +{ + public DateTime LastUpdated { get; set; } + public DateTime LastSnapshot { get; set; } + public bool HasPendingChanges { get; set; } + + // Current metrics + public int TotalAgents { get; set; } + public int TotalActiveStrategies { get; set; } + public decimal TotalPlatformPnL { get; set; } + public decimal TotalPlatformVolume { get; set; } + public decimal TotalOpenInterest { get; set; } + public int TotalPositionCount { get; set; } + + // 24-hour ago values (for comparison) + public int TotalAgents24hAgo { get; set; } + public int TotalActiveStrategies24hAgo { get; set; } + public decimal TotalPlatformPnL24hAgo { get; set; } + public decimal TotalPlatformVolume24hAgo { get; set; } + public decimal TotalOpenInterest24hAgo { get; set; } + public int TotalPositionCount24hAgo { get; set; } + + // Historical snapshots + public List HourlySnapshots { get; set; } = new(); + public List DailySnapshots { get; set; } = new(); + + // Volume breakdown by asset + public Dictionary VolumeByAsset { get; set; } = new(); + + // Position count breakdown + public Dictionary PositionCountByAsset { get; set; } = new(); + public Dictionary PositionCountByDirection { get; set; } = new(); +} + +/// +/// Hourly snapshot of platform metrics +/// +public class HourlySnapshot +{ + public DateTime Timestamp { get; set; } + public int TotalAgents { get; set; } + public int TotalStrategies { get; set; } + public decimal TotalVolume { get; set; } + public decimal TotalPnL { get; set; } + public decimal TotalOpenInterest { get; set; } + public int TotalPositionCount { get; set; } +} + +/// +/// Daily snapshot of platform metrics +/// +public class DailySnapshot +{ + public DateTime Date { get; set; } + public int TotalAgents { get; set; } + public int TotalStrategies { get; set; } + public decimal TotalVolume { get; set; } + public decimal TotalPnL { get; set; } + public decimal TotalOpenInterest { get; set; } + public int TotalPositionCount { get; set; } +} diff --git a/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs b/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs new file mode 100644 index 0000000..a10268c --- /dev/null +++ b/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs @@ -0,0 +1,102 @@ +using static Managing.Common.Enums; + +namespace Managing.Application.Abstractions.Models; + +/// +/// Platform-wide statistics without individual agent details +/// +public class PlatformSummaryViewModel +{ + /// + /// Total number of agents on the platform + /// + public int TotalAgents { get; set; } + + /// + /// Total number of active strategies across all agents + /// + public int TotalActiveStrategies { get; set; } + + /// + /// Total platform-wide profit and loss in USD + /// + public decimal TotalPlatformPnL { get; set; } + + /// + /// Total volume traded across all agents in USD + /// + public decimal TotalPlatformVolume { get; set; } + + /// + /// Total volume traded across all agents in the last 24 hours in USD + /// + public decimal TotalPlatformVolumeLast24h { get; set; } + + /// + /// Total open interest across all positions in USD + /// + public decimal TotalOpenInterest { get; set; } + + /// + /// Total number of open positions across all strategies + /// + public int TotalPositionCount { get; set; } + + // 24-hour changes + /// + /// Change in agent count over the last 24 hours + /// + public int AgentsChange24h { get; set; } + + /// + /// Change in strategy count over the last 24 hours + /// + public int StrategiesChange24h { get; set; } + + /// + /// Change in PnL over the last 24 hours + /// + public decimal PnLChange24h { get; set; } + + /// + /// Change in volume over the last 24 hours + /// + public decimal VolumeChange24h { get; set; } + + /// + /// Change in open interest over the last 24 hours + /// + public decimal OpenInterestChange24h { get; set; } + + /// + /// Change in position count over the last 24 hours + /// + public int PositionCountChange24h { get; set; } + + // Breakdowns + /// + /// Volume breakdown by asset/ticker + /// + public Dictionary VolumeByAsset { get; set; } = new(); + + /// + /// Position count breakdown by asset/ticker + /// + public Dictionary PositionCountByAsset { get; set; } = new(); + + /// + /// Position count breakdown by direction (Long/Short) + /// + public Dictionary PositionCountByDirection { get; set; } = new(); + + // Metadata + /// + /// When the data was last updated + /// + public DateTime LastUpdated { get; set; } + + /// + /// When the last 24-hour snapshot was taken + /// + public DateTime Last24HourSnapshot { get; set; } +} diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index f569aa9..ce8a187 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -633,6 +633,12 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable .Sum(p => p.Open.Quantity * p.Open.Price); var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0; + // Calculate long and short position counts + var longPositionCount = _tradingBot.Positions.Values + .Count(p => p.OriginDirection == TradeDirection.Long); + var shortPositionCount = _tradingBot.Positions.Values + .Count(p => p.OriginDirection == TradeDirection.Short); + // Create complete Bot object with all statistics bot = new Bot { @@ -648,7 +654,9 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable Pnl = pnl, Roi = roi, Volume = volume, - Fees = fees + Fees = fees, + LongPositionCount = longPositionCount, + ShortPositionCount = shortPositionCount }; } @@ -659,8 +667,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable if (success) { _logger.LogDebug( - "Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}", - _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees); + "Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}, Long={LongPositions}, Short={ShortPositions}", + _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees, bot.LongPositionCount, bot.ShortPositionCount); } else { diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs new file mode 100644 index 0000000..3d805ee --- /dev/null +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -0,0 +1,416 @@ +using Managing.Application.Abstractions; +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Models; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Bots; +using Microsoft.Extensions.Logging; +using static Managing.Common.Enums; + +namespace Managing.Application.Grains; + +/// +/// Grain for managing platform-wide summary metrics with real-time updates and periodic snapshots +/// +public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable +{ + private readonly IPersistentState _state; + private readonly IBotService _botService; + private readonly IAgentService _agentService; + private readonly ITradingService _tradingService; + private readonly ILogger _logger; + + private const string _hourlySnapshotReminder = "HourlySnapshot"; + private const string _dailySnapshotReminder = "DailySnapshot"; + + public PlatformSummaryGrain( + [PersistentState("platform-summary-state", "platform-summary-store")] + IPersistentState state, + IBotService botService, + IAgentService agentService, + ITradingService tradingService, + ILogger logger) + { + _state = state; + _botService = botService; + _agentService = agentService; + _tradingService = tradingService; + _logger = logger; + } + + public override async Task OnActivateAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Platform Summary Grain activated"); + + // Set up reminders for periodic snapshots + await this.RegisterOrUpdateReminder(_hourlySnapshotReminder, + TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + + var now = DateTime.UtcNow; + var nextMidnight = now.Date.AddDays(1); + var timeUntilMidnight = nextMidnight - now; + await this.RegisterOrUpdateReminder(_dailySnapshotReminder, + timeUntilMidnight, TimeSpan.FromDays(1)); + + // Initial data load if state is empty + if (_state.State.LastUpdated == default) + { + await RefreshDataAsync(); + } + } + + public async Task GetPlatformSummaryAsync() + { + // If data is stale or has pending changes, refresh it + if (IsDataStale() || _state.State.HasPendingChanges) + { + await RefreshDataAsync(); + } + + return MapToViewModel(_state.State); + } + + public async Task RefreshDataAsync() + { + try + { + _logger.LogInformation("Refreshing platform summary data"); + + // Get all data in parallel for better performance + var agentsTask = _agentService.GetAllAgentSummaries(); + var strategiesTask = _botService.GetBotsAsync(); + + await Task.WhenAll(agentsTask, strategiesTask); + + var agents = await agentsTask; + var strategies = await strategiesTask; + + // Calculate totals + var totalAgents = agents.Count(); + var totalActiveStrategies = strategies.Count(s => s.Status == BotStatus.Running); + + // Calculate volume and PnL from strategies + var totalVolume = strategies.Sum(s => s.Volume); + var totalPnL = strategies.Sum(s => s.Pnl); + + // Calculate real open interest and position count from actual positions + var (totalOpenInterest, totalPositionCount) = await CalculatePositionMetricsAsync(); + + // Update state + _state.State.TotalAgents = totalAgents; + _state.State.TotalActiveStrategies = totalActiveStrategies; + _state.State.TotalPlatformVolume = totalVolume; + _state.State.TotalPlatformPnL = totalPnL; + _state.State.TotalOpenInterest = totalOpenInterest; + _state.State.TotalPositionCount = totalPositionCount; + _state.State.LastUpdated = DateTime.UtcNow; + _state.State.HasPendingChanges = false; + + // Update volume breakdown by asset + await UpdateVolumeBreakdownAsync(strategies); + + // Update position count breakdown + await UpdatePositionCountBreakdownAsync(strategies); + + await _state.WriteStateAsync(); + + _logger.LogInformation("Platform summary data refreshed successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing platform summary data"); + } + } + + private async Task UpdateVolumeBreakdownAsync(IEnumerable strategies) + { + _state.State.VolumeByAsset.Clear(); + + // Group strategies by ticker and sum their volumes + var volumeByAsset = strategies + .Where(s => s.Volume > 0) + .GroupBy(s => s.Ticker.ToString()) + .ToDictionary(g => g.Key, g => g.Sum(s => s.Volume)); + + foreach (var kvp in volumeByAsset) + { + _state.State.VolumeByAsset[kvp.Key] = kvp.Value; + } + + _logger.LogDebug("Updated volume breakdown: {AssetCount} assets with total volume {TotalVolume}", + volumeByAsset.Count, volumeByAsset.Values.Sum()); + } + + private async Task UpdatePositionCountBreakdownAsync(IEnumerable strategies) + { + _state.State.PositionCountByAsset.Clear(); + _state.State.PositionCountByDirection.Clear(); + + // Use position counts directly from bot statistics + var activeStrategies = strategies.Where(s => s.Status != BotStatus.Saved).ToList(); + + if (activeStrategies.Any()) + { + // Group by asset and sum position counts per asset + var positionsByAsset = activeStrategies + .GroupBy(s => s.Ticker.ToString()) + .ToDictionary(g => g.Key, g => g.Sum(b => b.LongPositionCount + b.ShortPositionCount)); + + // Sum long and short position counts across all bots + var totalLongPositions = activeStrategies.Sum(s => s.LongPositionCount); + var totalShortPositions = activeStrategies.Sum(s => s.ShortPositionCount); + + // Update state + foreach (var kvp in positionsByAsset) + { + _state.State.PositionCountByAsset[kvp.Key] = kvp.Value; + } + + _state.State.PositionCountByDirection[TradeDirection.Long] = totalLongPositions; + _state.State.PositionCountByDirection[TradeDirection.Short] = totalShortPositions; + + _logger.LogDebug("Updated position breakdown from bot statistics: {AssetCount} assets, Long={LongPositions}, Short={ShortPositions}", + positionsByAsset.Count, totalLongPositions, totalShortPositions); + } + else + { + _logger.LogDebug("No active strategies found for position breakdown"); + } + } + + private async Task<(decimal totalOpenInterest, int totalPositionCount)> CalculatePositionMetricsAsync() + { + try + { + // Get all open positions from all accounts + var openPositions = await _tradingService.GetBrokerPositions(null); + + if (openPositions?.Any() == true) + { + var positionCount = openPositions.Count(); + + // Calculate open interest as the sum of position notional values + // Open interest = sum of (position size * price) for all open positions + var openInterest = openPositions + .Where(p => p.Open?.Price > 0 && p.Open?.Quantity > 0) + .Sum(p => p.Open.Price * p.Open.Quantity); + + _logger.LogDebug("Calculated position metrics: {PositionCount} positions, {OpenInterest} open interest", + positionCount, openInterest); + + return (openInterest, positionCount); + } + else + { + _logger.LogDebug("No open positions found for metrics calculation"); + return (0m, 0); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to calculate position metrics, returning zero values"); + return (0m, 0); + } + } + + public Task GetTotalVolumeAsync() + { + return Task.FromResult(_state.State.TotalPlatformVolume); + } + + public Task GetTotalPnLAsync() + { + return Task.FromResult(_state.State.TotalPlatformPnL); + } + + public Task GetTotalOpenInterestAsync() + { + return Task.FromResult(_state.State.TotalOpenInterest); + } + + public Task GetTotalPositionCountAsync() + { + return Task.FromResult(_state.State.TotalPositionCount); + } + + // Event handlers for immediate updates + public async Task OnStrategyDeployedAsync(StrategyDeployedEvent evt) + { + _logger.LogInformation("Strategy deployed: {StrategyId} - {StrategyName}", evt.StrategyId, evt.StrategyName); + + _state.State.TotalActiveStrategies++; + _state.State.HasPendingChanges = true; + await _state.WriteStateAsync(); + } + + public async Task OnStrategyStoppedAsync(StrategyStoppedEvent evt) + { + _logger.LogInformation("Strategy stopped: {StrategyId} - {StrategyName}", evt.StrategyId, evt.StrategyName); + + _state.State.TotalActiveStrategies--; + _state.State.HasPendingChanges = true; + await _state.WriteStateAsync(); + } + + public async Task OnPositionOpenedAsync(PositionOpenedEvent evt) + { + _logger.LogInformation("Position opened: {PositionId} for {Ticker}", evt.PositionId, evt.Ticker); + + _state.State.TotalPositionCount++; + _state.State.TotalOpenInterest += evt.NotionalValue; + _state.State.HasPendingChanges = true; + await _state.WriteStateAsync(); + } + + public async Task OnPositionClosedAsync(PositionClosedEvent evt) + { + _logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}", + evt.PositionId, evt.Ticker, evt.RealizedPnL); + + _state.State.TotalPositionCount--; + _state.State.TotalPlatformVolume += evt.Volume; + _state.State.TotalPlatformPnL += evt.RealizedPnL; + + // Update volume by asset + var asset = evt.Ticker; + if (!_state.State.VolumeByAsset.ContainsKey(asset)) + { + _state.State.VolumeByAsset[asset] = 0; + } + _state.State.VolumeByAsset[asset] += evt.Volume; + + _state.State.HasPendingChanges = true; + await _state.WriteStateAsync(); + } + + public async Task OnTradeExecutedAsync(TradeExecutedEvent evt) + { + _logger.LogInformation("Trade executed: {TradeId} for {Ticker} with volume: {Volume}", + evt.TradeId, evt.Ticker, evt.Volume); + + _state.State.TotalPlatformVolume += evt.Volume; + _state.State.TotalPlatformPnL += evt.PnL; + + // Update volume by asset + var asset = evt.Ticker; + if (!_state.State.VolumeByAsset.ContainsKey(asset)) + { + _state.State.VolumeByAsset[asset] = 0; + } + _state.State.VolumeByAsset[asset] += evt.Volume; + + _state.State.HasPendingChanges = true; + await _state.WriteStateAsync(); + } + + // Reminder handlers for periodic snapshots + public async Task ReceiveReminder(string reminderName, TickStatus status) + { + _logger.LogInformation("Reminder received: {ReminderName}", reminderName); + + switch (reminderName) + { + case _hourlySnapshotReminder: + await TakeHourlySnapshotAsync(); + break; + case _dailySnapshotReminder: + await TakeDailySnapshotAsync(); + break; + } + } + + private async Task TakeHourlySnapshotAsync() + { + _logger.LogInformation("Taking hourly snapshot"); + + var snapshot = new HourlySnapshot + { + Timestamp = DateTime.UtcNow, + TotalAgents = _state.State.TotalAgents, + TotalStrategies = _state.State.TotalActiveStrategies, + TotalVolume = _state.State.TotalPlatformVolume, + TotalPnL = _state.State.TotalPlatformPnL, + TotalOpenInterest = _state.State.TotalOpenInterest, + TotalPositionCount = _state.State.TotalPositionCount + }; + + _state.State.HourlySnapshots.Add(snapshot); + + // Keep only last 24 hours + var cutoff = DateTime.UtcNow.AddHours(-24); + _state.State.HourlySnapshots.RemoveAll(s => s.Timestamp < cutoff); + + await _state.WriteStateAsync(); + } + + private async Task TakeDailySnapshotAsync() + { + _logger.LogInformation("Taking daily snapshot"); + + // Store 24-hour ago values for comparison + _state.State.TotalAgents24hAgo = _state.State.TotalAgents; + _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.TotalPositionCount24hAgo = _state.State.TotalPositionCount; + + // Add daily snapshot + var dailySnapshot = new DailySnapshot + { + Date = DateTime.UtcNow.Date, + TotalAgents = _state.State.TotalAgents, + TotalStrategies = _state.State.TotalActiveStrategies, + TotalVolume = _state.State.TotalPlatformVolume, + TotalPnL = _state.State.TotalPlatformPnL, + TotalOpenInterest = _state.State.TotalOpenInterest, + TotalPositionCount = _state.State.TotalPositionCount + }; + + _state.State.DailySnapshots.Add(dailySnapshot); + + // Keep only last 30 days + var cutoff = DateTime.UtcNow.AddDays(-30); + _state.State.DailySnapshots.RemoveAll(s => s.Date < cutoff); + + _state.State.LastSnapshot = DateTime.UtcNow; + + await _state.WriteStateAsync(); + } + + private bool IsDataStale() + { + var timeSinceLastUpdate = DateTime.UtcNow - _state.State.LastUpdated; + return timeSinceLastUpdate > TimeSpan.FromMinutes(5); + } + + private PlatformSummaryViewModel MapToViewModel(PlatformSummaryGrainState state) + { + return new PlatformSummaryViewModel + { + TotalAgents = state.TotalAgents, + TotalActiveStrategies = state.TotalActiveStrategies, + TotalPlatformPnL = state.TotalPlatformPnL, + TotalPlatformVolume = state.TotalPlatformVolume, + TotalPlatformVolumeLast24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo, + TotalOpenInterest = state.TotalOpenInterest, + TotalPositionCount = state.TotalPositionCount, + + // 24-hour changes + AgentsChange24h = state.TotalAgents - state.TotalAgents24hAgo, + StrategiesChange24h = state.TotalActiveStrategies - state.TotalActiveStrategies24hAgo, + PnLChange24h = state.TotalPlatformPnL - state.TotalPlatformPnL24hAgo, + VolumeChange24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo, + OpenInterestChange24h = state.TotalOpenInterest - state.TotalOpenInterest24hAgo, + PositionCountChange24h = state.TotalPositionCount - state.TotalPositionCount24hAgo, + + // Breakdowns + VolumeByAsset = state.VolumeByAsset, + PositionCountByAsset = state.PositionCountByAsset, + PositionCountByDirection = state.PositionCountByDirection, + + // Metadata + LastUpdated = state.LastUpdated, + Last24HourSnapshot = state.LastSnapshot + }; + } +} diff --git a/src/Managing.Domain/Bots/Bot.cs b/src/Managing.Domain/Bots/Bot.cs index 6f324ef..eee8436 100644 --- a/src/Managing.Domain/Bots/Bot.cs +++ b/src/Managing.Domain/Bots/Bot.cs @@ -19,6 +19,8 @@ namespace Managing.Domain.Bots public decimal Roi { get; set; } public decimal Volume { get; set; } public decimal Fees { get; set; } + public int LongPositionCount { get; set; } + public int ShortPositionCount { get; set; } } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.Designer.cs new file mode 100644 index 0000000..2e2abaf --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.Designer.cs @@ -0,0 +1,1428 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20250814123925_AddBotPositionCounts")] + partial class AddBotPositionCounts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Score"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BacktestRequestsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Fee") + .HasColumnType("decimal(18,8)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.cs b/src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.cs new file mode 100644 index 0000000..e724f62 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddBotPositionCounts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LongPositionCount", + table: "Bots", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ShortPositionCount", + table: "Bots", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LongPositionCount", + table: "Bots"); + + migrationBuilder.DropColumn( + name: "ShortPositionCount", + table: "Bots"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs index 15a3f3e..4cb182f 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs @@ -27,4 +27,6 @@ public class BotEntity public decimal Roi { get; set; } public decimal Volume { get; set; } public decimal Fees { get; set; } + public int LongPositionCount { get; set; } + public int ShortPositionCount { get; set; } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index 6436683..81bf862 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -688,7 +688,9 @@ public static class PostgreSqlMappers Pnl = entity.Pnl, Roi = entity.Roi, Volume = entity.Volume, - Fees = entity.Fees + Fees = entity.Fees, + LongPositionCount = entity.LongPositionCount, + ShortPositionCount = entity.ShortPositionCount }; return bot; @@ -713,6 +715,8 @@ public static class PostgreSqlMappers Roi = bot.Roi, Volume = bot.Volume, Fees = bot.Fees, + LongPositionCount = bot.LongPositionCount, + ShortPositionCount = bot.ShortPositionCount, UpdatedAt = DateTime.UtcNow }; }