Fix Runtime
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -42,6 +42,21 @@ namespace Managing.Api.Models.Responses
|
||||
/// </summary>
|
||||
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>
|
||||
/// Average percentage of successful trades
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
// 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
|
||||
// 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<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
|
||||
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,
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }) => (
|
||||
<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 (
|
||||
<div className="container mx-auto pt-6 space-y-6">
|
||||
@@ -202,10 +269,10 @@ function AgentSearch({ index }: { index: number }) {
|
||||
</GridTile>
|
||||
|
||||
{/* Open Positions Section */}
|
||||
{agentData?.positions && agentData.positions.length > 0 && (
|
||||
{openPositions.length > 0 && (
|
||||
<GridTile title="Open Positions">
|
||||
<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 className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
@@ -426,6 +493,28 @@ function AgentSearch({ index }: { index: number }) {
|
||||
<div className="stat-desc text-xs">Fees paid across all strategies</div>
|
||||
</div>
|
||||
</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>
|
||||
</GridTile>
|
||||
)}
|
||||
@@ -457,7 +546,7 @@ function AgentSearch({ index }: { index: number }) {
|
||||
{strategy.roiPercentage && strategy.roiPercentage >= 0 ? '+' : ''}{(strategy.roiPercentage || 0).toFixed(2)}%
|
||||
</span>
|
||||
<span>
|
||||
{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 }) {
|
||||
</div>
|
||||
</GridTile>
|
||||
)}
|
||||
|
||||
{allPositions.length > 0 && (
|
||||
<GridTile title="All Positions">
|
||||
<Table
|
||||
columns={positionTableColumns}
|
||||
data={allPositions}
|
||||
showPagination={true}
|
||||
/>
|
||||
</GridTile>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user