Fix Runtime by adding TotalRuntimeInSeconds
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1467
src/Managing.Infrastructure.Database/Migrations/20251005132435_AddBotRuntimeTracking.Designer.cs
generated
Normal file
1467
src/Managing.Infrastructure.Database/Migrations/20251005132435_AddBotRuntimeTracking.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1467
src/Managing.Infrastructure.Database/Migrations/20251005133552_ConfigureBotRuntimeFields.Designer.cs
generated
Normal file
1467
src/Managing.Infrastructure.Database/Migrations/20251005133552_ConfigureBotRuntimeFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {PlayIcon, StopIcon} from '@heroicons/react/solid'
|
||||
import moment from 'moment'
|
||||
import {
|
||||
BotClient,
|
||||
BotStatus,
|
||||
DataClient,
|
||||
Position,
|
||||
PositionViewModel,
|
||||
TradeDirection,
|
||||
UserStrategyDetailsViewModel
|
||||
} from '../../generated/ManagingApi'
|
||||
@@ -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'
|
||||
const getStatusBadge = (state: BotStatus | null | undefined) => {
|
||||
const badgeClass = state === BotStatus.Running
|
||||
? 'badge badge-success'
|
||||
: state === 'Down'
|
||||
? 'badge badge-error'
|
||||
: 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>
|
||||
|
||||
Reference in New Issue
Block a user