Fix Runtime
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
_state.State.StartupTime = DateTime.UtcNow;
|
if (_state.State.StartupTime == default)
|
||||||
|
{
|
||||||
// Track runtime: set LastStartTime when bot starts
|
_state.State.StartupTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user