update stats data
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
29
src/Managing.WebApp/src/services/platformService.ts
Normal file
29
src/Managing.WebApp/src/services/platformService.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user