update stats data

This commit is contained in:
2025-08-15 07:42:26 +07:00
parent 0a4a4e1398
commit 7528405845
8 changed files with 413 additions and 450 deletions

View File

@@ -40,8 +40,10 @@ public interface IPlatformSummaryGrain : IGrainWithStringKey
Task<int> GetTotalPositionCountAsync(); Task<int> GetTotalPositionCountAsync();
// Event handlers for immediate updates // Event handlers for immediate updates
Task OnStrategyDeployedAsync(StrategyDeployedEvent evt); /// <summary>
Task OnStrategyStoppedAsync(StrategyStoppedEvent evt); /// Updates the active strategy count
/// </summary>
Task UpdateActiveStrategyCountAsync(int newActiveCount);
Task OnPositionOpenedAsync(PositionOpenedEvent evt); Task OnPositionOpenedAsync(PositionOpenedEvent evt);
Task OnPositionClosedAsync(PositionClosedEvent evt); Task OnPositionClosedAsync(PositionClosedEvent evt);
Task OnTradeExecutedAsync(TradeExecutedEvent evt); Task OnTradeExecutedAsync(TradeExecutedEvent evt);
@@ -57,37 +59,7 @@ public abstract class PlatformMetricsEvent
public DateTime Timestamp { get; set; } = DateTime.UtcNow; public DateTime Timestamp { get; set; } = DateTime.UtcNow;
} }
/// <summary>
/// Event fired when a new strategy is deployed
/// </summary>
[GenerateSerializer]
public class StrategyDeployedEvent : PlatformMetricsEvent
{
[Id(1)]
public Guid StrategyId { get; set; }
[Id(2)]
public string AgentName { get; set; } = string.Empty;
[Id(3)]
public string StrategyName { get; set; } = string.Empty;
}
/// <summary>
/// Event fired when a strategy is stopped
/// </summary>
[GenerateSerializer]
public class StrategyStoppedEvent : PlatformMetricsEvent
{
[Id(1)]
public Guid StrategyId { get; set; }
[Id(2)]
public string AgentName { get; set; } = string.Empty;
[Id(3)]
public string StrategyName { get; set; } = string.Empty;
}
/// <summary> /// <summary>
/// Event fired when a new position is opened /// Event fired when a new position is opened

View File

@@ -65,8 +65,8 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
"Bot {Identifier} registered successfully for user {UserId}. Total bots: {TotalBots}, Active bots: {ActiveBots}", "Bot {Identifier} registered successfully for user {UserId}. Total bots: {TotalBots}, Active bots: {ActiveBots}",
identifier, userId, _state.State.TotalBotsCount, _state.State.ActiveBotsCount); identifier, userId, _state.State.TotalBotsCount, _state.State.ActiveBotsCount);
// Notify platform summary grain about strategy deployment // Notify platform summary grain about strategy count change
await NotifyStrategyDeployedAsync(identifier, userId); await NotifyPlatformSummaryAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -102,8 +102,8 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
"Bot {Identifier} unregistered successfully from user {UserId}. Total bots: {TotalBots}", "Bot {Identifier} unregistered successfully from user {UserId}. Total bots: {TotalBots}",
identifier, entryToRemove.UserId, _state.State.TotalBotsCount); identifier, entryToRemove.UserId, _state.State.TotalBotsCount);
// Notify platform summary grain about strategy stopped // Notify platform summary grain about strategy count change
await NotifyStrategyStoppedAsync(identifier, entryToRemove.UserId); await NotifyPlatformSummaryAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -187,65 +187,19 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
return Task.FromResult(entry.Status); return Task.FromResult(entry.Status);
} }
private async Task NotifyStrategyDeployedAsync(Guid identifier, int userId) private async Task NotifyPlatformSummaryAsync()
{ {
try try
{ {
// Get bot details for the event var platformGrain = GrainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
var bot = await _botService.GetBotByIdentifier(identifier); await platformGrain.UpdateActiveStrategyCountAsync(_state.State.ActiveBotsCount);
if (bot != null)
{ _logger.LogDebug("Notified platform summary about active strategy count change. New count: {ActiveCount}",
var platformGrain = GrainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary"); _state.State.ActiveBotsCount);
var deployedEvent = new StrategyDeployedEvent
{
StrategyId = identifier,
AgentName = bot.User.AgentName,
StrategyName = bot.Name
};
await platformGrain.OnStrategyDeployedAsync(deployedEvent);
_logger.LogDebug("Notified platform summary about strategy deployment: {StrategyName}", bot.Name);
}
else
{
_logger.LogWarning("Could not find bot {Identifier} to notify platform summary", identifier);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to notify platform summary about strategy deployment for bot {Identifier}", identifier); _logger.LogError(ex, "Failed to notify platform summary about strategy count change");
}
}
private async Task NotifyStrategyStoppedAsync(Guid identifier, int userId)
{
try
{
// Get bot details for the event
var bot = await _botService.GetBotByIdentifier(identifier);
if (bot != null)
{
var platformGrain = GrainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
var stoppedEvent = new StrategyStoppedEvent
{
StrategyId = identifier,
AgentName = bot.User?.Name ?? $"User-{userId}",
StrategyName = bot.Name
};
await platformGrain.OnStrategyStoppedAsync(stoppedEvent);
_logger.LogDebug("Notified platform summary about strategy stopped: {StrategyName}", bot.Name);
}
else
{
_logger.LogWarning("Could not find bot {Identifier} to notify platform summary", identifier);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to notify platform summary about strategy stopped for bot {Identifier}", identifier);
} }
} }
} }

View File

@@ -190,7 +190,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
var openInterest = openPositions var openInterest = openPositions
.Sum(p => (p.Open.Price * p.Open.Quantity) * p.Open.Leverage); .Sum(p => (p.Open.Price * p.Open.Quantity) * p.Open.Leverage);
_logger.LogDebug("Calculated position metrics: {PositionCount} positions, {OpenInterest} leveraged open interest", _logger.LogDebug(
"Calculated position metrics: {PositionCount} positions, {OpenInterest} leveraged open interest",
positionCount, openInterest); positionCount, openInterest);
return (openInterest, positionCount); return (openInterest, positionCount);
@@ -229,20 +230,11 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
} }
// Event handlers for immediate updates // Event handlers for immediate updates
public async Task OnStrategyDeployedAsync(StrategyDeployedEvent evt) public async Task UpdateActiveStrategyCountAsync(int newActiveCount)
{ {
_logger.LogInformation("Strategy deployed: {StrategyId} - {StrategyName}", evt.StrategyId, evt.StrategyName); _logger.LogInformation("Updating active strategies count to: {NewActiveCount}", newActiveCount);
_state.State.TotalActiveStrategies++; _state.State.TotalActiveStrategies = newActiveCount;
_state.State.HasPendingChanges = true;
await _state.WriteStateAsync();
}
public async Task OnStrategyStoppedAsync(StrategyStoppedEvent evt)
{
_logger.LogInformation("Strategy stopped: {StrategyId} - {StrategyName}", evt.StrategyId, evt.StrategyName);
_state.State.TotalActiveStrategies--;
_state.State.HasPendingChanges = true; _state.State.HasPendingChanges = true;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
} }

View File

@@ -448,6 +448,8 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Roi).HasPrecision(18, 8); entity.Property(e => e.Roi).HasPrecision(18, 8);
entity.Property(e => e.Volume).HasPrecision(18, 8); entity.Property(e => e.Volume).HasPrecision(18, 8);
entity.Property(e => e.Fees).HasPrecision(18, 8); entity.Property(e => e.Fees).HasPrecision(18, 8);
entity.Property(e => e.LongPositionCount).IsRequired();
entity.Property(e => e.ShortPositionCount).IsRequired();
// Create indexes // Create indexes
entity.HasIndex(e => e.Identifier).IsUnique(); entity.HasIndex(e => e.Identifier).IsUnique();

View File

@@ -73,6 +73,8 @@ public class PostgreSqlBotRepository : IBotRepository
existingEntity.Volume = bot.Volume; existingEntity.Volume = bot.Volume;
existingEntity.Fees = bot.Fees; existingEntity.Fees = bot.Fees;
existingEntity.UpdatedAt = DateTime.UtcNow; existingEntity.UpdatedAt = DateTime.UtcNow;
existingEntity.LongPositionCount = bot.LongPositionCount;
existingEntity.ShortPositionCount = bot.ShortPositionCount;
await _context.SaveChangesAsync().ConfigureAwait(false); await _context.SaveChangesAsync().ConfigureAwait(false);
} }
@@ -158,13 +160,13 @@ public class PostgreSqlBotRepository : IBotRepository
} }
public async Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync( public async Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
int pageNumber, int pageNumber,
int pageSize, int pageSize,
BotStatus? status = null, BotStatus? status = null,
string? name = null, string? name = null,
string? ticker = null, string? ticker = null,
string? agentName = null, string? agentName = null,
string sortBy = "CreateDate", string sortBy = "CreateDate",
string sortDirection = "Desc") string sortDirection = "Desc")
{ {
// Build the query with filters // Build the query with filters
@@ -200,29 +202,29 @@ public class PostgreSqlBotRepository : IBotRepository
// Apply sorting // Apply sorting
query = sortBy.ToLower() switch query = sortBy.ToLower() switch
{ {
"name" => sortDirection.ToLower() == "asc" "name" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Name) ? query.OrderBy(b => b.Name)
: query.OrderByDescending(b => b.Name), : query.OrderByDescending(b => b.Name),
"ticker" => sortDirection.ToLower() == "asc" "ticker" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Ticker) ? query.OrderBy(b => b.Ticker)
: query.OrderByDescending(b => b.Ticker), : query.OrderByDescending(b => b.Ticker),
"status" => sortDirection.ToLower() == "asc" "status" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Status) ? query.OrderBy(b => b.Status)
: query.OrderByDescending(b => b.Status), : query.OrderByDescending(b => b.Status),
"startuptime" => sortDirection.ToLower() == "asc" "startuptime" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.StartupTime) ? query.OrderBy(b => b.StartupTime)
: query.OrderByDescending(b => b.StartupTime), : query.OrderByDescending(b => b.StartupTime),
"pnl" => sortDirection.ToLower() == "asc" "pnl" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Pnl) ? query.OrderBy(b => b.Pnl)
: query.OrderByDescending(b => b.Pnl), : query.OrderByDescending(b => b.Pnl),
"winrate" => sortDirection.ToLower() == "asc" "winrate" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.TradeWins / (b.TradeWins + b.TradeLosses)) ? query.OrderBy(b => b.TradeWins / (b.TradeWins + b.TradeLosses))
: query.OrderByDescending(b => b.TradeWins / (b.TradeWins + b.TradeLosses)), : query.OrderByDescending(b => b.TradeWins / (b.TradeWins + b.TradeLosses)),
"agentname" => sortDirection.ToLower() == "asc" "agentname" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.User.AgentName) ? query.OrderBy(b => b.User.AgentName)
: query.OrderByDescending(b => b.User.AgentName), : query.OrderByDescending(b => b.User.AgentName),
_ => sortDirection.ToLower() == "asc" _ => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.CreateDate) ? query.OrderBy(b => b.CreateDate)
: query.OrderByDescending(b => b.CreateDate) // Default to CreateDate : query.OrderByDescending(b => b.CreateDate) // Default to CreateDate
}; };

View File

@@ -72,7 +72,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [showManualPositionModal, setShowManualPositionModal] = useState(false) const [showManualPositionModal, setShowManualPositionModal] = useState(false)
const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null) const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null)
const [showTradesModal, setShowTradesModal] = useState(false) const [showTradesModal, setShowTradesModal] = useState(false)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null) const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ name: string; agentName: string } | null>(null)
const [showBotConfigModal, setShowBotConfigModal] = useState(false) const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{ const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
identifier: string identifier: string
@@ -144,10 +144,10 @@ const BotList: React.FC<IBotList> = ({ list }) => {
) )
} }
function getTradesBadge(botIdentifier: string, agentName: string) { function getTradesBadge(name: string, agentName: string) {
const classes = baseBadgeClass() + ' bg-secondary' const classes = baseBadgeClass() + ' bg-secondary'
return ( return (
<button className={classes} onClick={() => openTradesModal(botIdentifier, agentName)}> <button className={classes} onClick={() => openTradesModal(name, agentName)}>
<p className="text-primary-content flex"> <p className="text-primary-content flex">
<ChartBarIcon width={15}></ChartBarIcon> <ChartBarIcon width={15}></ChartBarIcon>
</p> </p>
@@ -160,8 +160,8 @@ const BotList: React.FC<IBotList> = ({ list }) => {
setShowManualPositionModal(true) setShowManualPositionModal(true)
} }
function openTradesModal(botIdentifier: string, agentName: string) { function openTradesModal(name: string, agentName: string) {
setSelectedBotForTrades({ identifier: botIdentifier, agentName }) setSelectedBotForTrades({ name: name, agentName })
setShowTradesModal(true) setShowTradesModal(true)
} }
@@ -311,7 +311,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
<div className={baseBadgeClass(true)}> <div className={baseBadgeClass(true)}>
PNL {bot.profitAndLoss.toFixed(2).toString()} $ PNL {bot.profitAndLoss.toFixed(2).toString()} $
</div> </div>
{getTradesBadge(bot.identifier, bot.agentName)} {getTradesBadge(bot.name, bot.agentName)}
</div> </div>
</div> </div>
</div> </div>
@@ -335,7 +335,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
/> />
<TradesModal <TradesModal
showModal={showTradesModal} showModal={showTradesModal}
strategyName={selectedBotForTrades?.identifier ?? null} strategyName={selectedBotForTrades?.name ?? null}
agentName={selectedBotForTrades?.agentName ?? null} agentName={selectedBotForTrades?.agentName ?? null}
onClose={() => { onClose={() => {
setShowTradesModal(false) setShowTradesModal(false)

View File

@@ -1,373 +1,385 @@
import React, {useEffect, useState} from 'react' import React from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import { import {fetchPlatformData} from '../../services/platformService'
DataClient,
type PlatformSummaryViewModel,
type TopStrategiesByRoiViewModel,
type TopStrategiesViewModel
} from '../../generated/ManagingApi'
function PlatformSummary({ index }: { index: number }) { function PlatformSummary({index}: { index: number }) {
const { apiUrl } = useApiUrlStore() const {apiUrl} = useApiUrlStore()
const [platformData, setPlatformData] = useState<PlatformSummaryViewModel | null>(null)
const [topStrategies, setTopStrategies] = useState<TopStrategiesViewModel | null>(null) const {
const [topStrategiesByRoi, setTopStrategiesByRoi] = useState<TopStrategiesByRoiViewModel | null>(null) data,
const [isLoading, setIsLoading] = useState(true) isLoading,
const [error, setError] = useState<string | null>(null) error,
isFetching
} = useQuery({
queryKey: ['platformData', apiUrl],
queryFn: () => fetchPlatformData(apiUrl),
refetchInterval: 30000, // Refetch every 30 seconds
staleTime: 25000, // Consider data stale after 25 seconds
placeholderData: (previousData) => previousData, // Keep previous data while fetching
enabled: !!apiUrl
})
const fetchPlatformData = async () => { const platformData = data?.platform
setIsLoading(true) const topStrategies = data?.topStrategies
setError(null) const topStrategiesByRoi = data?.topStrategiesByRoi
try { const formatCurrency = (value: number) => {
const client = new DataClient({}, apiUrl) if (value >= 1000000) {
return `$${(value / 1000000).toFixed(1)}M`
// Fetch all platform data in parallel } else if (value >= 1000) {
const [platform, top, topRoi] = await Promise.all([ return `$${(value / 1000).toFixed(1)}K`
client.data_GetPlatformSummary(), }
client.data_GetTopStrategies(), return `$${value.toFixed(0)}`
client.data_GetTopStrategiesByRoi()
])
setPlatformData(platform)
setTopStrategies(top)
setTopStrategiesByRoi(topRoi)
} catch (err) {
setError('Failed to fetch platform data')
console.error('Error fetching platform data:', err)
} finally {
setIsLoading(false)
} }
}
useEffect(() => { const formatNumber = (value: number) => {
fetchPlatformData() if (value >= 1000) {
return value.toLocaleString()
// Set up refresh interval (every 30 seconds) }
const interval = setInterval(fetchPlatformData, 30000) return value.toString()
return () => clearInterval(interval)
}, [apiUrl])
const formatCurrency = (value: number) => {
if (value >= 1000000) {
return `$${(value / 1000000).toFixed(1)}M`
} else if (value >= 1000) {
return `$${(value / 1000).toFixed(1)}K`
} }
return `$${value.toFixed(0)}`
}
const formatNumber = (value: number) => { const formatChangeIndicator = (change: number) => {
if (value >= 1000) { if (change > 0) {
return value.toLocaleString() return (
} <span className="text-green-500">
return value.toString()
}
const formatChangeIndicator = (change: number) => {
if (change > 0) {
return (
<span className="text-green-500">
+{change} Today +{change} Today
</span> </span>
) )
} else if (change < 0) { } else if (change < 0) {
return ( return (
<span className="text-red-500"> <span className="text-red-500">
{change} Today {change} Today
</span> </span>
) )
} }
return ( return (
<span className="text-gray-500"> <span className="text-gray-500">
No change No change
</span> </span>
) )
} }
const formatPercentageChange = (current: number, change: number) => { const formatPercentageChange = (current: number, change: number) => {
if (current === 0) return '0%' if (current === 0) return '0%'
const percentage = (change / (current - change)) * 100 const percentage = (change / (current - change)) * 100
return `${percentage >= 0 ? '+' : ''}${percentage.toFixed(1)}%` return `${percentage >= 0 ? '+' : ''}${percentage.toFixed(1)}%`
} }
// Show loading spinner only on initial load
if (isLoading && !data) {
return (
<div className="flex justify-center items-center min-h-96">
<div className="loading loading-spinner loading-lg"></div>
</div>
)
}
if (error && !data) {
return (
<div className="alert alert-error">
<span>Failed to fetch platform data</span>
<details className="text-sm mt-2">
{error instanceof Error ? error.message : 'Unknown error occurred'}
</details>
</div>
)
}
if (isLoading) {
return ( return (
<div className="flex justify-center items-center min-h-96"> <div className="p-6 bg-base-100 min-h-screen">
<div className="loading loading-spinner loading-lg"></div> {/* Subtle refetching indicator */}
</div> {isFetching && data && (
) <div className="fixed top-4 right-4 z-50">
} <div className="bg-blue-500 text-white px-3 py-1 rounded-full text-sm flex items-center gap-2">
<div className="loading loading-spinner loading-xs"></div>
Updating...
</div>
</div>
)}
if (error) { {/* Header */}
return ( <div className="mb-8">
<div className="alert alert-error"> <h1 className="text-4xl font-bold text-white mb-2">
<span>{error}</span> {formatNumber(platformData?.totalActiveStrategies || 0)} Strategies Deployed
</div> </h1>
) <div className="text-lg">
} {platformData && formatChangeIndicator(platformData.strategiesChange24h || 0)}
</div>
</div>
return ( {/* Main Stats Grid */}
<div className="p-6 bg-base-100 min-h-screen"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-8">
{/* Header */} {/* Total Volume Traded */}
<div className="mb-8"> <div className="bg-base-200 rounded-lg p-6">
<h1 className="text-4xl font-bold text-white mb-2"> <h3 className="text-lg font-semibold text-gray-400 mb-2">Total Volume Traded</h3>
{formatNumber(platformData?.totalActiveStrategies || 0)} Strategies Deployed <div className="text-3xl font-bold text-white mb-1">
</h1> {formatCurrency(platformData?.totalPlatformVolume || 0)}
<div className="text-lg"> </div>
{platformData && formatChangeIndicator(platformData.strategiesChange24h || 0)} <div
</div> className={`text-sm ${(platformData?.volumeChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
</div> {(platformData?.volumeChange24h || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.volumeChange24h || 0)} Today
<span className="ml-2 text-gray-400">
{/* Main Stats Grid */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-8">
{/* Total Volume Traded */}
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Volume Traded</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatCurrency(platformData?.totalPlatformVolume || 0)}
</div>
<div className={`text-sm ${(platformData?.volumeChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{(platformData?.volumeChange24h || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.volumeChange24h || 0)} Today
<span className="ml-2 text-gray-400">
({formatPercentageChange(platformData?.totalPlatformVolume || 0, platformData?.volumeChange24h || 0)}) ({formatPercentageChange(platformData?.totalPlatformVolume || 0, platformData?.volumeChange24h || 0)})
</span> </span>
</div> </div>
{/* Simple chart placeholder - you can replace with actual chart */} {/* Simple chart placeholder - you can replace with actual chart */}
<div className="mt-4 h-16 flex items-end"> <div className="mt-4 h-16 flex items-end">
<div className="w-full h-8 bg-green-500 rounded opacity-20"></div> <div className="w-full h-8 bg-green-500 rounded opacity-20"></div>
</div> </div>
</div> </div>
{/* Top 3 Most Profitable */} {/* Top 3 Most Profitable */}
<div className="bg-base-200 rounded-lg p-6"> <div className="bg-base-200 rounded-lg p-6">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🤑</span> <span className="text-2xl">🤑</span>
<h3 className="text-lg font-semibold text-gray-400">Top 3 Most Profitable</h3> <h3 className="text-lg font-semibold text-gray-400">Top 3 Most Profitable</h3>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => ( {topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between"> <div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white"> <span className="text-xs font-bold text-white">
{strategy.strategyName?.charAt(0) || 'S'} {strategy.strategyName?.charAt(0) || 'S'}
</span> </span>
</div> </div>
<div> <div>
<div className="text-sm text-white font-medium"> <div className="text-sm text-white font-medium">
{strategy.strategyName || '[Strategy Name]'} {strategy.strategyName || '[Strategy Name]'}
</div>
<div className="text-xs text-gray-400">📧</div>
</div>
</div>
<div
className={`text-sm font-bold ${strategy.pnL && strategy.pnL >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{strategy.pnL && strategy.pnL >= 0 ? '+' : ''}{formatCurrency(strategy.pnL || 0)}
</div>
</div>
)) || (
<div className="text-gray-500 text-sm">No profitable strategies found</div>
)}
</div> </div>
<div className="text-xs text-gray-400">📧</div>
</div>
</div> </div>
<div className={`text-sm font-bold ${strategy.pnL && strategy.pnL >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{strategy.pnL && strategy.pnL >= 0 ? '+' : ''}{formatCurrency(strategy.pnL || 0)}
</div>
</div>
)) || (
<div className="text-gray-500 text-sm">No profitable strategies found</div>
)}
</div>
</div>
{/* Top 3 Rising (by ROI) */} {/* Top 3 Rising (by ROI) */}
<div className="bg-base-200 rounded-lg p-6"> <div className="bg-base-200 rounded-lg p-6">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<span className="text-2xl">📈</span> <span className="text-2xl">📈</span>
<h3 className="text-lg font-semibold text-gray-400">Top 3 by ROI</h3> <h3 className="text-lg font-semibold text-gray-400">Top 3 by ROI</h3>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => ( {topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between"> <div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white"> <span className="text-xs font-bold text-white">
{strategy.strategyName?.charAt(0) || 'S'} {strategy.strategyName?.charAt(0) || 'S'}
</span> </span>
</div> </div>
<div> <div>
<div className="text-sm text-white font-medium"> <div className="text-sm text-white font-medium">
{strategy.strategyName || '[Strategy Name]'} {strategy.strategyName || '[Strategy Name]'}
</div>
<div className="text-xs text-gray-400">
Vol: {formatCurrency(strategy.volume || 0)}
</div>
</div>
</div>
<div className="text-right">
<div
className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}%
</div>
<div
className={`text-xs ${(strategy.pnL || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{(strategy.pnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.pnL || 0)}
</div>
</div>
</div>
)) || (
<div className="text-gray-500 text-sm">No ROI data available</div>
)}
</div> </div>
<div className="text-xs text-gray-400"> </div>
Vol: {formatCurrency(strategy.volume || 0)} </div>
{/* Platform Summary Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Agents</h3>
<div className="text-3xl font-bold text-white mb-1">
{formatNumber(platformData?.totalAgents || 0)}
</div>
<div
className={`text-sm ${(platformData?.agentsChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(platformData?.agentsChange24h || 0)}
</div> </div>
</div>
</div> </div>
<div className="text-right">
<div className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> <div className="bg-base-200 rounded-lg p-6">
{(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}% <h3 className="text-lg font-semibold text-gray-400 mb-2">Active Strategies</h3>
</div> <div className="text-3xl font-bold text-white mb-1">
<div className={`text-xs ${(strategy.pnL || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}> {formatNumber(platformData?.totalActiveStrategies || 0)}
{(strategy.pnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.pnL || 0)} </div>
</div> <div
className={`text-sm ${(platformData?.strategiesChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(platformData?.strategiesChange24h || 0)}
</div>
</div> </div>
</div>
)) || (
<div className="text-gray-500 text-sm">No ROI data available</div>
)}
</div>
</div>
</div>
{/* Platform Summary Stats */} <div className="bg-base-200 rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <h3 className="text-lg font-semibold text-gray-400 mb-2">Total Platform PnL</h3>
<div className="bg-base-200 rounded-lg p-6"> <div
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Agents</h3> className={`text-3xl font-bold ${(platformData?.totalPlatformPnL || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
<div className="text-3xl font-bold text-white mb-1"> {(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)}
{formatNumber(platformData?.totalAgents || 0)} </div>
</div> <div
<div className={`text-sm ${(platformData?.agentsChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> className={`text-sm ${(platformData?.pnLChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(platformData?.agentsChange24h || 0)} {(platformData?.pnLChange24h || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.pnLChange24h || 0)} Today
</div> </div>
</div> </div>
<div className="bg-base-200 rounded-lg p-6"> <div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Active Strategies</h3> <h3 className="text-lg font-semibold text-gray-400 mb-2">Open Interest</h3>
<div className="text-3xl font-bold text-white mb-1"> <div className="text-3xl font-bold text-white mb-1">
{formatNumber(platformData?.totalActiveStrategies || 0)} {formatCurrency(platformData?.totalOpenInterest || 0)}
</div> </div>
<div className={`text-sm ${(platformData?.strategiesChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> <div
{formatChangeIndicator(platformData?.strategiesChange24h || 0)} className={`text-sm ${(platformData?.openInterestChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
</div> {(platformData?.openInterestChange24h || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.openInterestChange24h || 0)} Today
</div> </div>
</div>
</div>
<div className="bg-base-200 rounded-lg p-6"> {/* Position Metrics */}
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Platform PnL</h3> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className={`text-3xl font-bold ${(platformData?.totalPlatformPnL || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> <div className="bg-base-200 rounded-lg p-6">
{(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)} <h3 className="text-lg font-semibold text-gray-400 mb-2">Total Positions</h3>
</div> <div className="text-3xl font-bold text-white mb-1">
<div className={`text-sm ${(platformData?.pnLChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> {formatNumber(platformData?.totalPositionCount || 0)}
{(platformData?.pnLChange24h || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.pnLChange24h || 0)} Today </div>
</div> <div
</div> className={`text-sm ${(platformData?.positionCountChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{formatChangeIndicator(platformData?.positionCountChange24h || 0)}
</div>
</div>
<div className="bg-base-200 rounded-lg p-6"> <div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Open Interest</h3> <h3 className="text-lg font-semibold text-gray-400 mb-2">Long Positions</h3>
<div className="text-3xl font-bold text-white mb-1"> <div className="text-3xl font-bold text-green-400 mb-1">
{formatCurrency(platformData?.totalOpenInterest || 0)} {formatNumber(platformData?.positionCountByDirection?.Long || 0)}
</div> </div>
<div className={`text-sm ${(platformData?.openInterestChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> <div className="text-sm text-gray-400">
{(platformData?.openInterestChange24h || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.openInterestChange24h || 0)} Today {platformData?.totalPositionCount ?
</div> ((platformData.positionCountByDirection?.Long || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}%
</div> of total
</div> </div>
</div>
{/* Position Metrics */} <div className="bg-base-200 rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> <h3 className="text-lg font-semibold text-gray-400 mb-2">Short Positions</h3>
<div className="bg-base-200 rounded-lg p-6"> <div className="text-3xl font-bold text-red-400 mb-1">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Positions</h3> {formatNumber(platformData?.positionCountByDirection?.Short || 0)}
<div className="text-3xl font-bold text-white mb-1"> </div>
{formatNumber(platformData?.totalPositionCount || 0)} <div className="text-sm text-gray-400">
</div> {platformData?.totalPositionCount ?
<div className={`text-sm ${(platformData?.positionCountChange24h || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}> ((platformData.positionCountByDirection?.Short || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}%
{formatChangeIndicator(platformData?.positionCountChange24h || 0)} of total
</div> </div>
</div> </div>
</div>
<div className="bg-base-200 rounded-lg p-6"> {/* Volume and Positions by Asset */}
<h3 className="text-lg font-semibold text-gray-400 mb-2">Long Positions</h3> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div className="text-3xl font-bold text-green-400 mb-1"> {/* Volume by Asset */}
{formatNumber(platformData?.positionCountByDirection?.Long || 0)} <div className="bg-base-200 rounded-lg p-6">
</div> <h3 className="text-lg font-semibold text-gray-400 mb-4">Volume by Asset</h3>
<div className="text-sm text-gray-400"> <div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.totalPositionCount ? {platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? (
((platformData.positionCountByDirection?.Long || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}% of total Object.entries(platformData.volumeByAsset)
</div> .sort(([, a], [, b]) => b - a)
</div> .slice(0, 10)
.map(([asset, volume]) => (
<div className="bg-base-200 rounded-lg p-6"> <div key={asset} className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-400 mb-2">Short Positions</h3> <div className="flex items-center gap-2">
<div className="text-3xl font-bold text-red-400 mb-1"> <div
{formatNumber(platformData?.positionCountByDirection?.Short || 0)} className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
</div>
<div className="text-sm text-gray-400">
{platformData?.totalPositionCount ?
((platformData.positionCountByDirection?.Short || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}% of total
</div>
</div>
</div>
{/* Volume and Positions by Asset */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Volume by Asset */}
<div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-4">Volume by Asset</h3>
<div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? (
Object.entries(platformData.volumeByAsset)
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
.map(([asset, volume]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white"> <span className="text-xs font-bold text-white">
{asset.substring(0, 2)} {asset.substring(0, 2)}
</span> </span>
</div> </div>
<span className="text-white font-medium">{asset}</span> <span className="text-white font-medium">{asset}</span>
</div>
<div className="text-right">
<div className="text-white font-semibold">
{formatCurrency(volume)}
</div>
</div>
</div>
))
) : (
<div className="text-gray-500 text-sm">No volume data available</div>
)}
</div> </div>
<div className="text-right"> </div>
<div className="text-white font-semibold">
{formatCurrency(volume)}
</div>
</div>
</div>
))
) : (
<div className="text-gray-500 text-sm">No volume data available</div>
)}
</div>
</div>
{/* Positions by Asset */} {/* Positions by Asset */}
<div className="bg-base-200 rounded-lg p-6"> <div className="bg-base-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-400 mb-4">Positions by Asset</h3> <h3 className="text-lg font-semibold text-gray-400 mb-4">Positions by Asset</h3>
<div className="space-y-3 max-h-80 overflow-y-auto"> <div className="space-y-3 max-h-80 overflow-y-auto">
{platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? ( {platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? (
Object.entries(platformData.positionCountByAsset) Object.entries(platformData.positionCountByAsset)
.sort(([,a], [,b]) => b - a) .sort(([, a], [, b]) => b - a)
.slice(0, 10) .slice(0, 10)
.map(([asset, count]) => ( .map(([asset, count]) => (
<div key={asset} className="flex items-center justify-between"> <div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center"> <div
className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-white"> <span className="text-xs font-bold text-white">
{asset.substring(0, 2)} {asset.substring(0, 2)}
</span> </span>
</div> </div>
<span className="text-white font-medium">{asset}</span> <span className="text-white font-medium">{asset}</span>
</div>
<div className="text-right">
<div className="text-white font-semibold">
{formatNumber(count)} positions
</div>
<div className="text-xs text-gray-400">
{platformData?.totalPositionCount ?
(count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of
total
</div>
</div>
</div>
))
) : (
<div className="text-gray-500 text-sm">No position data available</div>
)}
</div> </div>
<div className="text-right"> </div>
<div className="text-white font-semibold"> </div>
{formatNumber(count)} positions
</div>
<div className="text-xs text-gray-400">
{platformData?.totalPositionCount ?
(count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of total
</div>
</div>
</div>
))
) : (
<div className="text-gray-500 text-sm">No position data available</div>
)}
</div>
</div>
</div>
{/* Data Freshness Indicator */} {/* Data Freshness Indicator */}
<div className="bg-base-200 rounded-lg p-4"> <div className="bg-base-200 rounded-lg p-4">
<div className="flex items-center justify-between text-sm text-gray-400"> <div className="flex items-center justify-between text-sm text-gray-400">
<span>Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'}</span> <div className="flex items-center gap-2">
<span>24h snapshot: {platformData?.last24HourSnapshot ? new Date(platformData.last24HourSnapshot).toLocaleString() : 'Unknown'}</span> <span>Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'}</span>
{isFetching && (
<div className="flex items-center gap-1 text-blue-400">
<div className="loading loading-spinner loading-xs"></div>
<span>Refreshing...</span>
</div>
)}
</div>
<span>24h snapshot: {platformData?.last24HourSnapshot ? new Date(platformData.last24HourSnapshot).toLocaleString() : 'Unknown'}</span>
</div>
</div>
</div> </div>
</div> )
</div>
)
} }
export default PlatformSummary export default PlatformSummary

View File

@@ -0,0 +1,29 @@
import {
DataClient,
type PlatformSummaryViewModel,
type TopStrategiesByRoiViewModel,
type TopStrategiesViewModel
} from '../generated/ManagingApi'
export interface PlatformData {
platform: PlatformSummaryViewModel
topStrategies: TopStrategiesViewModel
topStrategiesByRoi: TopStrategiesByRoiViewModel
}
export const fetchPlatformData = async (apiUrl: string): Promise<PlatformData> => {
const client = new DataClient({}, apiUrl)
// Fetch all platform data in parallel
const [platform, topStrategies, topStrategiesByRoi] = await Promise.all([
client.data_GetPlatformSummary(),
client.data_GetTopStrategies(),
client.data_GetTopStrategiesByRoi()
])
return {
platform,
topStrategies,
topStrategiesByRoi
}
}