Fix Runtime

This commit is contained in:
2025-10-06 00:55:18 +07:00
parent 6cbfff38d0
commit dab4807334
8 changed files with 172 additions and 9 deletions

View File

@@ -499,6 +499,9 @@ public class DataController : ControllerBase
NetPnL = strategy.NetPnL, NetPnL = strategy.NetPnL,
ROIPercentage = strategy.Roi, ROIPercentage = strategy.Roi,
Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null, Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null,
LastStartTime = strategy.LastStartTime,
LastStopTime = strategy.LastStopTime,
AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds,
TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(), TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(),
WinRate = winRate, WinRate = winRate,
TotalVolumeTraded = totalVolume, TotalVolumeTraded = totalVolume,

View File

@@ -42,6 +42,21 @@ namespace Managing.Api.Models.Responses
/// </summary> /// </summary>
public long TotalRuntimeSeconds { get; set; } public long TotalRuntimeSeconds { get; set; }
/// <summary>
/// Time when the current or last session started
/// </summary>
public DateTime? LastStartTime { get; set; }
/// <summary>
/// Time when the last session stopped (null if currently running)
/// </summary>
public DateTime? LastStopTime { get; set; }
/// <summary>
/// Total accumulated runtime across all past sessions (seconds)
/// </summary>
public long AccumulatedRunTimeSeconds { get; set; }
/// <summary> /// <summary>
/// Average percentage of successful trades /// Average percentage of successful trades
/// </summary> /// </summary>

View File

@@ -168,11 +168,15 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
_tradingBot = CreateTradingBotInstance(_state.State.Config); _tradingBot = CreateTradingBotInstance(_state.State.Config);
await _tradingBot.Start(previousStatus); await _tradingBot.Start(previousStatus);
// Set startup time when bot actually starts running // Set startup time only once (first successful start)
if (_state.State.StartupTime == default)
{
_state.State.StartupTime = DateTime.UtcNow; _state.State.StartupTime = DateTime.UtcNow;
}
// Track runtime: set LastStartTime when bot starts // Track runtime: set LastStartTime when bot starts and clear LastStopTime
_state.State.LastStartTime = DateTime.UtcNow; _state.State.LastStartTime = DateTime.UtcNow;
_state.State.LastStopTime = null;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
@@ -205,6 +209,14 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
if (status == BotStatus.Running && _tradingBot != null) if (status == BotStatus.Running && _tradingBot != null)
{ {
await RegisterReminder(); 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()); _logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey());
return; return;
} }
@@ -732,7 +744,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
try try
{ {
Bot bot = null; Bot bot = null;
if (_tradingBot == null || _state.State.User == null) if (_tradingBot == null)
{ {
// Save bot statistics for saved bots // Save bot statistics for saved bots
bot = new Bot bot = new Bot
@@ -754,6 +766,25 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
} }
else else
{ {
// Ensure we have a User reference; fetch from DB if missing
if (_state.State.User == null)
{
try
{
var existingBot = await ServiceScopeHelpers.WithScopedService<IBotService, Bot>(
_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 // Calculate statistics using TradingBox helpers
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions); var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions);
var pnl = _tradingBot.GetProfitAndLoss(); var pnl = _tradingBot.GetProfitAndLoss();
@@ -781,6 +812,9 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
User = _state.State.User, User = _state.State.User,
Status = status, Status = status,
StartupTime = _state.State.StartupTime, StartupTime = _state.State.StartupTime,
LastStartTime = _state.State.LastStartTime,
LastStopTime = _state.State.LastStopTime,
AccumulatedRunTimeSeconds = _state.State.AccumulatedRunTimeSeconds,
CreateDate = _state.State.CreateDate, CreateDate = _state.State.CreateDate,
TradeWins = tradeWins, TradeWins = tradeWins,
TradeLosses = tradeLosses, TradeLosses = tradeLosses,

View File

@@ -359,6 +359,9 @@ namespace Managing.Application.ManageBot
|| existingBot.LongPositionCount != bot.LongPositionCount || existingBot.LongPositionCount != bot.LongPositionCount
|| existingBot.ShortPositionCount != bot.ShortPositionCount || existingBot.ShortPositionCount != bot.ShortPositionCount
|| !string.Equals(existingBot.Name, bot.Name, StringComparison.Ordinal) || !string.Equals(existingBot.Name, bot.Name, StringComparison.Ordinal)
|| existingBot.AccumulatedRunTimeSeconds != bot.AccumulatedRunTimeSeconds
|| existingBot.LastStartTime != bot.LastStartTime
|| existingBot.LastStopTime != bot.LastStopTime
|| existingBot.Ticker != bot.Ticker) || existingBot.Ticker != bot.Ticker)
{ {
_tradingBotLogger.LogInformation("Update bot statistics for bot {BotId}", _tradingBotLogger.LogInformation("Update bot statistics for bot {BotId}",

View File

@@ -76,6 +76,9 @@ public class PostgreSqlBotRepository : IBotRepository
existingEntity.UpdatedAt = DateTime.UtcNow; existingEntity.UpdatedAt = DateTime.UtcNow;
existingEntity.LongPositionCount = bot.LongPositionCount; existingEntity.LongPositionCount = bot.LongPositionCount;
existingEntity.ShortPositionCount = bot.ShortPositionCount; existingEntity.ShortPositionCount = bot.ShortPositionCount;
existingEntity.LastStartTime = bot.LastStartTime;
existingEntity.LastStopTime = bot.LastStopTime;
existingEntity.AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds;
await _context.SaveChangesAsync().ConfigureAwait(false); await _context.SaveChangesAsync().ConfigureAwait(false);
} }

View File

@@ -4493,6 +4493,9 @@ export interface UserStrategyDetailsViewModel {
roiPercentage?: number; roiPercentage?: number;
runtime?: Date | null; runtime?: Date | null;
totalRuntimeSeconds?: number; totalRuntimeSeconds?: number;
lastStartTime?: Date | null;
lastStopTime?: Date | null;
accumulatedRunTimeSeconds?: number;
winRate?: number; winRate?: number;
totalVolumeTraded?: number; totalVolumeTraded?: number;
volumeLast24H?: number; volumeLast24H?: number;

View File

@@ -976,6 +976,9 @@ export interface UserStrategyDetailsViewModel {
roiPercentage?: number; roiPercentage?: number;
runtime?: Date | null; runtime?: Date | null;
totalRuntimeSeconds?: number; totalRuntimeSeconds?: number;
lastStartTime?: Date | null;
lastStopTime?: Date | null;
accumulatedRunTimeSeconds?: number;
winRate?: number; winRate?: number;
totalVolumeTraded?: number; totalVolumeTraded?: number;
volumeLast24H?: number; volumeLast24H?: number;

View File

@@ -1,5 +1,5 @@
import React, {useState} from 'react' 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 useApiUrlStore from '../../app/store/apiStore'
import Plot from 'react-plotly.js' import Plot from 'react-plotly.js'
import useTheme from '../../hooks/useTheme' import useTheme from '../../hooks/useTheme'
@@ -7,6 +7,7 @@ import {
type AgentBalanceHistory, type AgentBalanceHistory,
BotStatus, BotStatus,
DataClient, DataClient,
PositionStatus,
type PositionViewModel, type PositionViewModel,
TradeDirection, TradeDirection,
type UserStrategyDetailsViewModel type UserStrategyDetailsViewModel
@@ -93,6 +94,7 @@ function AgentSearch({ index }: { index: number }) {
const totalVolume = agentData.strategies.reduce((sum, strategy) => sum + (strategy.totalVolumeTraded || 0), 0) 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 totalWins = agentData.strategies.reduce((sum, strategy) => sum + (strategy.wins || 0), 0)
const totalLosses = agentData.strategies.reduce((sum, strategy) => sum + (strategy.losses || 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 const avgWinRate = agentData.strategies.length > 0
? agentData.strategies.reduce((sum, strategy) => sum + (strategy.winRate || 0), 0) / agentData.strategies.length ? agentData.strategies.reduce((sum, strategy) => sum + (strategy.winRate || 0), 0) / agentData.strategies.length
: 0 : 0
@@ -104,6 +106,7 @@ function AgentSearch({ index }: { index: number }) {
totalWins, totalWins,
totalLosses, totalLosses,
avgWinRate, avgWinRate,
totalRuntimeSeconds,
activeStrategies: agentData.strategies.filter(s => s.state === BotStatus.Running).length, activeStrategies: agentData.strategies.filter(s => s.state === BotStatus.Running).length,
totalStrategies: agentData.strategies.length totalStrategies: agentData.strategies.length
} }
@@ -157,6 +160,70 @@ function AgentSearch({ index }: { index: number }) {
const currentBalance = getCurrentBalance() const currentBalance = getCurrentBalance()
const chartData = prepareChartData() const chartData = prepareChartData()
const theme = useTheme().themeProperty() 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 }) => (
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
{value >= 0 ? '+' : ''}{value.toFixed(2)}
</span>
)
},
{
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 }) => (
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
{value >= 0 ? '+' : ''}{value.toFixed(2)}%
</span>
)
},
],
[]
)
return ( return (
<div className="container mx-auto pt-6 space-y-6"> <div className="container mx-auto pt-6 space-y-6">
@@ -202,10 +269,10 @@ function AgentSearch({ index }: { index: number }) {
</GridTile> </GridTile>
{/* Open Positions Section */} {/* Open Positions Section */}
{agentData?.positions && agentData.positions.length > 0 && ( {openPositions.length > 0 && (
<GridTile title="Open Positions"> <GridTile title="Open Positions">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{agentData.positions.map((position, index) => ( {openPositions.map((position, index) => (
<div key={index} className="bg-base-200 p-4 rounded-lg border border-base-300"> <div key={index} className="bg-base-200 p-4 rounded-lg border border-base-300">
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
<div> <div>
@@ -426,6 +493,28 @@ function AgentSearch({ index }: { index: number }) {
<div className="stat-desc text-xs">Fees paid across all strategies</div> <div className="stat-desc text-xs">Fees paid across all strategies</div>
</div> </div>
</Card> </Card>
<Card name="Total Runtime">
<div className="stat">
<div className="stat-title text-xs">Total Runtime</div>
<div className="stat-value text-lg">
{(() => {
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`
}
})()}
</div>
<div className="stat-desc text-xs">Across all strategies</div>
</div>
</Card>
</div> </div>
</GridTile> </GridTile>
)} )}
@@ -457,7 +546,7 @@ function AgentSearch({ index }: { index: number }) {
{strategy.roiPercentage && strategy.roiPercentage >= 0 ? '+' : ''}{(strategy.roiPercentage || 0).toFixed(2)}% {strategy.roiPercentage && strategy.roiPercentage >= 0 ? '+' : ''}{(strategy.roiPercentage || 0).toFixed(2)}%
</span> </span>
<span> <span>
{strategy.totalRuntimeSeconds > 0 ? (() => { {strategy.totalRuntimeSeconds && strategy.totalRuntimeSeconds > 0 ? (() => {
const totalSeconds = strategy.totalRuntimeSeconds const totalSeconds = strategy.totalRuntimeSeconds
const days = Math.floor(totalSeconds / 86400) const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600) const hours = Math.floor((totalSeconds % 86400) / 3600)
@@ -486,6 +575,16 @@ function AgentSearch({ index }: { index: number }) {
</div> </div>
</GridTile> </GridTile>
)} )}
{allPositions.length > 0 && (
<GridTile title="All Positions">
<Table
columns={positionTableColumns}
data={allPositions}
showPagination={true}
/>
</GridTile>
)}
</div> </div>
) )
} }