Update balance tracking

This commit is contained in:
2025-10-03 16:43:20 +07:00
parent 6928770da7
commit f72bfc4ab8
11 changed files with 136 additions and 34 deletions

View File

@@ -5,7 +5,7 @@ namespace Managing.Application.Abstractions.Repositories;
public interface IAgentBalanceRepository public interface IAgentBalanceRepository
{ {
void InsertAgentBalance(AgentBalance balance); void InsertAgentBalance(AgentBalance balance);
Task<IList<AgentBalance>> GetAgentBalances(string agentName, DateTime start, DateTime? end = null); Task<IList<AgentBalance>> GetAgentBalancesByUserId(int userId, DateTime start, DateTime? end = null);
Task<(IList<AgentBalanceHistory> result, int totalCount)> GetAllAgentBalancesWithHistory(DateTime start, Task<(IList<AgentBalanceHistory> result, int totalCount)> GetAllAgentBalancesWithHistory(DateTime start,
DateTime? end); DateTime? end);

View File

@@ -5,6 +5,7 @@ namespace Managing.Application.Abstractions.Services;
public interface IAgentService public interface IAgentService
{ {
Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start, DateTime? end = null); Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start, DateTime? end = null);
Task<AgentBalanceHistory> GetAgentBalancesByUserId(int userId, DateTime start, DateTime? end = null);
Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(DateTime start, DateTime? end = null, Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(DateTime start, DateTime? end = null,
int page = 1, int page = 1,

View File

@@ -31,9 +31,40 @@ public class AgentService : IAgentService
public async Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start, public async Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start,
DateTime? end = null) DateTime? end = null)
{
// Get userId from agentName by looking up the user
var userId = await ServiceScopeHelpers.WithScopedService<IUserService, int?>(_serviceScopeFactory,
async (userService) =>
{
var user = await userService.GetUserByAgentName(agentName);
return user?.Id;
});
if (!userId.HasValue)
{
// Return empty result if user not found
return new AgentBalanceHistory
{
UserId = 0,
AgentName = agentName,
AgentBalances = new List<AgentBalance>()
};
}
// Use the UserId-based method internally
var result = await GetAgentBalancesByUserId(userId.Value, start, end);
// Override the AgentName to use the requested agentName instead of the default
result.AgentName = agentName;
return result;
}
public async Task<AgentBalanceHistory> GetAgentBalancesByUserId(int userId, DateTime start,
DateTime? end = null)
{ {
var effectiveEnd = end ?? DateTime.UtcNow; var effectiveEnd = end ?? DateTime.UtcNow;
string cacheKey = $"AgentBalances_{agentName}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}"; string cacheKey = $"AgentBalancesByUserId_{userId}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";
// Check if the balances are already cached // Check if the balances are already cached
var cachedBalances = _cacheService.GetValue<AgentBalanceHistory>(cacheKey); var cachedBalances = _cacheService.GetValue<AgentBalanceHistory>(cacheKey);
@@ -43,12 +74,13 @@ public class AgentService : IAgentService
return cachedBalances; return cachedBalances;
} }
var balances = await _agentBalanceRepository.GetAgentBalances(agentName, start, end); var balances = await _agentBalanceRepository.GetAgentBalancesByUserId(userId, start, end);
// Create a single AgentBalanceHistory with all balances // Create a single AgentBalanceHistory with all balances
var result = new AgentBalanceHistory var result = new AgentBalanceHistory
{ {
AgentName = agentName, UserId = userId,
AgentName = $"User_{userId}",
AgentBalances = balances.OrderBy(b => b.Time).ToList() AgentBalances = balances.OrderBy(b => b.Time).ToList()
}; };

View File

@@ -193,8 +193,10 @@ public class AgentGrain : Grain, IAgentGrain
_ => 0 _ => 0
}; };
// Calculate total balance (USDC + open positions value) // Calculate total balance (USDC wallet + USDC in open positions value)
decimal totalBalance = 0; decimal totalBalance = 0;
decimal usdcWalletValue = 0;
decimal usdcInPositionsValue = 0;
try try
{ {
var userId = (int)this.GetPrimaryKeyLong(); var userId = (int)this.GetPrimaryKeyLong();
@@ -209,20 +211,25 @@ public class AgentGrain : Grain, IAgentGrain
// Get USDC balance // Get USDC balance
var usdcBalances = await GetOrRefreshBalanceDataAsync(account.Name); var usdcBalances = await GetOrRefreshBalanceDataAsync(account.Name);
var usdcBalance = usdcBalances?.UsdcValue ?? 0; var usdcBalance = usdcBalances?.UsdcValue ?? 0;
totalBalance += usdcBalance; usdcWalletValue += usdcBalance;
} }
foreach (var position in positions.Where(p => !p.IsFinished())) foreach (var position in positions.Where(p => !p.IsFinished()))
{ {
totalBalance += position.Open.Price * position.Open.Quantity; var positionUsd = position.Open.Price * position.Open.Quantity;
totalBalance += position.ProfitAndLoss?.Realized ?? 0; var realized = position.ProfitAndLoss?.Realized ?? 0;
usdcInPositionsValue += positionUsd + realized;
} }
totalBalance = usdcWalletValue + usdcInPositionsValue;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error calculating total balance for agent {UserId}", this.GetPrimaryKeyLong()); _logger.LogError(ex, "Error calculating total balance for agent {UserId}", this.GetPrimaryKeyLong());
totalBalance = 0; // Set to 0 if calculation fails totalBalance = 0; // Set to 0 if calculation fails
usdcWalletValue = 0;
usdcInPositionsValue = 0;
} }
var bots = await ServiceScopeHelpers.WithScopedService<IBotService, IEnumerable<Bot>>(_scopeFactory, var bots = await ServiceScopeHelpers.WithScopedService<IBotService, IEnumerable<Bot>>(_scopeFactory,
@@ -269,7 +276,7 @@ public class AgentGrain : Grain, IAgentGrain
await _agentService.SaveOrUpdateAgentSummary(summary); await _agentService.SaveOrUpdateAgentSummary(summary);
// Insert balance tracking data // Insert balance tracking data
InsertBalanceTrackingData(totalBalance, botsAllocationUsdValue, netPnL); InsertBalanceTrackingData(totalBalance, botsAllocationUsdValue, netPnL, usdcWalletValue, usdcInPositionsValue);
_logger.LogDebug( _logger.LogDebug(
"Updated agent summary from position data for user {UserId}: NetPnL={NetPnL}, TotalPnL={TotalPnL}, Fees={Fees}, Volume={Volume}, Wins={Wins}, Losses={Losses}", "Updated agent summary from position data for user {UserId}: NetPnL={NetPnL}, TotalPnL={TotalPnL}, Fees={Fees}, Volume={Volume}, Wins={Wins}, Losses={Losses}",
@@ -626,14 +633,16 @@ public class AgentGrain : Grain, IAgentGrain
/// <summary> /// <summary>
/// Inserts balance tracking data into the AgentBalanceRepository /// Inserts balance tracking data into the AgentBalanceRepository
/// </summary> /// </summary>
private void InsertBalanceTrackingData(decimal totalAccountUsdValue, decimal botsAllocationUsdValue, decimal pnl) private void InsertBalanceTrackingData(decimal totalAccountUsdValue, decimal botsAllocationUsdValue, decimal pnl, decimal usdcWalletValue, decimal usdcInPositionsValue)
{ {
try try
{ {
var agentBalance = new AgentBalance var agentBalance = new AgentBalance
{ {
AgentName = _state.State.AgentName, UserId = (int)this.GetPrimaryKeyLong(),
TotalBalanceValue = totalAccountUsdValue, TotalBalanceValue = totalAccountUsdValue,
UsdcWalletValue = usdcWalletValue,
UsdcInPositionsValue = usdcInPositionsValue,
BotsAllocationUsdValue = botsAllocationUsdValue, BotsAllocationUsdValue = botsAllocationUsdValue,
PnL = pnl, PnL = pnl,
Time = DateTime.UtcNow Time = DateTime.UtcNow
@@ -642,13 +651,13 @@ public class AgentGrain : Grain, IAgentGrain
_agentBalanceRepository.InsertAgentBalance(agentBalance); _agentBalanceRepository.InsertAgentBalance(agentBalance);
_logger.LogDebug( _logger.LogDebug(
"Inserted balance tracking data for agent {AgentName}: TotalBalanceValue={TotalBalanceValue}, BotsAllocationUsdValue={BotsAllocationUsdValue}, PnL={PnL}", "Inserted balance tracking data for user {UserId}: TotalBalanceValue={TotalBalanceValue}, BotsAllocationUsdValue={BotsAllocationUsdValue}, PnL={PnL}",
agentBalance.AgentName, agentBalance.TotalBalanceValue, agentBalance.BotsAllocationUsdValue, agentBalance.UserId, agentBalance.TotalBalanceValue, agentBalance.BotsAllocationUsdValue,
agentBalance.PnL); agentBalance.PnL);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error inserting balance tracking data for agent {AgentName}", _state.State.AgentName); _logger.LogError(ex, "Error inserting balance tracking data for user {UserId}", (int)this.GetPrimaryKeyLong());
} }
} }
} }

View File

@@ -2,8 +2,10 @@ namespace Managing.Domain.Statistics;
public class AgentBalance public class AgentBalance
{ {
public string AgentName { get; set; } public int UserId { get; set; }
public decimal TotalBalanceValue { get; set; } public decimal TotalBalanceValue { get; set; }
public decimal UsdcWalletValue { get; set; }
public decimal UsdcInPositionsValue { get; set; }
public decimal BotsAllocationUsdValue { get; set; } public decimal BotsAllocationUsdValue { get; set; }
public decimal PnL { get; set; } public decimal PnL { get; set; }
public DateTime Time { get; set; } public DateTime Time { get; set; }

View File

@@ -2,6 +2,7 @@ namespace Managing.Domain.Statistics;
public class AgentBalanceHistory public class AgentBalanceHistory
{ {
public int UserId { get; set; }
public string AgentName { get; set; } public string AgentName { get; set; }
public List<AgentBalance> AgentBalances { get; set; } public List<AgentBalance> AgentBalances { get; set; }

View File

@@ -25,8 +25,10 @@ public class AgentBalanceRepository : IAgentBalanceRepository
{ {
var balanceDto = new AgentBalanceDto var balanceDto = new AgentBalanceDto
{ {
AgentName = balance.AgentName, UserId = balance.UserId,
TotalBalanceValue = balance.TotalBalanceValue, TotalBalanceValue = balance.TotalBalanceValue,
UsdcWalletValue = balance.UsdcWalletValue,
UsdcInPositionsValue = balance.UsdcInPositionsValue,
BotsAllocationUsdValue = balance.BotsAllocationUsdValue, BotsAllocationUsdValue = balance.BotsAllocationUsdValue,
PnL = balance.PnL, PnL = balance.PnL,
Time = balance.Time Time = balance.Time
@@ -40,7 +42,7 @@ public class AgentBalanceRepository : IAgentBalanceRepository
}); });
} }
public async Task<IList<AgentBalance>> GetAgentBalances(string agentName, DateTime start, DateTime? end = null) public async Task<IList<AgentBalance>> GetAgentBalancesByUserId(int userId, DateTime start, DateTime? end = null)
{ {
var results = await _influxDbRepository.QueryAsync(async query => var results = await _influxDbRepository.QueryAsync(async query =>
{ {
@@ -48,15 +50,17 @@ public class AgentBalanceRepository : IAgentBalanceRepository
$"|> range(start: {start:s}Z" + $"|> range(start: {start:s}Z" +
(end.HasValue ? $", stop: {end.Value:s}Z" : "") + (end.HasValue ? $", stop: {end.Value:s}Z" : "") +
$") " + $") " +
$"|> filter(fn: (r) => r[\"agent_name\"] == \"{agentName}\") " + $"|> filter(fn: (r) => r[\"user_id\"] == \"{userId}\") " +
$"|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")"; $"|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")";
var result = await query.QueryAsync<AgentBalanceDto>(flux, _influxDbRepository.Organization); var result = await query.QueryAsync<AgentBalanceDto>(flux, _influxDbRepository.Organization);
return result.Select(balance => new AgentBalance return result.Select(balance => new AgentBalance
{ {
AgentName = balance.AgentName, UserId = balance.UserId,
TotalBalanceValue = balance.TotalBalanceValue, TotalBalanceValue = balance.TotalBalanceValue,
UsdcWalletValue = balance.UsdcWalletValue,
UsdcInPositionsValue = balance.UsdcInPositionsValue,
BotsAllocationUsdValue = balance.BotsAllocationUsdValue, BotsAllocationUsdValue = balance.BotsAllocationUsdValue,
PnL = balance.PnL, PnL = balance.PnL,
Time = balance.Time Time = balance.Time
@@ -81,20 +85,31 @@ public class AgentBalanceRepository : IAgentBalanceRepository
var balances = await query.QueryAsync<AgentBalanceDto>(flux, _influxDbRepository.Organization); var balances = await query.QueryAsync<AgentBalanceDto>(flux, _influxDbRepository.Organization);
// Group balances by agent name // Group balances by user ID
var agentGroups = balances var agentGroups = balances
.GroupBy(b => b.AgentName) .GroupBy(b => b.UserId)
.Select(g => new AgentBalanceHistory .Select(g =>
{ {
AgentName = g.Key, var userBalances = g.Select(b => new AgentBalance
AgentBalances = g.Select(b => new AgentBalance
{ {
AgentName = b.AgentName, UserId = b.UserId,
TotalBalanceValue = b.TotalBalanceValue, TotalBalanceValue = b.TotalBalanceValue,
UsdcWalletValue = b.UsdcWalletValue,
UsdcInPositionsValue = b.UsdcInPositionsValue,
BotsAllocationUsdValue = b.BotsAllocationUsdValue, BotsAllocationUsdValue = b.BotsAllocationUsdValue,
PnL = b.PnL, PnL = b.PnL,
Time = b.Time Time = b.Time
}).OrderBy(b => b.Time).ToList() }).OrderBy(b => b.Time).ToList();
// Use a default agent name since we don't store it in AgentBalance anymore
var mostRecentAgentName = $"User_{g.Key}";
return new AgentBalanceHistory
{
UserId = g.Key,
AgentName = mostRecentAgentName,
AgentBalances = userBalances
};
}).ToList(); }).ToList();
return (agentGroups, agentGroups.Count); return (agentGroups, agentGroups.Count);

View File

@@ -5,10 +5,14 @@ namespace Managing.Infrastructure.Databases.InfluxDb.Models;
[Measurement("agent_balance")] [Measurement("agent_balance")]
public class AgentBalanceDto public class AgentBalanceDto
{ {
[Column("agent_name", IsTag = true)] public string AgentName { get; set; } [Column("user_id", IsTag = true)] public int UserId { get; set; }
[Column("total_balance_value")] public decimal TotalBalanceValue { get; set; } [Column("total_balance_value")] public decimal TotalBalanceValue { get; set; }
[Column("usdc_wallet_value")] public decimal UsdcWalletValue { get; set; }
[Column("usdc_in_positions_value")] public decimal UsdcInPositionsValue { get; set; }
[Column("bots_allocation_usd_value")] public decimal BotsAllocationUsdValue { get; set; } [Column("bots_allocation_usd_value")] public decimal BotsAllocationUsdValue { get; set; }
[Column("pnl")] public decimal PnL { get; set; } [Column("pnl")] public decimal PnL { get; set; }

View File

@@ -4588,13 +4588,16 @@ export enum SortableFields {
} }
export interface AgentBalanceHistory { export interface AgentBalanceHistory {
userId?: number;
agentName?: string | null; agentName?: string | null;
agentBalances?: AgentBalance[] | null; agentBalances?: AgentBalance[] | null;
} }
export interface AgentBalance { export interface AgentBalance {
agentName?: string | null; userId?: number;
totalBalanceValue?: number; totalBalanceValue?: number;
usdcWalletValue?: number;
usdcInPositionsValue?: number;
botsAllocationUsdValue?: number; botsAllocationUsdValue?: number;
pnL?: number; pnL?: number;
time?: Date; time?: Date;

View File

@@ -1071,13 +1071,16 @@ export enum SortableFields {
} }
export interface AgentBalanceHistory { export interface AgentBalanceHistory {
userId?: number;
agentName?: string | null; agentName?: string | null;
agentBalances?: AgentBalance[] | null; agentBalances?: AgentBalance[] | null;
} }
export interface AgentBalance { export interface AgentBalance {
agentName?: string | null; userId?: number;
totalBalanceValue?: number; totalBalanceValue?: number;
usdcWalletValue?: number;
usdcInPositionsValue?: number;
botsAllocationUsdValue?: number; botsAllocationUsdValue?: number;
pnL?: number; pnL?: number;
time?: Date; time?: Date;

View File

@@ -80,6 +80,8 @@ function AgentSearch({ index }: { index: number }) {
const latestBalance = balances[balances.length - 1] const latestBalance = balances[balances.length - 1]
return { return {
totalBalanceValue: latestBalance.totalBalanceValue || 0, totalBalanceValue: latestBalance.totalBalanceValue || 0,
usdcWalletValue: latestBalance.usdcWalletValue || 0,
usdcInPositionsValue: latestBalance.usdcInPositionsValue || 0,
botsAllocation: latestBalance.botsAllocationUsdValue || 0, botsAllocation: latestBalance.botsAllocationUsdValue || 0,
} }
} }
@@ -139,12 +141,16 @@ function AgentSearch({ index }: { index: number }) {
const dates = sortedBalances.map(balance => new Date(balance.time || 0)) const dates = sortedBalances.map(balance => new Date(balance.time || 0))
const totalBalanceValues = sortedBalances.map(balance => balance.totalBalanceValue || 0) const totalBalanceValues = sortedBalances.map(balance => balance.totalBalanceValue || 0)
const usdcWalletValues = sortedBalances.map(balance => balance.usdcWalletValue || 0)
const usdcInPositionsValues = sortedBalances.map(balance => balance.usdcInPositionsValue || 0)
const botsAllocationValues = sortedBalances.map(balance => balance.botsAllocationUsdValue || 0) const botsAllocationValues = sortedBalances.map(balance => balance.botsAllocationUsdValue || 0)
const pnlValues = sortedBalances.map(balance => balance.pnL || 0) const pnlValues = sortedBalances.map(balance => balance.pnL || 0)
return { return {
dates, dates,
totalBalanceValues, totalBalanceValues,
usdcWalletValues,
usdcInPositionsValues,
botsAllocationValues, botsAllocationValues,
pnlValues pnlValues
} }
@@ -247,11 +253,19 @@ function AgentSearch({ index }: { index: number }) {
{currentBalance && ( {currentBalance && (
<div className="mb-6 p-4 bg-primary/10 rounded-lg border border-primary/20"> <div className="mb-6 p-4 bg-primary/10 rounded-lg border border-primary/20">
<h3 className="text-lg font-semibold mb-2">Current Balance</h3> <h3 className="text-lg font-semibold mb-2">Current Balance</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div> <div>
<div className="text-sm opacity-70">Total Balance Value</div> <div className="text-sm opacity-70">Total Balance Value</div>
<div className="text-2xl font-bold">${currentBalance.totalBalanceValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div> <div className="text-2xl font-bold">${currentBalance.totalBalanceValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
</div> </div>
<div>
<div className="text-sm opacity-70">USDC in Wallet</div>
<div className="text-xl font-semibold text-blue-500">${currentBalance.usdcWalletValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
</div>
<div>
<div className="text-sm opacity-70">USDC in Positions</div>
<div className="text-xl font-semibold text-green-500">${currentBalance.usdcInPositionsValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
</div>
<div> <div>
<div className="text-sm opacity-70">Bots Allocation</div> <div className="text-sm opacity-70">Bots Allocation</div>
<div className="text-xl font-semibold">${currentBalance.botsAllocation.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div> <div className="text-xl font-semibold">${currentBalance.botsAllocation.toLocaleString(undefined, { maximumFractionDigits: 2 })}</div>
@@ -275,14 +289,32 @@ function AgentSearch({ index }: { index: number }) {
line: { color: theme.primary, width: 3 }, line: { color: theme.primary, width: 3 },
marker: { size: 6 } marker: { size: 6 }
}, },
{
x: chartData.dates,
y: chartData.usdcWalletValues,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'USDC in Wallet',
line: { color: '#3b82f6', width: 2 },
marker: { size: 5 }
},
{
x: chartData.dates,
y: chartData.usdcInPositionsValues,
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'USDC in Positions',
line: { color: '#10b981', width: 2 },
marker: { size: 5 }
},
{ {
x: chartData.dates, x: chartData.dates,
y: chartData.botsAllocationValues, y: chartData.botsAllocationValues,
type: 'scatter' as const, type: 'scatter' as const,
mode: 'lines+markers' as const, mode: 'lines+markers' as const,
name: 'Bots Allocation', name: 'Bots Allocation',
line: { color: theme.success, width: 3 }, line: { color: theme.success, width: 2 },
marker: { size: 6 } marker: { size: 5 }
}, },
{ {
x: chartData.dates, x: chartData.dates,
@@ -290,8 +322,8 @@ function AgentSearch({ index }: { index: number }) {
type: 'scatter' as const, type: 'scatter' as const,
mode: 'lines+markers' as const, mode: 'lines+markers' as const,
name: 'PnL', name: 'PnL',
line: { color: theme.warning, width: 3 }, line: { color: theme.warning, width: 2 },
marker: { size: 6 } marker: { size: 5 }
} }
]} ]}
layout={{ layout={{