Fix Runtime by adding TotalRuntimeInSeconds

This commit is contained in:
2025-10-05 20:51:46 +07:00
parent 976c1a6580
commit f67ee330b3
18 changed files with 3142 additions and 50 deletions

View File

@@ -498,7 +498,8 @@ public class DataController : ControllerBase
PnL = strategy.Pnl,
NetPnL = strategy.NetPnL,
ROIPercentage = strategy.Roi,
Runtime = strategy.StartupTime,
Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null,
TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(),
WinRate = winRate,
TotalVolumeTraded = totalVolume,
VolumeLast24H = volumeLast24h,

View File

@@ -33,9 +33,14 @@ namespace Managing.Api.Models.Responses
public decimal ROIPercentage { get; set; }
/// <summary>
/// Date and time when the strategy was started
/// Date and time when the strategy was started (only present when running, for live ticker)
/// </summary>
public DateTime Runtime { get; set; }
public DateTime? Runtime { get; set; }
/// <summary>
/// Total accumulated runtime in seconds (including current session if running)
/// </summary>
public long TotalRuntimeSeconds { get; set; }
/// <summary>
/// Average percentage of successful trades

View File

@@ -170,6 +170,10 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
// Set startup time when bot actually starts running
_state.State.StartupTime = DateTime.UtcNow;
// Track runtime: set LastStartTime when bot starts
_state.State.LastStartTime = DateTime.UtcNow;
await _state.WriteStateAsync();
// Start the in-memory timer and persistent reminder
@@ -281,6 +285,18 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
StopAndDisposeTimer();
await UnregisterReminder();
// Track runtime: accumulate current session runtime when stopping
if (_state.State.LastStartTime.HasValue)
{
var currentSessionSeconds = (long)(DateTime.UtcNow - _state.State.LastStartTime.Value).TotalSeconds;
_state.State.AccumulatedRunTimeSeconds += currentSessionSeconds;
_state.State.LastStopTime = DateTime.UtcNow;
_state.State.LastStartTime = null; // Clear since bot is no longer running
_logger.LogInformation("Bot {GrainId} accumulated {Seconds} seconds of runtime. Total: {TotalSeconds} seconds",
this.GetPrimaryKey(), currentSessionSeconds, _state.State.AccumulatedRunTimeSeconds);
}
// Sync state from the volatile TradingBotBase before destroying it
SyncStateFromBase();
await _state.WriteStateAsync();

View File

@@ -121,4 +121,22 @@ public class TradingBotGrainState
/// </summary>
[Id(18)]
public Candle? LastCandle { get; set; }
/// <summary>
/// The last time the bot was started (for runtime tracking)
/// </summary>
[Id(19)]
public DateTime? LastStartTime { get; set; }
/// <summary>
/// The last time the bot was stopped (for runtime tracking)
/// </summary>
[Id(20)]
public DateTime? LastStopTime { get; set; }
/// <summary>
/// Total accumulated runtime in seconds (excluding current session if running)
/// </summary>
[Id(21)]
public long AccumulatedRunTimeSeconds { get; set; }
}

View File

@@ -13,6 +13,11 @@ namespace Managing.Domain.Bots
public DateTime StartupTime { get; set; }
public DateTime CreateDate { get; set; }
// Runtime tracking fields
public DateTime? LastStartTime { get; set; }
public DateTime? LastStopTime { get; set; }
public long AccumulatedRunTimeSeconds { get; set; }
public int TradeWins { get; set; }
public int TradeLosses { get; set; }
public decimal Pnl { get; set; }
@@ -23,5 +28,21 @@ namespace Managing.Domain.Bots
public int LongPositionCount { get; set; }
public int ShortPositionCount { get; set; }
/// <summary>
/// Gets the total runtime in seconds, including the current session if the bot is running
/// </summary>
public long GetTotalRuntimeSeconds()
{
var totalSeconds = AccumulatedRunTimeSeconds;
// If bot is currently running, add current session time
if (Status == BotStatus.Running && LastStartTime.HasValue)
{
var currentSessionSeconds = (long)(DateTime.UtcNow - LastStartTime.Value).TotalSeconds;
totalSeconds += currentSessionSeconds;
}
return totalSeconds;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class AddBotRuntimeTracking : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "AccumulatedRunTimeSeconds",
table: "Bots",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<DateTime>(
name: "LastStartTime",
table: "Bots",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "LastStopTime",
table: "Bots",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AccumulatedRunTimeSeconds",
table: "Bots");
migrationBuilder.DropColumn(
name: "LastStartTime",
table: "Bots");
migrationBuilder.DropColumn(
name: "LastStopTime",
table: "Bots");
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class ConfigureBotRuntimeFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -244,6 +244,9 @@ namespace Managing.Infrastructure.Databases.Migrations
.HasMaxLength(255)
.HasColumnType("uuid");
b.Property<long>("AccumulatedRunTimeSeconds")
.HasColumnType("bigint");
b.Property<DateTime>("CreateDate")
.HasColumnType("timestamp with time zone");
@@ -251,6 +254,12 @@ namespace Managing.Infrastructure.Databases.Migrations
.HasPrecision(18, 8)
.HasColumnType("numeric(18,8)");
b.Property<DateTime?>("LastStartTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("LastStopTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("LongPositionCount")
.HasColumnType("integer");

View File

@@ -21,6 +21,12 @@ public class BotEntity
public DateTime CreateDate { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime StartupTime { get; set; }
// Runtime tracking fields
public DateTime? LastStartTime { get; set; }
public DateTime? LastStopTime { get; set; }
public long AccumulatedRunTimeSeconds { get; set; }
public int TradeWins { get; set; }
public int TradeLosses { get; set; }
public decimal Pnl { get; set; }

View File

@@ -445,6 +445,10 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.CreateDate).IsRequired();
entity.Property(e => e.StartupTime).IsRequired();
// Runtime tracking fields
entity.Property(e => e.LastStartTime);
entity.Property(e => e.LastStopTime);
entity.Property(e => e.AccumulatedRunTimeSeconds);
entity.Property(e => e.TradeWins).IsRequired();
entity.Property(e => e.TradeLosses).IsRequired();
entity.Property(e => e.Pnl).HasPrecision(18, 8);

View File

@@ -692,6 +692,9 @@ public static class PostgreSqlMappers
Name = entity.Name,
Ticker = entity.Ticker,
StartupTime = entity.StartupTime,
LastStartTime = entity.LastStartTime,
LastStopTime = entity.LastStopTime,
AccumulatedRunTimeSeconds = entity.AccumulatedRunTimeSeconds,
TradeWins = entity.TradeWins,
TradeLosses = entity.TradeLosses,
Pnl = entity.Pnl,
@@ -719,6 +722,9 @@ public static class PostgreSqlMappers
Name = bot.Name,
Ticker = bot.Ticker,
StartupTime = bot.StartupTime,
LastStartTime = bot.LastStartTime,
LastStopTime = bot.LastStopTime,
AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds,
TradeWins = bot.TradeWins,
TradeLosses = bot.TradeLosses,
Pnl = bot.Pnl,

View File

@@ -105,6 +105,7 @@ const LogIn = () => {
<label
htmlFor="name"
className="dark:text-white block mb-2 text-sm font-medium text-gray-900"
hidden={true}
>
Name
</label>

View File

@@ -4491,7 +4491,8 @@ export interface UserStrategyDetailsViewModel {
pnL?: number;
netPnL?: number;
roiPercentage?: number;
runtime?: Date;
runtime?: Date | null;
totalRuntimeSeconds?: number;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;

View File

@@ -974,7 +974,8 @@ export interface UserStrategyDetailsViewModel {
pnL?: number;
netPnL?: number;
roiPercentage?: number;
runtime?: Date;
runtime?: Date | null;
totalRuntimeSeconds?: number;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;

View File

@@ -48,14 +48,10 @@ function AgentSearch({ index }: { index: number }) {
)
])
// Extract open positions from all strategies
// Extract positions from all strategies (status not available in generated PositionViewModel)
const allPositions = strategies.flatMap(strategy => strategy.positions || [])
const openPositions = allPositions.filter(position =>
position.status !== 'Finished' &&
position.status !== 'Canceled'
)
setAgentData({ strategies, balances, positions: openPositions })
setAgentData({ strategies, balances, positions: allPositions })
} catch (err) {
setError('Failed to fetch agent data. Please check the agent name and try again.')
console.error('Error fetching agent data:', err)
@@ -461,13 +457,20 @@ function AgentSearch({ index }: { index: number }) {
{strategy.roiPercentage && strategy.roiPercentage >= 0 ? '+' : ''}{(strategy.roiPercentage || 0).toFixed(2)}%
</span>
<span>
{strategy.runtime ? (() => {
const runtime = new Date(strategy.runtime)
const now = new Date()
const diffHours = Math.floor((now.getTime() - runtime.getTime()) / (1000 * 60 * 60))
const diffDays = Math.floor(diffHours / 24)
return diffDays > 0 ? `${diffDays}d ${diffHours % 24}h` : `${diffHours}h`
{strategy.totalRuntimeSeconds > 0 ? (() => {
const totalSeconds = strategy.totalRuntimeSeconds
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`
}
})() : '-'}
{strategy.state === BotStatus.Running && <span className="ml-1 text-green-500"></span>}
</span>
<span>{(strategy.winRate || 0).toFixed(0)}%</span>
</div>

View File

@@ -1,13 +1,12 @@
import React, {useEffect, useState} from 'react'
import {PlayIcon, StopIcon} from '@heroicons/react/solid'
import moment from 'moment'
import {
BotClient,
BotStatus,
DataClient,
Position,
TradeDirection,
UserStrategyDetailsViewModel
BotClient,
BotStatus,
DataClient,
PositionViewModel,
TradeDirection,
UserStrategyDetailsViewModel
} from '../../generated/ManagingApi'
import useApiUrlStore from '../../app/store/apiStore'
import {Toast} from '../../components/mollecules'
@@ -62,16 +61,12 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`
}
const formatDuration = (runtime?: Date | null) => {
if (!runtime) return '0h'
const formatDuration = (totalRuntimeSeconds?: number | null) => {
if (!totalRuntimeSeconds || totalRuntimeSeconds === 0) return '0h'
const start = moment(runtime)
const now = moment()
const duration = moment.duration(now.diff(start))
const days = Math.floor(duration.asDays())
const hours = duration.hours()
const minutes = duration.minutes()
const days = Math.floor(totalRuntimeSeconds / 86400)
const hours = Math.floor((totalRuntimeSeconds % 86400) / 3600)
const minutes = Math.floor((totalRuntimeSeconds % 3600) / 60)
if (days > 0) {
return `${days}d ${hours}h ${minutes}m`
@@ -82,22 +77,22 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
}
}
const getStatusBadge = (state: string | null | undefined) => {
const badgeClass = state === 'Up'
? 'badge badge-success'
: state === 'Down'
? 'badge badge-error'
const getStatusBadge = (state: BotStatus | null | undefined) => {
const badgeClass = state === BotStatus.Running
? 'badge badge-success'
: state === BotStatus.Stopped
? 'badge badge-warning'
: 'badge badge-neutral'
return <span className={badgeClass}>{state}</span>
}
const toggleStrategyStatus = (status: string | null | undefined, identifier: string) => {
const isUp = status === 'Up'
const t = new Toast(isUp ? 'Stopping strategy' : 'Starting strategy')
const toggleStrategyStatus = (status: BotStatus | null | undefined, identifier: string) => {
const isRunning = status === BotStatus.Running
const t = new Toast(isRunning ? 'Stopping strategy' : 'Starting strategy')
console.log('toggleStrategyStatus', status, identifier)
if (status === 'Up') {
if (status === BotStatus.Running) {
botClient
.bot_Stop(identifier)
.then(() => {
@@ -108,7 +103,7 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
.catch((err) => {
t.update('error', err)
})
} else if (status === 'Down' || status === 'None') {
} else {
botClient
.bot_Restart(identifier)
.then(() => {
@@ -212,7 +207,7 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
</div>
<div className="stat bg-base-200 rounded-lg">
<div className="stat-title">Runtime</div>
<div className="stat-value text-sm">{formatDuration(strategyData.runtime)}</div>
<div className="stat-value text-sm">{formatDuration(strategyData.totalRuntimeSeconds)}</div>
</div>
</div>
@@ -292,7 +287,7 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title">Total Runtime</h3>
<div className="text-2xl font-bold">{formatDuration(strategyData.runtime)}</div>
<div className="text-2xl font-bold">{formatDuration(strategyData.totalRuntimeSeconds)}</div>
</div>
</div>
</div>
@@ -302,7 +297,7 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
<div className="card bg-base-200">
<div className="card-body">
<h3 className="card-title">Trade History</h3>
{strategyData.positions && Object.keys(strategyData.positions).length > 0 ? (
{strategyData.positions && strategyData.positions.length > 0 ? (
<div className="overflow-x-auto">
<table className="table table-zebra">
<thead>
@@ -317,8 +312,7 @@ const AgentStrategy: React.FC<AgentStrategyProps> = ({ index }) => {
</tr>
</thead>
<tbody>
{Object.values(strategyData.positions).map((position: Position, index: number) => (
console.log(position),
{(strategyData.positions || []).map((position: PositionViewModel, index: number) => (
<tr key={index}>
<td>{new Date(position.Open.date || '').toLocaleDateString()}</td>
<td>{position.ticker}</td>