From dab4807334e17bb5845fc6048799e241743df9b1 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 6 Oct 2025 00:55:18 +0700 Subject: [PATCH] Fix Runtime --- .../Controllers/DataController.cs | 3 + .../Responses/UserStrategyDetailsViewModel.cs | 15 +++ .../Bots/Grains/LiveTradingBotGrain.cs | 44 ++++++- .../ManageBot/BotService.cs | 3 + .../PostgreSql/PostgreSqlBotRepository.cs | 3 + .../src/generated/ManagingApi.ts | 3 + .../src/generated/ManagingApiTypes.ts | 3 + .../src/pages/dashboardPage/agentSearch.tsx | 107 +++++++++++++++++- 8 files changed, 172 insertions(+), 9 deletions(-) diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 232c9124..76e50af5 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -499,6 +499,9 @@ public class DataController : ControllerBase NetPnL = strategy.NetPnL, ROIPercentage = strategy.Roi, Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null, + LastStartTime = strategy.LastStartTime, + LastStopTime = strategy.LastStopTime, + AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds, TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(), WinRate = winRate, TotalVolumeTraded = totalVolume, diff --git a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs index 922c5561..022a4e99 100644 --- a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs +++ b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs @@ -42,6 +42,21 @@ namespace Managing.Api.Models.Responses /// public long TotalRuntimeSeconds { get; set; } + /// + /// Time when the current or last session started + /// + public DateTime? LastStartTime { get; set; } + + /// + /// Time when the last session stopped (null if currently running) + /// + public DateTime? LastStopTime { get; set; } + + /// + /// Total accumulated runtime across all past sessions (seconds) + /// + public long AccumulatedRunTimeSeconds { get; set; } + /// /// Average percentage of successful trades /// diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index bebb0976..7823934c 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -168,11 +168,15 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable _tradingBot = CreateTradingBotInstance(_state.State.Config); await _tradingBot.Start(previousStatus); - // Set startup time when bot actually starts running - _state.State.StartupTime = DateTime.UtcNow; - - // Track runtime: set LastStartTime when bot starts + // Set startup time only once (first successful start) + if (_state.State.StartupTime == default) + { + _state.State.StartupTime = DateTime.UtcNow; + } + + // Track runtime: set LastStartTime when bot starts and clear LastStopTime _state.State.LastStartTime = DateTime.UtcNow; + _state.State.LastStopTime = null; await _state.WriteStateAsync(); @@ -205,6 +209,14 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable if (status == BotStatus.Running && _tradingBot != null) { await RegisterReminder(); + // Ensure runtime timestamps are consistent if already running + if (!_state.State.LastStartTime.HasValue) + { + _state.State.LastStartTime = DateTime.UtcNow; + _state.State.LastStopTime = null; + await _state.WriteStateAsync(); + await SaveBotAsync(BotStatus.Running); + } _logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey()); return; } @@ -732,7 +744,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable try { Bot bot = null; - if (_tradingBot == null || _state.State.User == null) + if (_tradingBot == null) { // Save bot statistics for saved bots bot = new Bot @@ -754,6 +766,25 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable } else { + // Ensure we have a User reference; fetch from DB if missing + if (_state.State.User == null) + { + try + { + var existingBot = await ServiceScopeHelpers.WithScopedService( + _scopeFactory, + async botService => await botService.GetBotByIdentifier(_state.State.Identifier)); + if (existingBot?.User != null) + { + _state.State.User = existingBot.User; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unable to load user for bot {BotId} while saving stats", _state.State.Identifier); + } + } + // Calculate statistics using TradingBox helpers var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions); var pnl = _tradingBot.GetProfitAndLoss(); @@ -781,6 +812,9 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable User = _state.State.User, Status = status, StartupTime = _state.State.StartupTime, + LastStartTime = _state.State.LastStartTime, + LastStopTime = _state.State.LastStopTime, + AccumulatedRunTimeSeconds = _state.State.AccumulatedRunTimeSeconds, CreateDate = _state.State.CreateDate, TradeWins = tradeWins, TradeLosses = tradeLosses, diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index 3eae0f72..815b6797 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -359,6 +359,9 @@ namespace Managing.Application.ManageBot || existingBot.LongPositionCount != bot.LongPositionCount || existingBot.ShortPositionCount != bot.ShortPositionCount || !string.Equals(existingBot.Name, bot.Name, StringComparison.Ordinal) + || existingBot.AccumulatedRunTimeSeconds != bot.AccumulatedRunTimeSeconds + || existingBot.LastStartTime != bot.LastStartTime + || existingBot.LastStopTime != bot.LastStopTime || existingBot.Ticker != bot.Ticker) { _tradingBotLogger.LogInformation("Update bot statistics for bot {BotId}", diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs index 91d51add..b7e70089 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs @@ -76,6 +76,9 @@ public class PostgreSqlBotRepository : IBotRepository existingEntity.UpdatedAt = DateTime.UtcNow; existingEntity.LongPositionCount = bot.LongPositionCount; existingEntity.ShortPositionCount = bot.ShortPositionCount; + existingEntity.LastStartTime = bot.LastStartTime; + existingEntity.LastStopTime = bot.LastStopTime; + existingEntity.AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds; await _context.SaveChangesAsync().ConfigureAwait(false); } diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 120bc984..1aa6be4f 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -4493,6 +4493,9 @@ export interface UserStrategyDetailsViewModel { roiPercentage?: number; runtime?: Date | null; totalRuntimeSeconds?: number; + lastStartTime?: Date | null; + lastStopTime?: Date | null; + accumulatedRunTimeSeconds?: number; winRate?: number; totalVolumeTraded?: number; volumeLast24H?: number; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 65d8b486..8b6dadcd 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -976,6 +976,9 @@ export interface UserStrategyDetailsViewModel { roiPercentage?: number; runtime?: Date | null; totalRuntimeSeconds?: number; + lastStartTime?: Date | null; + lastStopTime?: Date | null; + accumulatedRunTimeSeconds?: number; winRate?: number; totalVolumeTraded?: number; volumeLast24H?: number; diff --git a/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx b/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx index 9ad8ad8d..b90beff1 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react' -import {Card, FormInput, GridTile} from '../../components/mollecules' +import {Card, FormInput, GridTile, Table} from '../../components/mollecules' import useApiUrlStore from '../../app/store/apiStore' import Plot from 'react-plotly.js' import useTheme from '../../hooks/useTheme' @@ -7,6 +7,7 @@ import { type AgentBalanceHistory, BotStatus, DataClient, + PositionStatus, type PositionViewModel, TradeDirection, type UserStrategyDetailsViewModel @@ -93,6 +94,7 @@ function AgentSearch({ index }: { index: number }) { const totalVolume = agentData.strategies.reduce((sum, strategy) => sum + (strategy.totalVolumeTraded || 0), 0) const totalWins = agentData.strategies.reduce((sum, strategy) => sum + (strategy.wins || 0), 0) const totalLosses = agentData.strategies.reduce((sum, strategy) => sum + (strategy.losses || 0), 0) + const totalRuntimeSeconds = agentData.strategies.reduce((sum, strategy) => sum + (strategy.totalRuntimeSeconds || 0), 0) const avgWinRate = agentData.strategies.length > 0 ? agentData.strategies.reduce((sum, strategy) => sum + (strategy.winRate || 0), 0) / agentData.strategies.length : 0 @@ -104,6 +106,7 @@ function AgentSearch({ index }: { index: number }) { totalWins, totalLosses, avgWinRate, + totalRuntimeSeconds, activeStrategies: agentData.strategies.filter(s => s.state === BotStatus.Running).length, totalStrategies: agentData.strategies.length } @@ -157,6 +160,70 @@ function AgentSearch({ index }: { index: number }) { const currentBalance = getCurrentBalance() const chartData = prepareChartData() const theme = useTheme().themeProperty() + const openPositions = (agentData?.positions || []).filter(p => [ + PositionStatus.New, + PositionStatus.Updating, + PositionStatus.Filled, + PositionStatus.Flipped + ].includes(p.status)) + const allPositions = agentData?.positions || [] + + const positionTableColumns = React.useMemo( + () => [ + { + Header: 'Date', + accessor: (row: PositionViewModel) => new Date(row.date).toLocaleString(), + }, + { + Header: 'Ticker', + accessor: (row: PositionViewModel) => `${row.ticker}/USDC`, + }, + { + Header: 'Direction', + accessor: (row: PositionViewModel) => row.originDirection, + }, + { + Header: 'Leverage', + accessor: (row: PositionViewModel) => `${row.Open?.leverage ?? 0}x`, + }, + { + Header: 'Quantity', + accessor: (row: PositionViewModel) => row.Open?.quantity ?? 0, + }, + { + Header: 'Entry Price', + accessor: (row: PositionViewModel) => row.Open?.price ?? 0, + }, + { + Header: 'Status', + accessor: (row: PositionViewModel) => row.status, + }, + { + Header: 'Realized PnL ($)', + accessor: (row: PositionViewModel) => row.ProfitAndLoss?.realized ?? 0, + Cell: ({ value }: { value: number }) => ( + = 0 ? 'text-green-500' : 'text-red-500'}> + {value >= 0 ? '+' : ''}{value.toFixed(2)} + + ) + }, + { + Header: 'ROI %', + accessor: (row: PositionViewModel) => { + const costBasis = (row.Open?.price ?? 0) * (row.Open?.quantity ?? 0) + if (!costBasis) return 0 + const realized = row.ProfitAndLoss?.realized ?? 0 + return (realized / costBasis) * 100 + }, + Cell: ({ value }: { value: number }) => ( + = 0 ? 'text-green-500' : 'text-red-500'}> + {value >= 0 ? '+' : ''}{value.toFixed(2)}% + + ) + }, + ], + [] + ) return (
@@ -202,10 +269,10 @@ function AgentSearch({ index }: { index: number }) { {/* Open Positions Section */} - {agentData?.positions && agentData.positions.length > 0 && ( + {openPositions.length > 0 && (
- {agentData.positions.map((position, index) => ( + {openPositions.map((position, index) => (
@@ -426,6 +493,28 @@ function AgentSearch({ index }: { index: number }) {
Fees paid across all strategies
+ + +
+
Total Runtime
+
+ {(() => { + const totalSeconds = summary.totalRuntimeSeconds || 0 + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + if (days > 0) { + return `${days}d ${hours}h` + } else if (hours > 0) { + return `${hours}h ${minutes}m` + } else { + return `${minutes}m` + } + })()} +
+
Across all strategies
+
+
)} @@ -457,7 +546,7 @@ function AgentSearch({ index }: { index: number }) { {strategy.roiPercentage && strategy.roiPercentage >= 0 ? '+' : ''}{(strategy.roiPercentage || 0).toFixed(2)}% - {strategy.totalRuntimeSeconds > 0 ? (() => { + {strategy.totalRuntimeSeconds && strategy.totalRuntimeSeconds > 0 ? (() => { const totalSeconds = strategy.totalRuntimeSeconds const days = Math.floor(totalSeconds / 86400) const hours = Math.floor((totalSeconds % 86400) / 3600) @@ -486,6 +575,16 @@ function AgentSearch({ index }: { index: number }) {
)} + + {allPositions.length > 0 && ( + + + + )} ) }