Add Agent tracking balance

This commit is contained in:
2025-05-16 22:30:18 +07:00
parent b34e3aa886
commit 1cfb83f0b1
34 changed files with 764 additions and 115 deletions

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using Managing.Common;

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
@@ -17,7 +18,7 @@ public class LeaderboardWorker : BaseWorker<FeeWorker>
logger,
TimeSpan.FromHours(24),
workerService
)
)
{
_statisticService = statisticService;
}
@@ -26,4 +27,4 @@ public class LeaderboardWorker : BaseWorker<FeeWorker>
{
await _statisticService.UpdateLeaderboard();
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
@@ -17,7 +18,7 @@ public class NoobiesboardWorker : BaseWorker<FeeWorker>
logger,
TimeSpan.FromHours(24),
workerService
)
)
{
_statisticService = statisticService;
}
@@ -26,4 +27,4 @@ public class NoobiesboardWorker : BaseWorker<FeeWorker>
{
await _statisticService.UpdateNoobiesboard();
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Workers;
@@ -17,7 +18,7 @@ public class PricesFifteenMinutesWorker : PricesBaseWorker<PricesFifteenMinutesW
TimeSpan.FromMinutes(1),
WorkerType.PriceFifteenMinutes,
Timeframe.FifteenMinutes
)
)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Workers;

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Workers;
@@ -17,7 +18,7 @@ public class PricesFourHoursWorker : PricesBaseWorker<PricesFourHoursWorker>
TimeSpan.FromHours(2),
WorkerType.PriceFourHour,
Timeframe.FourHour
)
)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Workers;
@@ -17,7 +18,7 @@ public class PricesOneDayWorker : PricesBaseWorker<PricesOneDayWorker>
TimeSpan.FromHours(12),
WorkerType.PriceOneDay,
Timeframe.OneDay
)
)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
namespace Managing.Api.Workers.Workers;
@@ -19,4 +20,4 @@ public class PricesOneHourWorker : PricesBaseWorker<PricesOneHourWorker>
Timeframe.OneHour)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using Managing.Common;

View File

@@ -1,4 +1,5 @@
using Managing.Application.Workers;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using static Managing.Common.Enums;
@@ -17,7 +18,7 @@ public class TopVolumeTickerWorker : BaseWorker<TopVolumeTickerWorker>
logger,
TimeSpan.FromHours(12),
workerService
)
)
{
_statisticService = statisticService;
}
@@ -26,4 +27,4 @@ public class TopVolumeTickerWorker : BaseWorker<TopVolumeTickerWorker>
{
await _statisticService.UpdateTopVolumeTicker(TradingExchanges.Evm, 10);
}
}
}

View File

@@ -3,7 +3,6 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
using Managing.Application.Workers.Abstractions;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Shared.Helpers;
@@ -88,13 +87,18 @@ public class DataController : ControllerBase
{ "AAVE", "https://assets.coingecko.com/coins/images/12645/standard/AAVE.png?1696512452" },
{ "ADA", "https://assets.coingecko.com/coins/images/975/standard/cardano.png?1696502090" },
{ "APE", "https://assets.coingecko.com/coins/images/24383/standard/apecoin.jpg?1696523566" },
{ "ARB", "https://assets.coingecko.com/coins/images/16547/small/photo_2023-03-29_21.47.00.jpeg?1680097630" },
{
"ARB", "https://assets.coingecko.com/coins/images/16547/small/photo_2023-03-29_21.47.00.jpeg?1680097630"
},
{ "ATOM", "https://assets.coingecko.com/coins/images/1481/standard/cosmos_hub.png?1696502525" },
{ "AVAX", "https://assets.coingecko.com/coins/images/12559/small/coin-round-red.png?1604021818" },
{ "BNB", "https://assets.coingecko.com/coins/images/825/standard/bnb-icon2_2x.png?1696501970" },
{ "BTC", "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579" },
{ "DOGE", "https://assets.coingecko.com/coins/images/5/small/dogecoin.png?1547792256" },
{ "DOT", "https://static.coingecko.com/s/polkadot-73b0c058cae10a2f076a82dcade5cbe38601fad05d5e6211188f09eb96fa4617.gif" },
{
"DOT",
"https://static.coingecko.com/s/polkadot-73b0c058cae10a2f076a82dcade5cbe38601fad05d5e6211188f09eb96fa4617.gif"
},
{ "ETH", "https://assets.coingecko.com/coins/images/279/small/ethereum.png?1595348880" },
{ "FIL", "https://assets.coingecko.com/coins/images/12817/standard/filecoin.png?1696512609" },
{ "GMX", "https://assets.coingecko.com/coins/images/18323/small/arbit.png?1631532468" },
@@ -121,15 +125,24 @@ public class DataController : ControllerBase
{ "TAO", "https://assets.coingecko.com/coins/images/28452/standard/ARUsPeNQ_400x400.jpeg?1696527447" },
{ "BONK", "https://assets.coingecko.com/coins/images/28600/standard/bonk.jpg?1696527587" },
{ "WLD", "https://assets.coingecko.com/coins/images/31069/standard/worldcoin.jpeg?1696529903" },
{ "tBTC", "https://assets.coingecko.com/coins/images/11224/standard/0x18084fba666a33d37592fa2633fd49a74dd93a88.png?1696511155" },
{
"tBTC",
"https://assets.coingecko.com/coins/images/11224/standard/0x18084fba666a33d37592fa2633fd49a74dd93a88.png?1696511155"
},
{ "EIGEN", "https://assets.coingecko.com/coins/images/37441/standard/eigen.jpg?1728023974" },
{ "SUI", "https://assets.coingecko.com/coins/images/26375/standard/sui-ocean-square.png?1727791290" },
{ "SEI", "https://assets.coingecko.com/coins/images/28205/standard/Sei_Logo_-_Transparent.png?1696527207" },
{ "DAI", "https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734" },
{ "TIA", "https://assets.coingecko.com/coins/images/31967/standard/tia.jpg?1696530772" },
{ "TRX", "https://assets.coingecko.com/coins/images/1094/standard/tron-logo.png?1696502193" },
{ "TON", "https://assets.coingecko.com/coins/images/17980/standard/photo_2024-09-10_17.09.00.jpeg?1725963446" },
{ "PENDLE", "https://assets.coingecko.com/coins/images/15069/standard/Pendle_Logo_Normal-03.png?1696514728" },
{
"TON",
"https://assets.coingecko.com/coins/images/17980/standard/photo_2024-09-10_17.09.00.jpeg?1725963446"
},
{
"PENDLE",
"https://assets.coingecko.com/coins/images/15069/standard/Pendle_Logo_Normal-03.png?1696514728"
},
{ "wstETH", "https://assets.coingecko.com/coins/images/18834/standard/wstETH.png?1696518295" },
{ "USDe", "https://assets.coingecko.com/coins/images/33613/standard/USDE.png?1716355685" },
{ "SATS", "https://assets.coingecko.com/coins/images/30666/standard/_dD8qr3M_400x400.png?1702913020" },
@@ -147,11 +160,17 @@ public class DataController : ControllerBase
{ "ANIME", "https://assets.coingecko.com/coins/images/53575/standard/anime.jpg?1736748703" },
{ "BERA", "https://assets.coingecko.com/coins/images/25235/standard/BERA.png?1738822008" },
{ "VIRTUAL", "https://assets.coingecko.com/coins/images/34057/standard/LOGOMARK.png?1708356054" },
{ "PENGU", "https://assets.coingecko.com/coins/images/52622/standard/PUDGY_PENGUINS_PENGU_PFP.png?1733809110" },
{
"PENGU",
"https://assets.coingecko.com/coins/images/52622/standard/PUDGY_PENGUINS_PENGU_PFP.png?1733809110"
},
{ "FET", "https://assets.coingecko.com/coins/images/5681/standard/ASI.png?1719827289" },
{ "ONDO", "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656" },
{ "AIXBT", "https://assets.coingecko.com/coins/images/51784/standard/3.png?1731981138" },
{ "CAKE", "https://assets.coingecko.com/coins/images/12632/standard/pancakeswap-cake-logo_%281%29.png?1696512440" },
{
"CAKE",
"https://assets.coingecko.com/coins/images/12632/standard/pancakeswap-cake-logo_%281%29.png?1696512440"
},
{ "S", "https://assets.coingecko.com/coins/images/38108/standard/200x200_Sonic_Logo.png?1734679256" },
{ "JUP", "https://assets.coingecko.com/coins/images/34188/standard/jup.png?1704266489" },
{ "HYPE", "https://assets.coingecko.com/coins/images/50882/standard/hyperliquid.jpg?1729431300" },
@@ -163,7 +182,8 @@ public class DataController : ControllerBase
var tickerInfo = new TickerInfos
{
Ticker = ticker,
ImageUrl = tokens.GetValueOrDefault(ticker.ToString(), "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579") // Default to BTC image if not found
ImageUrl = tokens.GetValueOrDefault(ticker.ToString(),
"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579") // Default to BTC image if not found
};
tickerInfos.Add(tickerInfo);
}
@@ -539,4 +559,64 @@ public class DataController : ControllerBase
return Ok(summary);
}
/// <summary>
/// Retrieves balance history for a specific agent within a date range
/// </summary>
/// <param name="agentName">The name of the agent to retrieve balances for</param>
/// <param name="startDate">The start date for the balance history</param>
/// <param name="endDate">Optional end date for the balance history (defaults to current time)</param>
/// <returns>A list of agent balances within the specified date range</returns>
[HttpGet("GetAgentBalances")]
public async Task<ActionResult<IList<AgentBalance>>> GetAgentBalances(
string agentName,
DateTime startDate,
DateTime? endDate = null)
{
string cacheKey = $"AgentBalances_{agentName}_{startDate:yyyyMMdd}_{endDate?.ToString("yyyyMMdd") ?? "now"}";
// Check if the balances are already cached
var cachedBalances = _cacheService.GetValue<IList<AgentBalance>>(cacheKey);
if (cachedBalances != null)
{
return Ok(cachedBalances);
}
var balances = await _statisticService.GetAgentBalances(agentName, startDate, endDate);
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, balances, TimeSpan.FromMinutes(5));
return Ok(balances);
}
/// <summary>
/// Retrieves a paginated list of the best performing agents based on their total value
/// </summary>
/// <param name="startDate">The start date for calculating agent performance</param>
/// <param name="endDate">Optional end date for calculating agent performance (defaults to current time)</param>
/// <param name="page">Page number (defaults to 1)</param>
/// <param name="pageSize">Number of items per page (defaults to 10)</param>
/// <returns>A paginated list of agent balances and total count</returns>
[HttpGet("GetBestAgents")]
public async Task<ActionResult<BestAgentsResponse>> GetBestAgents(
DateTime startDate,
DateTime? endDate = null,
int page = 1,
int pageSize = 10)
{
var (agents, totalCount) = await _statisticService.GetBestAgents(startDate, endDate, page, pageSize);
var response = new BestAgentsResponse
{
Agents = agents,
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
return Ok(response);
}
}

View File

@@ -52,4 +52,8 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="Workers\"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
using Managing.Domain.Statistics;
namespace Managing.Api.Models.Responses;
public class BestAgentsResponse
{
public IList<AgentBalanceHistory> Agents { get; set; }
public int TotalCount { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
}

View File

@@ -4,7 +4,6 @@ using HealthChecks.UI.Client;
using Managing.Api.Authorization;
using Managing.Api.Filters;
using Managing.Api.HealthChecks;
using Managing.Api.Workers;
using Managing.Application.Hubs;
using Managing.Application.Workers;
using Managing.Bootstrap;
@@ -204,7 +203,7 @@ if (builder.Configuration.GetValue<bool>("EnableBotManager", false))
builder.Services.AddHostedService<BotManagerWorker>();
}
builder.Services.AddHostedService<BalanceTrackingWorker>();
// Workers are now registered in ApiBootstrap.cs
// App
var app = builder.Build();

View File

@@ -38,5 +38,8 @@
"ButtonExpirationMinutes": 2
},
"AllowedHosts": "*",
"EnableBotManager": true
"Workers": {
"BotManager": true,
"BalancesTracking": true
}
}

View File

@@ -0,0 +1,12 @@
using Managing.Domain.Statistics;
namespace Managing.Application.Abstractions.Repositories;
public interface IAgentBalanceRepository
{
void InsertAgentBalance(AgentBalance balance);
Task<IList<AgentBalance>> GetAgentBalances(string agentName, DateTime start, DateTime? end = null);
Task<(IList<AgentBalanceHistory> result, int totalCount)> GetAllAgentBalancesWithHistory(DateTime start,
DateTime? end);
}

View File

@@ -1,11 +1,16 @@
using Managing.Common;
using Managing.Common;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
namespace Managing.Application.Workers.Abstractions;
namespace Managing.Application.Abstractions.Services;
public interface IStatisticService
{
Task<IList<AgentBalanceHistory>> GetAgentBalances(string agentName, DateTime start, DateTime? end = null);
Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(DateTime start, DateTime? end = null, int page = 1,
int pageSize = 10);
List<Trader> GetBadTraders();
List<Trader> GetBestTraders();
SpotlightOverview GetLastSpotlight(DateTime dateTime);

View File

@@ -50,19 +50,10 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
{
worker = await _workerService.GetWorker(_workerType);
if (worker.IsActive || worker.WorkerType.Equals(WorkerType.BotManager))
{
await Run(cancellationToken);
_executionCount++;
await _workerService.UpdateWorker(_workerType, _executionCount);
_logger.LogInformation($"[{_workerType}] Run ok. Next run at : {DateTime.UtcNow.Add(_delay)}");
}
else
{
_logger.LogInformation(
$"[{_workerType}] Worker not active. Next run at : {DateTime.UtcNow.Add(_delay)}");
}
await Run(cancellationToken);
_executionCount++;
await _workerService.UpdateWorker(_workerType, _executionCount);
_logger.LogInformation($"[{_workerType}] Run ok. Next run at : {DateTime.UtcNow.Add(_delay)}");
await Task.Delay(_delay);
}

View File

@@ -1,6 +1,5 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions;
using Managing.Domain.Accounts;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
@@ -24,6 +23,7 @@ public class StatisticService : IStatisticService
private readonly ITradaoService _tradaoService;
private readonly IMessengerService _messengerService;
private readonly ICacheService _cacheService;
private readonly IAgentBalanceRepository _agentBalanceRepository;
private readonly ILogger<StatisticService> _logger;
public StatisticService(
@@ -36,7 +36,8 @@ public class StatisticService : IStatisticService
IBacktester backtester,
ITradaoService tradaoService,
IMessengerService messengerService,
ICacheService cacheService)
ICacheService cacheService,
IAgentBalanceRepository agentBalanceRepository)
{
_exchangeService = exchangeService;
_accountService = accountService;
@@ -48,6 +49,7 @@ public class StatisticService : IStatisticService
_tradaoService = tradaoService;
_messengerService = messengerService;
_cacheService = cacheService;
_agentBalanceRepository = agentBalanceRepository;
}
public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top)
@@ -375,4 +377,74 @@ public class StatisticService : IStatisticService
await _messengerService.SendBadTraders(lastBadTrader);
}
public async Task<IList<AgentBalanceHistory>> GetAgentBalances(string agentName, DateTime start,
DateTime? end = null)
{
var effectiveEnd = end ?? DateTime.UtcNow;
string cacheKey = $"AgentBalances_{agentName}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";
// Check if the balances are already cached
var cachedBalances = _cacheService.GetValue<IList<AgentBalanceHistory>>(cacheKey);
if (cachedBalances != null)
{
return cachedBalances;
}
var balances = await _agentBalanceRepository.GetAgentBalances(agentName, start, end);
// Create a single AgentBalanceHistory with all balances
var result = new List<AgentBalanceHistory>
{
new AgentBalanceHistory
{
AgentName = agentName,
AgentBalances = balances.OrderBy(b => b.Time).ToList()
}
};
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
return result;
}
public async Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(
DateTime start,
DateTime? end = null,
int page = 1,
int pageSize = 10)
{
var effectiveEnd = end ?? DateTime.UtcNow;
string cacheKey = $"BestAgents_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";
// Check if the results are already cached
var cachedResult = _cacheService.GetValue<(IList<AgentBalanceHistory>, int)>(cacheKey);
if (cachedResult != default)
{
// Apply pagination to cached results
var (cachedAgents, cachedTotalCount) = cachedResult;
var paginatedAgents = cachedAgents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return (paginatedAgents, cachedTotalCount);
}
// Get all agents with their balance history
var (fetchedAgents, fetchedTotalCount) = await _agentBalanceRepository.GetAllAgentBalancesWithHistory(start, end);
// Cache all results for 5 minutes
_cacheService.SaveValue(cacheKey, (fetchedAgents, fetchedTotalCount), TimeSpan.FromMinutes(5));
// Apply pagination
var result = fetchedAgents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return (result, fetchedTotalCount);
}
}

View File

@@ -1,52 +1,67 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Application.Workers.Abstractions;
using Managing.Domain.Statistics;
using MediatR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Workers;
public class BalanceTrackingWorker : BackgroundService
public class BalanceTrackingWorker : BaseWorker<BalanceTrackingWorker>
{
private readonly ILogger<BalanceTrackingWorker> _logger;
private readonly IMediator _mediator;
private readonly IAccountService _accountService;
private readonly TimeSpan _interval = TimeSpan.FromMinutes(1);
private readonly IAgentBalanceRepository _agentBalanceRepository;
private bool _isInitialized;
public BalanceTrackingWorker(
ILogger<BalanceTrackingWorker> logger,
IMediator mediator,
IAccountService accountService)
IAccountService accountService,
IAgentBalanceRepository agentBalanceRepository,
IWorkerService workerService)
: base(
WorkerType.BalanceTracking,
logger,
TimeSpan.FromHours(1),
workerService)
{
_logger = logger;
_mediator = mediator;
_accountService = accountService;
_agentBalanceRepository = agentBalanceRepository;
_isInitialized = false;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
protected override async Task Run(CancellationToken cancellationToken)
{
while (!stoppingToken.IsCancellationRequested)
if (!_isInitialized)
{
try
{
_logger.LogInformation("Starting balance tracking...");
await TrackBalances();
_logger.LogInformation("Completed balance tracking");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while tracking balances");
}
await Task.Delay(_interval, stoppingToken);
_logger.LogInformation("Waiting 5 minutes for bots to initialize before starting balance tracking...");
await Task.Delay(TimeSpan.FromMinutes(3), cancellationToken);
_isInitialized = true;
}
}
private async Task TrackBalances()
{
_logger.LogInformation("Starting balance tracking...");
// Get all active bots
var bots = await _mediator.Send(new GetActiveBotsCommand());
if (bots.Count == 0)
{
_logger.LogWarning("No active bots found. Skipping balance tracking.");
return;
}
_logger.LogInformation($"Found {bots.Count} active bots. Proceeding with balance tracking.");
await TrackBalances(bots);
_logger.LogInformation("Completed balance tracking");
}
private async Task TrackBalances(List<ITradingBot> bots)
{
// Group bots by agent/user
var botsByAgent = bots
.Where(b => b.User != null)
@@ -59,8 +74,25 @@ public class BalanceTrackingWorker : BackgroundService
{
var agentName = agentEntry.Key;
var agentBots = agentEntry.Value;
// Check if we need to update this agent's balance
var lastBalance = (await _agentBalanceRepository.GetAgentBalances(
agentName,
DateTime.UtcNow.AddDays(-1),
DateTime.UtcNow)).OrderByDescending(b => b.Time).FirstOrDefault();
if (lastBalance != null && DateTime.UtcNow.Subtract(lastBalance.Time).TotalHours < 24)
{
_logger.LogInformation(
$"Skipping agent {agentName} - Last balance update was {lastBalance.Time:g} UTC");
continue;
}
decimal totalAgentValue = 0;
decimal totalBotAllocatedBalance = 0;
decimal totalAccountUsdValue = 0;
decimal botsAllocationUsdValue = 0;
decimal totalPnL = 0;
_logger.LogInformation($"Processing agent: {agentName} with {agentBots.Count} bots");
@@ -80,7 +112,7 @@ public class BalanceTrackingWorker : BackgroundService
if (accountBalance.Balances != null)
{
var accountTotalValue = accountBalance.Balances.Sum(b => b.Value);
// If this is the account that holds the bot balances (USDC), subtract the allocated amounts
var usdcBalance = accountBalance.Balances.FirstOrDefault(b => b.TokenName == "USDC");
if (usdcBalance != null)
@@ -92,11 +124,11 @@ public class BalanceTrackingWorker : BackgroundService
$"Account {accountBalance.Name} USDC balance after bot allocation: {usdcBalance.Value} USD");
}
totalAgentValue += accountTotalValue;
totalAccountUsdValue += accountTotalValue;
_logger.LogInformation(
$"Account {accountBalance.Name} total value: {accountTotalValue} USD");
// Log individual token balances for debugging
foreach (var balance in accountBalance.Balances)
{
@@ -106,25 +138,42 @@ public class BalanceTrackingWorker : BackgroundService
}
}
// Add up all bot wallet balances for this agent
// Process all bots in a single iteration
foreach (var bot in agentBots)
{
// Get wallet balance
var latestBotBalance = bot.WalletBalances
.OrderByDescending(x => x.Key)
.FirstOrDefault();
if (latestBotBalance.Key != default)
{
totalAgentValue += latestBotBalance.Value;
botsAllocationUsdValue += latestBotBalance.Value;
_logger.LogInformation(
$"Bot {bot.Name} wallet balance: {latestBotBalance.Value} USD at {latestBotBalance.Key}");
}
// Calculate PnL
totalPnL += bot.GetProfitAndLoss();
}
totalAgentValue = totalAccountUsdValue + botsAllocationUsdValue;
_logger.LogInformation(
$"Agent {agentName} total aggregated value: {totalAgentValue} USD");
// TODO: Save aggregated agent balance to database
$"Agent {agentName} total aggregated value: {totalAgentValue} USD (Account: {totalAccountUsdValue} USD, Bot Wallet: {botsAllocationUsdValue} USD)");
// Create and save the agent balance
var agentBalance = new AgentBalance
{
AgentName = agentName,
TotalValue = totalAgentValue,
TotalAccountUsdValue = totalAccountUsdValue,
BotsAllocationUsdValue = botsAllocationUsdValue,
PnL = totalPnL,
Time = DateTime.UtcNow
};
_agentBalanceRepository.InsertAgentBalance(agentBalance);
}
catch (Exception ex)
{

View File

@@ -1,10 +1,10 @@
using Managing.Application.ManageBot;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using MediatR;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Api.Workers;
namespace Managing.Application.Workers;
public class BotManagerWorker(
ILogger<BotManagerWorker> logger,

View File

@@ -1,6 +1,4 @@
using System.Reflection;
using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients;
using Discord.Commands;
using Discord.WebSocket;
using FluentValidation;
@@ -55,7 +53,10 @@ public static class ApiBootstrap
public static IServiceCollection RegisterApiDependencies(this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<Web3ProxySettings>(configuration.GetSection("Web3Proxy"));
return services
.AddWorkers(configuration)
.AddApplication()
.AddInfrastructure(configuration)
.AddFluentValidation()
@@ -78,6 +79,22 @@ public static class ApiBootstrap
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
// Processors
services.AddTransient<IExchangeProcessor, EvmProcessor>();
services.AddTransient<IExchangeProcessor, FtxProcessor>();
services.AddTransient<IExchangeProcessor, BinanceProcessor>();
services.AddTransient<IExchangeProcessor, KrakenProcessor>();
services.AddTransient<ITradaoService, TradaoService>();
services.AddTransient<IExchangeService, ExchangeService>();
services.AddTransient<IExchangeStream, ExchangeStream>();
services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IDiscordService, DiscordService>();
services.AddSingleton<IBotService, BotService>();
services.AddSingleton<IWorkerService, WorkerService>();
services.AddTransient<IPrivyService, PrivyService>();
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
return services;
}
@@ -114,36 +131,27 @@ public static class ApiBootstrap
services.AddTransient<IWorkflowRepository, WorkflowRepository>();
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IWorkerRepository, WorkerRepository>();
services.AddTransient<IAgentBalanceRepository, AgentBalanceRepository>();
// Cache
services.AddDistributedMemoryCache();
services.AddTransient<ICacheService, CacheService>();
services.AddSingleton<ITaskCache, TaskCache>();
// Processors
services.AddTransient<IExchangeProcessor, EvmProcessor>();
services.AddTransient<IExchangeProcessor, FtxProcessor>();
services.AddTransient<IExchangeProcessor, BinanceProcessor>();
services.AddTransient<IExchangeProcessor, KrakenProcessor>();
return services;
}
// Services
services.AddTransient<ITradaoService, TradaoService>();
services.AddTransient<IExchangeService, ExchangeService>();
services.AddTransient<IExchangeStream, ExchangeStream>();
services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IDiscordService, DiscordService>();
services.AddSingleton<IBotService, BotService>();
services.AddSingleton<IWorkerService, WorkerService>();
services.AddTransient<IPrivyService, PrivyService>();
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
{
if (configuration.GetValue<bool>("Workers:BotManager", false))
{
services.AddHostedService<BotManagerWorker>();
}
// Web3Proxy Configuration
services.Configure<Web3ProxySettings>(configuration.GetSection("Web3Proxy"));
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
// Stream
services.AddSingleton<IBinanceSocketClient, BinanceSocketClient>();
services.AddSingleton<IStreamService, StreamService>();
if (configuration.GetValue<bool>("Workers:BalancesTracking", false))
{
services.AddHostedService<BalanceTrackingWorker>();
}
return services;
}

View File

@@ -380,7 +380,8 @@ public static class Enums
LeaderboardWorker,
Noobiesboard,
BotManager,
FundingRatesWatcher
FundingRatesWatcher,
BalanceTracking
}
public enum WorkflowUsage

View File

@@ -0,0 +1,16 @@
public class AgentBalance
{
public string AgentName { get; set; }
public decimal TotalValue { get; set; }
public decimal TotalAccountUsdValue { get; set; }
public decimal BotsAllocationUsdValue { get; set; }
public decimal PnL { get; set; }
public DateTime Time { get; set; }
public IList<PnLPoint> PnLHistory { get; set; }
}
public class PnLPoint
{
public DateTime Time { get; set; }
public decimal Value { get; set; }
}

View File

@@ -0,0 +1,18 @@
namespace Managing.Domain.Statistics;
public class AgentBalanceHistory
{
public string AgentName { get; set; }
public decimal TotalValue { get; set; }
public decimal TotalAccountUsdValue { get; set; }
public decimal BotsAllocationUsdValue { get; set; }
public decimal PnL { get; set; }
public DateTime Time { get; set; }
public IList<PnLPoint> PnLHistory { get; set; }
}
public class PnLPoint
{
public DateTime Time { get; set; }
public decimal Value { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace Managing.Domain.Statistics;
public class AgentBalance
{
public string AgentName { get; set; }
public decimal TotalValue { get; set; }
public decimal TotalAccountUsdValue { get; set; }
public decimal BotsAllocationUsdValue { get; set; }
public decimal PnL { get; set; }
public DateTime Time { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Managing.Domain.Statistics;
public class AgentBalanceHistory
{
public string AgentName { get; set; }
public IList<AgentBalance> AgentBalances { get; set; }
}

View File

@@ -0,0 +1,107 @@
using InfluxDB.Client.Api.Domain;
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Statistics;
using Managing.Infrastructure.Databases.InfluxDb.Abstractions;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Databases;
public class AgentBalanceRepository : IAgentBalanceRepository
{
private readonly string _balanceBucket = "agent-balances-bucket";
private readonly IInfluxDbRepository _influxDbRepository;
private readonly ILogger<AgentBalanceRepository> _logger;
public AgentBalanceRepository(IInfluxDbRepository influxDbRepository, ILogger<AgentBalanceRepository> logger)
{
_influxDbRepository = influxDbRepository;
_logger = logger;
}
public void InsertAgentBalance(AgentBalance balance)
{
_influxDbRepository.Write(write =>
{
var balanceDto = new AgentBalanceDto
{
AgentName = balance.AgentName,
TotalValue = balance.TotalValue,
TotalAccountUsdValue = balance.TotalAccountUsdValue,
BotsAllocationUsdValue = balance.BotsAllocationUsdValue,
PnL = balance.PnL,
Time = balance.Time
};
write.WriteMeasurement(
balanceDto,
WritePrecision.Ns,
_balanceBucket,
_influxDbRepository.Organization);
});
}
public async Task<IList<AgentBalance>> GetAgentBalances(string agentName, DateTime start, DateTime? end = null)
{
var results = await _influxDbRepository.QueryAsync(async query =>
{
var flux = $"from(bucket:\"{_balanceBucket}\") " +
$"|> range(start: {start:s}Z" +
(end.HasValue ? $", stop: {end.Value:s}Z" : "") +
$") " +
$"|> filter(fn: (r) => r[\"agent_name\"] == \"{agentName}\")";
var result = await query.QueryAsync<AgentBalanceDto>(flux, _influxDbRepository.Organization);
return result.Select(balance => new AgentBalance
{
AgentName = balance.AgentName,
TotalValue = balance.TotalValue,
TotalAccountUsdValue = balance.TotalAccountUsdValue,
BotsAllocationUsdValue = balance.BotsAllocationUsdValue,
PnL = balance.PnL,
Time = balance.Time
}).ToList();
});
return results;
}
public async Task<(IList<AgentBalanceHistory> result, int totalCount)> GetAllAgentBalancesWithHistory(
DateTime start, DateTime? end)
{
var results = await _influxDbRepository.QueryAsync(async query =>
{
// Get all balances within the time range, pivoted so each row is a full AgentBalanceDto
var flux = $@"
from(bucket: ""{_balanceBucket}"")
|> range(start: {start:s}Z{(end.HasValue ? $", stop: {end.Value:s}Z" : "")})
|> filter(fn: (r) => r._measurement == ""agent_balance"")
|> pivot(rowKey: [""_time""], columnKey: [""_field""], valueColumn: ""_value"")
";
var balances = await query.QueryAsync<AgentBalanceDto>(flux, _influxDbRepository.Organization);
// Group balances by agent name
var agentGroups = balances
.GroupBy(b => b.AgentName)
.Select(g => new AgentBalanceHistory
{
AgentName = g.Key,
AgentBalances = g.Select(b => new AgentBalance
{
AgentName = b.AgentName,
TotalValue = b.TotalValue,
TotalAccountUsdValue = b.TotalAccountUsdValue,
BotsAllocationUsdValue = b.BotsAllocationUsdValue,
PnL = b.PnL,
Time = b.Time
}).OrderBy(b => b.Time).ToList()
}).ToList();
return (agentGroups, agentGroups.Count);
});
return results;
}
}

View File

@@ -0,0 +1,19 @@
using InfluxDB.Client.Core;
namespace Managing.Infrastructure.Databases.InfluxDb.Models;
[Measurement("agent_balance")]
public class AgentBalanceDto
{
[Column("agent_name", IsTag = true)] public string AgentName { get; set; }
[Column("total_value")] public decimal TotalValue { get; set; }
[Column("total_account_usd_value")] public decimal TotalAccountUsdValue { get; set; }
[Column("bots_allocation_usd_value")] public decimal BotsAllocationUsdValue { get; set; }
[Column("pnl")] public decimal PnL { get; set; }
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
}

View File

@@ -1,5 +1,5 @@
using Discord.WebSocket;
using Managing.Application.Workers.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Statistics;
namespace Managing.Infrastructure.Messengers.Discord;

View File

@@ -1131,6 +1131,98 @@ export class DataClient extends AuthorizedApiBase {
}
return Promise.resolve<PlatformSummaryViewModel>(null as any);
}
data_GetAgentBalances(agentName: string | null | undefined, startDate: Date | undefined, endDate: Date | null | undefined): Promise<AgentBalance[]> {
let url_ = this.baseUrl + "/Data/GetAgentBalances?";
if (agentName !== undefined && agentName !== null)
url_ += "agentName=" + encodeURIComponent("" + agentName) + "&";
if (startDate === null)
throw new Error("The parameter 'startDate' cannot be null.");
else if (startDate !== undefined)
url_ += "startDate=" + encodeURIComponent(startDate ? "" + startDate.toISOString() : "") + "&";
if (endDate !== undefined && endDate !== null)
url_ += "endDate=" + encodeURIComponent(endDate ? "" + endDate.toISOString() : "") + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processData_GetAgentBalances(_response);
});
}
protected processData_GetAgentBalances(response: Response): Promise<AgentBalance[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AgentBalance[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<AgentBalance[]>(null as any);
}
data_GetBestAgents(startDate: Date | undefined, endDate: Date | null | undefined, page: number | undefined, pageSize: number | undefined): Promise<BestAgentsResponse> {
let url_ = this.baseUrl + "/Data/GetBestAgents?";
if (startDate === null)
throw new Error("The parameter 'startDate' cannot be null.");
else if (startDate !== undefined)
url_ += "startDate=" + encodeURIComponent(startDate ? "" + startDate.toISOString() : "") + "&";
if (endDate !== undefined && endDate !== null)
url_ += "endDate=" + encodeURIComponent(endDate ? "" + endDate.toISOString() : "") + "&";
if (page === null)
throw new Error("The parameter 'page' cannot be null.");
else if (page !== undefined)
url_ += "page=" + encodeURIComponent("" + page) + "&";
if (pageSize === null)
throw new Error("The parameter 'pageSize' cannot be null.");
else if (pageSize !== undefined)
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processData_GetBestAgents(_response);
});
}
protected processData_GetBestAgents(response: Response): Promise<BestAgentsResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BestAgentsResponse;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<BestAgentsResponse>(null as any);
}
}
export class MoneyManagementClient extends AuthorizedApiBase {
@@ -3132,6 +3224,7 @@ export interface UserStrategyDetailsViewModel {
wins?: number;
losses?: number;
positions?: Position[] | null;
identifier?: string | null;
}
export interface PlatformSummaryViewModel {
@@ -3158,6 +3251,38 @@ export interface AgentSummaryViewModel {
volumeLast24h?: number;
}
export interface AgentBalance {
agentName?: string | null;
totalValue?: number;
totalAccountUsdValue?: number;
botsAllocationUsdValue?: number;
pnL?: number;
time?: Date;
}
export interface BestAgentsResponse {
agents?: AgentBalanceHistory[] | null;
totalCount?: number;
currentPage?: number;
pageSize?: number;
totalPages?: number;
}
export interface AgentBalanceHistory {
agentName?: string | null;
totalValue?: number;
totalAccountUsdValue?: number;
botsAllocationUsdValue?: number;
pnL?: number;
time?: Date;
pnLHistory?: PnLPoint[] | null;
}
export interface PnLPoint {
time?: Date;
value?: number;
}
export enum RiskLevel {
Low = "Low",
Medium = "Medium",

View File

@@ -0,0 +1,92 @@
import React, {useEffect, useState} from 'react'
import {GridTile, Table} from '../../../components/mollecules'
import useApiUrlStore from '../../../app/store/apiStore'
import {type AgentBalanceHistory, type BestAgentsResponse, DataClient} from '../../../generated/ManagingApi'
// Extend the type to include agentBalances for runtime use
export interface AgentBalanceWithBalances extends AgentBalanceHistory {
agentBalances?: Array<{
totalValue?: number
totalAccountUsdValue?: number
botsAllocationUsdValue?: number
pnL?: number
time?: string
}>;
}
function BestAgents() {
const { apiUrl } = useApiUrlStore()
const [data, setData] = useState<AgentBalanceWithBalances[]>([])
const [isLoading, setIsLoading] = useState(true)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [totalPages, setTotalPages] = useState(1)
const [expandedAgent, setExpandedAgent] = useState<string | null>(null)
useEffect(() => {
setIsLoading(true)
const client = new DataClient({}, apiUrl)
const now = new Date()
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30)
client.data_GetBestAgents(startDate, now, page, pageSize).then((res: BestAgentsResponse) => {
setData(res.agents as AgentBalanceWithBalances[] ?? [])
setTotalPages(res.totalPages ?? 1)
console.log(res)
}).finally(() => setIsLoading(false))
}, [apiUrl, page, pageSize])
// Type guard for agentBalances
function hasAgentBalances(agent: AgentBalanceWithBalances): agent is Required<AgentBalanceWithBalances> {
return Array.isArray(agent.agentBalances) && agent.agentBalances.length > 0;
}
// Get the latest balance for each agent
const latestBalances = data.map(agent => {
if (hasAgentBalances(agent)) {
const lastBalance = agent.agentBalances[agent.agentBalances.length - 1]
return {
agentName: agent.agentName,
originalAgent: agent, // Store the original agent for row details
...lastBalance
}
}
// fallback: just agentName
return { agentName: agent.agentName, originalAgent: agent }
})
const columns = [
{ Header: 'Agent', accessor: 'agentName' },
{ Header: 'Total Value (USD)', accessor: 'totalValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'Account Value (USD)', accessor: 'totalAccountUsdValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'Bots Allocation (USD)', accessor: 'botsAllocationUsdValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'PnL (USD)', accessor: 'pnL', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'Last Update', accessor: 'time', Cell: ({ value }: any) => value ? new Date(value).toLocaleString() : '' },
]
return (
<div className="container mx-auto pt-6">
<GridTile title="Best Agents">
{isLoading ? (
<progress className="progress progress-primary w-56"></progress>
) : (
<Table
columns={columns}
data={latestBalances}
showPagination={false}
/>
)}
<div className="flex justify-between items-center mt-4">
<button className="btn" onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1}>Previous</button>
<span>Page {page} of {totalPages}</span>
<button className="btn" onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={page === totalPages}>Next</button>
<select className="select select-bordered ml-2" value={pageSize} onChange={e => { setPageSize(Number(e.target.value)); setPage(1) }}>
{[10, 20, 30, 40, 50].map(size => <option key={size} value={size}>Show {size}</option>)}
</select>
</div>
</GridTile>
</div>
)
}
export default BestAgents

View File

@@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'
import React, {useEffect, useState} from 'react'
import Tabs from '../../components/mollecules/Tabs/Tabs'
import type { ITabsType } from '../../global/type'
import type {ITabsType} from '../../global/type'
import Analytics from './analytics/analytics'
import Monitoring from './monitoring'
import BestAgents from './analytics/bestAgents'
const tabs: ITabsType = [
{
@@ -17,6 +18,11 @@ const tabs: ITabsType = [
index: 2,
label: 'Analytics',
},
{
Component: BestAgents,
index: 3,
label: 'Best Agents',
},
]
const Dashboard: React.FC = () => {