Update Agent balance tracking

This commit is contained in:
2025-10-03 15:30:39 +07:00
parent 43d301e47a
commit 7c13ad5f06
10 changed files with 101 additions and 210 deletions

View File

@@ -413,10 +413,11 @@ public class DataController : ControllerBase
// Convert to detailed view model with additional information using separate scopes to avoid DbContext concurrency // Convert to detailed view model with additional information using separate scopes to avoid DbContext concurrency
var result = await Task.WhenAll( var result = await Task.WhenAll(
userStrategies.Select(strategy => userStrategies.Select(strategy =>
ServiceScopeHelpers.WithScopedService<ITradingService, UserStrategyDetailsViewModel>( ServiceScopeHelpers.WithScopedService<ITradingService, UserStrategyDetailsViewModel>(
_serviceScopeFactory, _serviceScopeFactory,
async tradingService => await MapStrategyToViewModelAsync(strategy, agentBalanceHistory, tradingService))) async tradingService =>
await MapStrategyToViewModelAsync(strategy, agentBalanceHistory, tradingService)))
); );
return Ok(result); return Ok(result);
@@ -487,7 +488,7 @@ public class DataController : ControllerBase
// Convert agent balance history to wallet balances dictionary // Convert agent balance history to wallet balances dictionary
var walletBalances = agentBalanceHistory?.AgentBalances? var walletBalances = agentBalanceHistory?.AgentBalances?
.ToDictionary(b => b.Time, b => b.TotalValue) ?? new Dictionary<DateTime, decimal>(); .ToDictionary(b => b.Time, b => b.TotalBalanceValue) ?? new Dictionary<DateTime, decimal>();
return new UserStrategyDetailsViewModel return new UserStrategyDetailsViewModel
{ {

View File

@@ -22,6 +22,7 @@ public interface IBotService
Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction); Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction);
Task<Position> ClosePositionAsync(Guid identifier, Guid positionId); Task<Position> ClosePositionAsync(Guid identifier, Guid positionId);
Task<TradingBotConfig> GetBotConfig(Guid identifier); Task<TradingBotConfig> GetBotConfig(Guid identifier);
Task<IEnumerable<TradingBotConfig>> GetBotConfigsByIdsAsync(IEnumerable<Guid> botIds);
Task<bool> UpdateBotStatisticsAsync(Guid identifier); Task<bool> UpdateBotStatisticsAsync(Guid identifier);
Task<bool> SaveBotStatisticsAsync(Bot bot); Task<bool> SaveBotStatisticsAsync(Bot bot);

View File

@@ -1,6 +1,7 @@
#nullable enable #nullable enable
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Bots.Models; using Managing.Application.Bots.Models;
using Managing.Application.Orleans; using Managing.Application.Orleans;
@@ -31,6 +32,7 @@ public class AgentGrain : Grain, IAgentGrain
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly ITradingService _tradingService; private readonly ITradingService _tradingService;
private readonly IAgentBalanceRepository _agentBalanceRepository;
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory;
public AgentGrain( public AgentGrain(
@@ -43,6 +45,7 @@ public class AgentGrain : Grain, IAgentGrain
IUserService userService, IUserService userService,
IAccountService accountService, IAccountService accountService,
ITradingService tradingService, ITradingService tradingService,
IAgentBalanceRepository agentBalanceRepository,
IServiceScopeFactory scopeFactory) IServiceScopeFactory scopeFactory)
{ {
_state = state; _state = state;
@@ -53,6 +56,7 @@ public class AgentGrain : Grain, IAgentGrain
_userService = userService; _userService = userService;
_accountService = accountService; _accountService = accountService;
_tradingService = tradingService; _tradingService = tradingService;
_agentBalanceRepository = agentBalanceRepository;
_scopeFactory = scopeFactory; _scopeFactory = scopeFactory;
} }
@@ -88,7 +92,7 @@ public class AgentGrain : Grain, IAgentGrain
await _agentService.SaveOrUpdateAgentSummary(emptySummary); await _agentService.SaveOrUpdateAgentSummary(emptySummary);
_logger.LogInformation("Agent {UserId} initialized with name {AgentName} and empty summary", userId, agentName); _logger.LogInformation("Agent {UserId} initialized with name {AgentName} and empty summary", userId, agentName);
// Notify platform summary about new agent activation // Notify platform summary about new agent activation
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory => await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory =>
{ {
var platformGrain = grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary"); var platformGrain = grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
@@ -101,7 +105,7 @@ public class AgentGrain : Grain, IAgentGrain
{ {
_state.State.AgentName = agentName; _state.State.AgentName = agentName;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
// Use the efficient method to update only the agent name in the summary // Use the efficient method to update only the agent name in the summary
await _agentService.UpdateAgentSummaryNameAsync((int)this.GetPrimaryKeyLong(), agentName); await _agentService.UpdateAgentSummaryNameAsync((int)this.GetPrimaryKeyLong(), agentName);
_logger.LogInformation("Agent {UserId} updated with name {AgentName}", this.GetPrimaryKeyLong(), agentName); _logger.LogInformation("Agent {UserId} updated with name {AgentName}", this.GetPrimaryKeyLong(), agentName);
@@ -127,7 +131,8 @@ public class AgentGrain : Grain, IAgentGrain
{ {
try try
{ {
_logger.LogInformation("Position closed event received for user {UserId}, position: {PositionId}, PnL: {PnL}", _logger.LogInformation(
"Position closed event received for user {UserId}, position: {PositionId}, PnL: {PnL}",
this.GetPrimaryKeyLong(), evt.PositionIdentifier, evt.RealizedPnL); this.GetPrimaryKeyLong(), evt.PositionIdentifier, evt.RealizedPnL);
await UpdateSummary(); await UpdateSummary();
@@ -171,7 +176,7 @@ public class AgentGrain : Grain, IAgentGrain
var totalVolume = positions.Sum(p => p.Open.Price * p.Open.Quantity * p.Open.Leverage); var totalVolume = positions.Sum(p => p.Open.Price * p.Open.Quantity * p.Open.Leverage);
var collateral = positions.Sum(p => p.Open.Price * p.Open.Quantity); var collateral = positions.Sum(p => p.Open.Price * p.Open.Quantity);
var totalFees = positions.Sum(p => p.CalculateTotalFees()); var totalFees = positions.Sum(p => p.CalculateTotalFees());
// Store total fees in grain state for caching // Store total fees in grain state for caching
_state.State.TotalFees = totalFees; _state.State.TotalFees = totalFees;
@@ -220,11 +225,10 @@ public class AgentGrain : Grain, IAgentGrain
totalBalance = 0; // Set to 0 if calculation fails totalBalance = 0; // Set to 0 if calculation fails
} }
var bots = await ServiceScopeHelpers.WithScopedService<IBotService, IEnumerable<Bot>> (_scopeFactory, async (botService) => { var bots = await ServiceScopeHelpers.WithScopedService<IBotService, IEnumerable<Bot>>(_scopeFactory,
return await botService.GetBotsByUser((int)this.GetPrimaryKeyLong()); async (botService) => { return await botService.GetBotsByUser((int)this.GetPrimaryKeyLong()); });
});
// Calculate Runtime based on the earliest position date // Calculate Runtime based on the earliest position date
DateTime? runtime = null; DateTime? runtime = null;
if (positions.Any()) if (positions.Any())
{ {
@@ -233,6 +237,18 @@ public class AgentGrain : Grain, IAgentGrain
var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running); var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running);
// Calculate bots allocation USD value from bot configurations
var botsAllocationUsdValue = 0m;
if (bots.Any())
{
var botIds = bots.Select(b => b.Identifier);
var botConfigs =
await ServiceScopeHelpers.WithScopedService<IBotService, IEnumerable<TradingBotConfig>>(
_scopeFactory,
async (botService) => { return await botService.GetBotConfigsByIdsAsync(botIds); });
botsAllocationUsdValue = botConfigs.Sum(config => config.BotTradingBalance);
}
var summary = new AgentSummary var summary = new AgentSummary
{ {
UserId = (int)this.GetPrimaryKeyLong(), UserId = (int)this.GetPrimaryKeyLong(),
@@ -251,8 +267,12 @@ public class AgentGrain : Grain, IAgentGrain
// Save summary to database // Save summary to database
await _agentService.SaveOrUpdateAgentSummary(summary); await _agentService.SaveOrUpdateAgentSummary(summary);
_logger.LogDebug("Updated agent summary from position data for user {UserId}: NetPnL={NetPnL}, TotalPnL={TotalPnL}, Fees={Fees}, Volume={Volume}, Wins={Wins}, Losses={Losses}", // Insert balance tracking data
InsertBalanceTrackingData(totalBalance, botsAllocationUsdValue, netPnL);
_logger.LogDebug(
"Updated agent summary from position data for user {UserId}: NetPnL={NetPnL}, TotalPnL={TotalPnL}, Fees={Fees}, Volume={Volume}, Wins={Wins}, Losses={Losses}",
this.GetPrimaryKeyLong(), netPnL, totalPnL, totalFees, totalVolume, totalWins, totalLosses); this.GetPrimaryKeyLong(), netPnL, totalPnL, totalFees, totalVolume, totalWins, totalLosses);
} }
catch (Exception ex) catch (Exception ex)
@@ -512,17 +532,18 @@ public class AgentGrain : Grain, IAgentGrain
{ {
var botGrain = GrainFactory.GetGrain<ILiveTradingBotGrain>(botEntry.Identifier); var botGrain = GrainFactory.GetGrain<ILiveTradingBotGrain>(botEntry.Identifier);
var hasOpenPositions = await botGrain.HasOpenPositionsAsync(); var hasOpenPositions = await botGrain.HasOpenPositionsAsync();
if (hasOpenPositions) if (hasOpenPositions)
{ {
_logger.LogInformation("Bot {BotId} has open positions, blocking autoswap for user {UserId}", _logger.LogInformation("Bot {BotId} has open positions, blocking autoswap for user {UserId}",
botEntry.Identifier, this.GetPrimaryKeyLong()); botEntry.Identifier, this.GetPrimaryKeyLong());
return true; return true;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Error checking open positions for bot {BotId}, skipping", botEntry.Identifier); _logger.LogWarning(ex, "Error checking open positions for bot {BotId}, skipping",
botEntry.Identifier);
// Continue checking other bots even if one fails // Continue checking other bots even if one fails
} }
} }
@@ -532,7 +553,8 @@ public class AgentGrain : Grain, IAgentGrain
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error checking for open positions across all bots for user {UserId}", this.GetPrimaryKeyLong()); _logger.LogError(ex, "Error checking for open positions across all bots for user {UserId}",
this.GetPrimaryKeyLong());
return false; // Default to false on error to avoid blocking autoswap return false; // Default to false on error to avoid blocking autoswap
} }
} }
@@ -600,4 +622,33 @@ public class AgentGrain : Grain, IAgentGrain
return null; return null;
} }
} }
/// <summary>
/// Inserts balance tracking data into the AgentBalanceRepository
/// </summary>
private void InsertBalanceTrackingData(decimal totalAccountUsdValue, decimal botsAllocationUsdValue, decimal pnl)
{
try
{
var agentBalance = new AgentBalance
{
AgentName = _state.State.AgentName,
TotalBalanceValue = totalAccountUsdValue,
BotsAllocationUsdValue = botsAllocationUsdValue,
PnL = pnl,
Time = DateTime.UtcNow
};
_agentBalanceRepository.InsertAgentBalance(agentBalance);
_logger.LogDebug(
"Inserted balance tracking data for agent {AgentName}: TotalBalanceValue={TotalBalanceValue}, BotsAllocationUsdValue={BotsAllocationUsdValue}, PnL={PnL}",
agentBalance.AgentName, agentBalance.TotalBalanceValue, agentBalance.BotsAllocationUsdValue,
agentBalance.PnL);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error inserting balance tracking data for agent {AgentName}", _state.State.AgentName);
}
}
} }

View File

@@ -208,6 +208,28 @@ namespace Managing.Application.ManageBot
return await grain.GetConfiguration(); return await grain.GetConfiguration();
} }
public async Task<IEnumerable<TradingBotConfig>> GetBotConfigsByIdsAsync(IEnumerable<Guid> botIds)
{
var configs = new List<TradingBotConfig>();
foreach (var botId in botIds)
{
try
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(botId);
var config = await grain.GetConfiguration();
configs.Add(config);
}
catch (Exception ex)
{
_tradingBotLogger.LogWarning(ex, "Failed to get configuration for bot {BotId}", botId);
// Continue with other bots even if one fails
}
}
return configs;
}
public async Task<IEnumerable<string>> GetActiveBotsNamesAsync() public async Task<IEnumerable<string>> GetActiveBotsNamesAsync()
{ {
var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Running); var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Running);

View File

@@ -1,171 +0,0 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using Managing.Domain.Statistics;
using MediatR;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Workers;
public class BalanceTrackingWorker : BaseWorker<BalanceTrackingWorker>
{
private readonly IMediator _mediator;
private readonly IAccountService _accountService;
private readonly IAgentBalanceRepository _agentBalanceRepository;
private bool _isInitialized;
public BalanceTrackingWorker(
ILogger<BalanceTrackingWorker> logger,
IServiceProvider serviceProvider,
IMediator mediator,
IAccountService accountService,
IAgentBalanceRepository agentBalanceRepository)
: base(
WorkerType.BalanceTracking,
logger,
TimeSpan.FromHours(1),
serviceProvider)
{
_mediator = mediator;
_accountService = accountService;
_agentBalanceRepository = agentBalanceRepository;
_isInitialized = false;
}
protected override async Task Run(CancellationToken cancellationToken)
{
if (!_isInitialized)
{
_logger.LogInformation("Waiting 5 minutes for bots to initialize before starting balance tracking...");
await Task.Delay(TimeSpan.FromMinutes(3), cancellationToken);
_isInitialized = true;
}
_logger.LogInformation("Starting balance tracking...");
// Get all active bots
var bots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Running));
var botCount = bots.Count();
if (botCount == 0)
{
_logger.LogWarning("No active bots found. Skipping balance tracking.");
return;
}
_logger.LogInformation($"Found {botCount} active bots. Proceeding with balance tracking.");
await TrackBalances(bots);
_logger.LogInformation("Completed balance tracking");
}
private async Task TrackBalances(IEnumerable<Bot> bots)
{
// Group bots by agent/user
var botsByAgent = bots
.Where(b => b.User != null)
.GroupBy(b => b.User.AgentName)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var agentEntry in botsByAgent)
{
try
{
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");
// Calculate total allocated balance for all bots
foreach (var bot in agentBots)
{
totalBotAllocatedBalance += bot.Volume;
_logger.LogInformation(
$"Bot {bot.Name} allocated balance: {bot.Volume} USD");
}
// Get account balances for this agent (only once per agent)
var agent = agentBots.First().User; // Get the user object from the first bot
var accountBalances = _accountService.GetAccountsBalancesByUser(agent, true);
foreach (var accountBalance in accountBalances)
{
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)
{
_logger.LogInformation(
$"Account {accountBalance.Name} USDC balance before bot allocation: {usdcBalance.Value} USD");
usdcBalance.Value -= totalBotAllocatedBalance;
_logger.LogInformation(
$"Account {accountBalance.Name} USDC balance after bot allocation: {usdcBalance.Value} USD");
}
totalAccountUsdValue += accountTotalValue;
_logger.LogInformation(
$"Account {accountBalance.Name} total value: {accountTotalValue} USD");
// Log individual token balances for debugging
foreach (var balance in accountBalance.Balances)
{
_logger.LogInformation(
$" - {balance.TokenName}: {balance.Amount} (Value: {balance.Value} USD)");
}
}
}
// Process all bots in a single iteration
foreach (var bot in agentBots)
{
totalPnL += bot.Pnl;
}
totalAgentValue = totalAccountUsdValue + botsAllocationUsdValue;
_logger.LogInformation(
$"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)
{
_logger.LogError(ex, $"Error processing agent {agentEntry.Key}");
}
}
}
}

View File

@@ -443,12 +443,6 @@ public static class ApiBootstrap
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration) private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
{ {
// Balance Workers
if (configuration.GetValue<bool>("WorkerBalancesTracking", false))
{
services.AddHostedService<BalanceTrackingWorker>();
}
if (configuration.GetValue<bool>("WorkerNotifyBundleBacktest", false)) if (configuration.GetValue<bool>("WorkerNotifyBundleBacktest", false))
{ {
services.AddHostedService<NotifyBundleBacktestWorker>(); services.AddHostedService<NotifyBundleBacktestWorker>();

View File

@@ -1,8 +1,7 @@
public class AgentBalance public class AgentBalance
{ {
public string AgentName { get; set; } public string AgentName { get; set; }
public decimal TotalValue { get; set; } public decimal TotalBalanceValue { get; set; }
public decimal TotalAccountUsdValue { 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

@@ -3,8 +3,7 @@ namespace Managing.Domain.Statistics;
public class AgentBalance public class AgentBalance
{ {
public string AgentName { get; set; } public string AgentName { get; set; }
public decimal TotalValue { get; set; } public decimal TotalBalanceValue { get; set; }
public decimal TotalAccountUsdValue { 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

@@ -5,7 +5,7 @@ using Managing.Infrastructure.Databases.InfluxDb.Abstractions;
using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.InfluxDb.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Databases; namespace Managing.Infrastructure.Databases.InfluxDb;
public class AgentBalanceRepository : IAgentBalanceRepository public class AgentBalanceRepository : IAgentBalanceRepository
{ {
@@ -26,8 +26,7 @@ public class AgentBalanceRepository : IAgentBalanceRepository
var balanceDto = new AgentBalanceDto var balanceDto = new AgentBalanceDto
{ {
AgentName = balance.AgentName, AgentName = balance.AgentName,
TotalValue = balance.TotalValue, TotalBalanceValue = balance.TotalBalanceValue,
TotalAccountUsdValue = balance.TotalAccountUsdValue,
BotsAllocationUsdValue = balance.BotsAllocationUsdValue, BotsAllocationUsdValue = balance.BotsAllocationUsdValue,
PnL = balance.PnL, PnL = balance.PnL,
Time = balance.Time Time = balance.Time
@@ -57,8 +56,7 @@ public class AgentBalanceRepository : IAgentBalanceRepository
return result.Select(balance => new AgentBalance return result.Select(balance => new AgentBalance
{ {
AgentName = balance.AgentName, AgentName = balance.AgentName,
TotalValue = balance.TotalValue, TotalBalanceValue = balance.TotalBalanceValue,
TotalAccountUsdValue = balance.TotalAccountUsdValue,
BotsAllocationUsdValue = balance.BotsAllocationUsdValue, BotsAllocationUsdValue = balance.BotsAllocationUsdValue,
PnL = balance.PnL, PnL = balance.PnL,
Time = balance.Time Time = balance.Time
@@ -89,11 +87,10 @@ public class AgentBalanceRepository : IAgentBalanceRepository
.Select(g => new AgentBalanceHistory .Select(g => new AgentBalanceHistory
{ {
AgentName = g.Key, AgentName = g.Key,
AgentBalances = g.Select(b => new AgentBalance AgentBalances = g.Select(b => new AgentBalance
{ {
AgentName = b.AgentName, AgentName = b.AgentName,
TotalValue = b.TotalValue, TotalBalanceValue = b.TotalBalanceValue,
TotalAccountUsdValue = b.TotalAccountUsdValue,
BotsAllocationUsdValue = b.BotsAllocationUsdValue, BotsAllocationUsdValue = b.BotsAllocationUsdValue,
PnL = b.PnL, PnL = b.PnL,
Time = b.Time Time = b.Time

View File

@@ -7,9 +7,7 @@ public class AgentBalanceDto
{ {
[Column("agent_name", IsTag = true)] public string AgentName { get; set; } [Column("agent_name", IsTag = true)] public string AgentName { get; set; }
[Column("total_value")] public decimal TotalValue { get; set; } [Column("total_balance_value")] public decimal TotalBalanceValue { get; set; }
[Column("total_account_usd_value")] public decimal TotalAccountUsdValue { get; set; }
[Column("bots_allocation_usd_value")] public decimal BotsAllocationUsdValue { get; set; } [Column("bots_allocation_usd_value")] public decimal BotsAllocationUsdValue { get; set; }