Add agentView

This commit is contained in:
2025-06-02 14:59:17 +07:00
parent ea4458e21a
commit de9f77d5ba
4 changed files with 454 additions and 4 deletions

View File

@@ -1,11 +1,11 @@
import ArrowDownIcon from '@heroicons/react/solid/ArrowDownIcon'
import ArrowUpIcon from '@heroicons/react/solid/ArrowUpIcon'
import { Position, TradeDirection } from '../../../generated/ManagingApi'
import type { ICardPosition, ICardSignal, ICardText } from '../../../global/type'
import {Position, TradeDirection} from '../../../generated/ManagingApi'
import type {ICardPosition, ICardSignal, ICardText} from '../../../global/type'
function getItemTextHeaderClass() {
return 'text-xs '
return 'text-xs opacity-50 '
}
function getItemTextValueClass() {
return 'text-md '

View File

@@ -81,6 +81,35 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
return averageHours.toFixed(2);
};
// Calculate maximum open time for winning positions
const getMaxOpenTimeWinning = () => {
const winningPositions = positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0;
return realized > 0;
});
if (winningPositions.length === 0) return "0.00";
const openTimes = winningPositions.map(position => calculateOpenTimeInHours(position));
const maxHours = Math.max(...openTimes);
return maxHours.toFixed(2);
};
// Calculate median opening time for all positions
const getMedianOpenTime = () => {
if (positions.length === 0) return "0.00";
const openTimes = positions.map(position => calculateOpenTimeInHours(position));
const sortedTimes = openTimes.sort((a, b) => a - b);
const mid = Math.floor(sortedTimes.length / 2);
const median = sortedTimes.length % 2 === 0
? (sortedTimes[mid - 1] + sortedTimes[mid]) / 2
: sortedTimes[mid];
return median.toFixed(2);
};
// Calculate total volume traded with leverage
const getTotalVolumeTraded = () => {
let totalVolume = 0;
@@ -116,7 +145,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
// Calculate estimated UI fee (0.02% of total volume)
const getEstimatedUIFee = () => {
const totalVolume = getTotalVolumeTraded();
const uiFeePercentage = 0.0002; // 0.02%
const uiFeePercentage = 0.001; // 0.1%
return totalVolume * uiFeePercentage;
};
@@ -185,6 +214,14 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
title="Avg Open Time (Losing)"
content={getAverageOpenTimeLosing() + " hours"}
></CardText>
<CardText
title="Max Open Time (Winning)"
content={getMaxOpenTimeWinning() + " hours"}
></CardText>
<CardText
title="Median Open Time"
content={getMedianOpenTime() + " hours"}
></CardText>
<CardText
title="Volume Traded"
content={"$" + getTotalVolumeTraded().toLocaleString('en-US', {

View File

@@ -0,0 +1,407 @@
import React, {useState} from 'react'
import {Card, FormInput, GridTile} from '../../components/mollecules'
import useApiUrlStore from '../../app/store/apiStore'
import {
type AgentBalanceHistory,
DataClient,
type Position,
TradeDirection,
type UserStrategyDetailsViewModel
} from '../../generated/ManagingApi'
interface AgentData {
strategies: UserStrategyDetailsViewModel[]
balances: AgentBalanceHistory | null
positions: Position[]
}
const FILTERS = [
{ label: '1D', value: '1D', days: 1 },
{ label: '1W', value: '1W', days: 7 },
{ label: '1M', value: '1M', days: 30 },
{ label: 'Total', value: 'Total', days: null },
]
function AgentSearch() {
const { apiUrl } = useApiUrlStore()
const [agentName, setAgentName] = useState('')
const [agentData, setAgentData] = useState<AgentData | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [selectedFilter, setSelectedFilter] = useState('Total')
const searchAgent = async () => {
if (!agentName.trim()) return
setIsLoading(true)
setError(null)
try {
const client = new DataClient({}, apiUrl)
// Fetch data for a longer period to support all filters
const startDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) // Last year
// Fetch agent strategies and balances in parallel
const [strategies, balances] = await Promise.all([
client.data_GetUserStrategies(agentName.trim()),
client.data_GetAgentBalances(
agentName.trim(),
startDate,
new Date()
)
])
// Extract open positions from all strategies
const allPositions = strategies.flatMap(strategy => strategy.positions || [])
const openPositions = allPositions.filter(position =>
position.status !== 'Finished' &&
position.status !== 'Canceled'
)
setAgentData({ strategies, balances, positions: openPositions })
} catch (err) {
setError('Failed to fetch agent data. Please check the agent name and try again.')
console.error('Error fetching agent data:', err)
} finally {
setIsLoading(false)
}
}
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
searchAgent()
}
}
// Filter balances based on selected time period
const getFilteredBalances = () => {
if (!agentData?.balances?.agentBalances) return []
const filterObj = FILTERS.find(f => f.value === selectedFilter)
if (!filterObj?.days) return agentData.balances.agentBalances // Total - return all
const now = new Date()
const cutoff = new Date(now.getTime() - filterObj.days * 24 * 60 * 60 * 1000)
return agentData.balances.agentBalances.filter(balance =>
balance.time && new Date(balance.time) >= cutoff
)
}
// Calculate current total balance
const getCurrentBalance = () => {
const balances = agentData?.balances?.agentBalances
if (!balances || balances.length === 0) return null
// Get the most recent balance
const latestBalance = balances[balances.length - 1]
return {
totalValue: latestBalance.totalValue || 0,
totalAccountValue: latestBalance.totalAccountUsdValue || 0,
botsAllocation: latestBalance.botsAllocationUsdValue || 0,
}
}
// Calculate PnL for the selected time period
const calculateFilteredPnL = () => {
const filteredBalances = getFilteredBalances()
if (filteredBalances.length < 2) return 0
const startBalance = filteredBalances[0]
const endBalance = filteredBalances[filteredBalances.length - 1]
const startValue = startBalance.totalValue || 0
const endValue = endBalance.totalValue || 0
return endValue - startValue
}
// Calculate summary statistics from strategies
const calculateSummary = () => {
if (!agentData?.strategies.length) return null
const totalPnL = agentData.strategies.reduce((sum, strategy) => sum + (strategy.pnL || 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 totalLosses = agentData.strategies.reduce((sum, strategy) => sum + (strategy.losses || 0), 0)
const avgWinRate = agentData.strategies.length > 0
? agentData.strategies.reduce((sum, strategy) => sum + (strategy.winRate || 0), 0) / agentData.strategies.length
: 0
// Calculate filtered PnL based on selected time period
const filteredPnL = calculateFilteredPnL()
return {
totalPnL,
filteredPnL,
totalVolume,
totalWins,
totalLosses,
avgWinRate,
activeStrategies: agentData.strategies.filter(s => s.state === 'RUNNING').length,
totalStrategies: agentData.strategies.length
}
}
// Format position time ago
const formatTimeAgo = (date: Date) => {
const now = new Date()
const diffMs = now.getTime() - new Date(date).getTime()
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffHours / 24)
if (diffDays > 0) {
return `${diffDays}day${diffDays > 1 ? 's' : ''} ago`
} else if (diffHours > 0) {
return `${diffHours}hour${diffHours > 1 ? 's' : ''} ago`
} else {
const diffMinutes = Math.floor(diffMs / (1000 * 60))
return `${diffMinutes}minute${diffMinutes > 1 ? 's' : ''} ago`
}
}
const summary = calculateSummary()
const currentBalance = getCurrentBalance()
return (
<div className="container mx-auto pt-6 space-y-6">
<GridTile title="Agent Search">
<div className="flex gap-4 items-end">
<div className="flex-1">
<FormInput
label="Agent Name"
htmlFor="agentName"
>
<input
id="agentName"
type="text"
className="input input-bordered w-full"
value={agentName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAgentName(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter agent name to search..."
/>
</FormInput>
</div>
<button
className="btn btn-primary"
onClick={searchAgent}
disabled={isLoading || !agentName.trim()}
>
{isLoading ? (
<>
<span className="loading loading-spinner loading-xs"></span>
Searching...
</>
) : (
'Search'
)}
</button>
</div>
{error && (
<div className="alert alert-error mt-4">
<span>{error}</span>
</div>
)}
</GridTile>
{/* Open Positions Section */}
{agentData?.positions && agentData.positions.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) => (
<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>
<h3 className="text-lg font-bold">{position.ticker}/USDC</h3>
<div className="text-sm opacity-70">{formatTimeAgo(position.date)}</div>
</div>
</div>
<div className="mb-3">
<span className={`text-sm font-medium ${
position.originDirection === TradeDirection.Long ? 'text-green-500' : 'text-red-500'
}`}>
{position.originDirection === TradeDirection.Long ? 'Long' : 'Short'} {position.moneyManagement.leverage}x
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`text-lg font-bold ${
(position.profitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500'
}`}>
{(position.profitAndLoss?.realized || 0) >= 0 ? '+' : ''}${(position.profitAndLoss?.realized || 0).toFixed(2)}
</span>
<span className={`flex items-center gap-1 text-sm ${
(position.profitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500'
}`}>
{(position.profitAndLoss?.realized || 0) >= 0 ? '↑' : '↓'}
{Math.abs(((position.profitAndLoss?.realized || 0) / (position.open?.price * position.open?.quantity || 1)) * 100).toFixed(2)}%
</span>
</div>
</div>
</div>
))}
</div>
</GridTile>
)}
{summary && (
<GridTile title={`Agent Stats - ${agentName}`}>
{/* Time Filter Buttons */}
<div className="flex gap-2 mb-4">
{FILTERS.map(f => (
<button
key={f.value}
className={`px-3 py-1 text-sm rounded ${selectedFilter === f.value ? 'bg-primary text-primary-content' : 'bg-base-200 hover:bg-base-300'}`}
onClick={() => setSelectedFilter(f.value)}
>
{f.label}
</button>
))}
</div>
{/* Current Balance Display */}
{currentBalance && (
<div className="mb-6 p-4 bg-primary/10 rounded-lg border border-primary/20">
<h3 className="text-lg font-semibold mb-2">Current Balance</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<div className="text-sm opacity-70">Total Balance</div>
<div className="text-2xl font-bold">${currentBalance.totalValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
</div>
<div>
<div className="text-sm opacity-70">Account USDC Value</div>
<div className="text-xl font-semibold">${currentBalance.totalAccountValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
</div>
<div>
<div className="text-sm opacity-70">Bots Allocation</div>
<div className="text-xl font-semibold">${currentBalance.botsAllocation.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card name="Total Volume Traded">
<div className="stat">
<div className="stat-title text-xs">Total Volume Traded</div>
<div className="stat-value text-lg">${summary.totalVolume.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
<div className="stat-desc text-xs">All time trading volume</div>
</div>
</Card>
<Card name="Win / Loss">
<div className="stat">
<div className="stat-title text-xs">Win / Loss</div>
<div className="stat-value text-lg">{summary.totalWins}/{summary.totalLosses}</div>
<div className="stat-desc text-xs">{summary.avgWinRate.toFixed(0)}% Avg Winrate</div>
</div>
</Card>
<Card name={`PnL (${selectedFilter})`}>
<div className="stat">
<div className="stat-title text-xs">PnL ({selectedFilter})</div>
<div className="stat-value text-lg">
<span className={summary.filteredPnL >= 0 ? 'text-green-500' : 'text-red-500'}>
{summary.filteredPnL >= 0 ? '+' : ''}${summary.filteredPnL.toLocaleString(undefined, { maximumFractionDigits: 2 })}
</span>
</div>
<div className="stat-desc text-xs">
<span className={summary.filteredPnL >= 0 ? 'text-green-500' : 'text-red-500'}>
{currentBalance ? `${((summary.filteredPnL / currentBalance.totalValue) * 100).toFixed(2)}%` : ''}
</span>
</div>
</div>
</Card>
<Card name="Active Strategies">
<div className="stat">
<div className="stat-title text-xs">Strategies</div>
<div className="stat-value text-lg">{summary.activeStrategies}/{summary.totalStrategies}</div>
<div className="stat-desc text-xs">Active / Total</div>
</div>
</Card>
<Card name="ROI">
<div className="stat">
<div className="stat-title text-xs">ROI</div>
<div className="stat-value text-lg">
<span className={summary.totalPnL >= 0 ? 'text-green-500' : 'text-red-500'}>
{((summary.totalPnL / (summary.totalVolume || 1)) * 100).toFixed(2)}%
</span>
</div>
<div className="stat-desc text-xs">Return on Investment</div>
</div>
</Card>
<Card name="Total PnL">
<div className="stat">
<div className="stat-title text-xs">Total PnL</div>
<div className="stat-value text-lg">
<span className={summary.totalPnL >= 0 ? 'text-green-500' : 'text-red-500'}>
{summary.totalPnL >= 0 ? '+' : ''}${summary.totalPnL.toLocaleString(undefined, { maximumFractionDigits: 2 })}
</span>
</div>
<div className="stat-desc text-xs">All time P&L</div>
</div>
</Card>
</div>
</GridTile>
)}
{agentData?.strategies && agentData.strategies.length > 0 && (
<GridTile title="Strategies">
<div className="space-y-4">
<div className="grid grid-cols-6 gap-4 p-3 bg-base-300 rounded-lg text-sm font-medium">
<span>Name</span>
<span>Status</span>
<span>PnL</span>
<span>ROI</span>
<span>Runtime</span>
<span>Avg Winrate</span>
</div>
{agentData.strategies.map((strategy, index) => (
<div key={index} className="grid grid-cols-6 gap-4 p-3 bg-base-200 rounded-lg text-sm">
<span className="font-medium">{strategy.name}</span>
<span className={`badge ${strategy.state === 'RUNNING' ? 'badge-success' : 'badge-warning'}`}>
{strategy.state}
</span>
<span className={strategy.pnL && strategy.pnL >= 0 ? 'text-green-500' : 'text-red-500'}>
{strategy.pnL && strategy.pnL >= 0 ? '+' : ''}${(strategy.pnL || 0).toFixed(2)}
</span>
<span className={strategy.roiPercentage && strategy.roiPercentage >= 0 ? 'text-green-500' : 'text-red-500'}>
{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`
})() : '-'}
</span>
<span>{(strategy.winRate || 0).toFixed(0)}%</span>
</div>
))}
</div>
</GridTile>
)}
{agentData && agentData.strategies.length === 0 && (
<GridTile title="No Data">
<div className="text-center py-8">
<p>No strategies found for agent "{agentName}"</p>
</div>
</GridTile>
)}
</div>
)
}
export default AgentSearch

View File

@@ -6,6 +6,7 @@ import type {ITabsType} from '../../global/type'
import Analytics from './analytics/analytics'
import Monitoring from './monitoring'
import BestAgents from './analytics/bestAgents'
import AgentSearch from './agentSearch'
const tabs: ITabsType = [
{
@@ -23,6 +24,11 @@ const tabs: ITabsType = [
index: 3,
label: 'Best Agents',
},
{
Component: AgentSearch,
index: 4,
label: 'Agent Search',
},
]
const Dashboard: React.FC = () => {