Add agentView
This commit is contained in:
@@ -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 '
|
||||
|
||||
@@ -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', {
|
||||
|
||||
407
src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx
Normal file
407
src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx
Normal 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
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user