diff --git a/AssetManager.Infrastructure/Services/MockMarketDataService.cs b/AssetManager.Infrastructure/Services/MockMarketDataService.cs index 0c78b53..edbaea6 100644 --- a/AssetManager.Infrastructure/Services/MockMarketDataService.cs +++ b/AssetManager.Infrastructure/Services/MockMarketDataService.cs @@ -15,33 +15,43 @@ public class MockMarketDataService : IMarketDataService _logger = logger; } - public Task GetStockPriceAsync(string symbol) + public async Task GetStockPriceAsync(string symbol) { _logger.LogInformation("Mock 获取股票价格: {Symbol}", symbol); + // 先拿历史数据,取最后一根 K 线收盘价作为 PreviousClose + var historicalData = await GetStockHistoricalDataAsync(symbol, "1d", 2); + decimal previousClose = historicalData.Count >= 1 ? historicalData[^1].Close : 0; + // Mock 价格:基于标的代码生成一个稳定的价格 decimal basePrice = symbol.GetHashCode() % 1000 + 50; - return Task.FromResult(new MarketPriceResponse + return new MarketPriceResponse { Symbol = symbol, Price = basePrice, + PreviousClose = previousClose, Timestamp = DateTime.UtcNow, AssetType = "Stock" - }); + }; } - public Task GetCryptoPriceAsync(string symbol) + public async Task GetCryptoPriceAsync(string symbol) { _logger.LogInformation("Mock 获取加密货币价格: {Symbol}", symbol); + // 先拿历史数据,取最后一根 K 线收盘价作为 PreviousClose + var historicalData = await GetCryptoHistoricalDataAsync(symbol, "1d", 2); + decimal previousClose = historicalData.Count >= 1 ? historicalData[^1].Close : 0; + decimal basePrice = symbol.GetHashCode() % 50000 + 10000; - return Task.FromResult(new MarketPriceResponse + return new MarketPriceResponse { Symbol = symbol, Price = basePrice, + PreviousClose = previousClose, Timestamp = DateTime.UtcNow, AssetType = "Crypto" - }); + }; } public Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) diff --git a/AssetManager.Infrastructure/StrategyEngine/Calculators/RiskParityCalculator.cs b/AssetManager.Infrastructure/StrategyEngine/Calculators/RiskParityCalculator.cs index 304aea9..c11ef8f 100644 --- a/AssetManager.Infrastructure/StrategyEngine/Calculators/RiskParityCalculator.cs +++ b/AssetManager.Infrastructure/StrategyEngine/Calculators/RiskParityCalculator.cs @@ -86,8 +86,8 @@ public class RiskParityCalculator : IStrategyCalculator targetWeights[kvp.Key] = kvp.Value; } - // 计算当前权重 - var currentWeights = CalculateCurrentWeights(positions, targetAssets); + // 计算当前权重(使用实时市值) + var currentWeights = await CalculateCurrentWeightsAsync(positions, targetAssets, cancellationToken); // 根据偏差决定是否再平衡 var maxDeviation = 0m; @@ -203,21 +203,43 @@ public class RiskParityCalculator : IStrategyCalculator } /// - /// 计算当前持仓权重 + /// 计算当前持仓权重(使用实时市值) /// - private Dictionary CalculateCurrentWeights(List positions, List targetAssets) + private async Task> CalculateCurrentWeightsAsync( + List positions, + List targetAssets, + CancellationToken cancellationToken) { var weights = new Dictionary(); + var marketValues = new Dictionary(); + decimal totalValue = 0; - // 计算总价值 - var totalValue = positions.Sum(p => p.Shares * p.AvgPrice); + // 获取每个持仓的实时价格并计算市值 + foreach (var position in positions) + { + cancellationToken.ThrowIfCancellationRequested(); + MarketPriceResponse priceResponse; + if (position.AssetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) + { + priceResponse = await _marketDataService.GetCryptoPriceAsync(position.StockCode); + } + else + { + priceResponse = await _marketDataService.GetStockPriceAsync(position.StockCode); + } + + decimal marketValue = position.Shares * priceResponse.Price; + marketValues[position.StockCode] = marketValue; + totalValue += marketValue; + } + + // 计算权重 foreach (var symbol in targetAssets) { - var position = positions.FirstOrDefault(p => p.StockCode == symbol); - if (position != null && totalValue > 0) + if (marketValues.TryGetValue(symbol, out var marketValue) && totalValue > 0) { - weights[symbol] = (position.Shares * position.AvgPrice) / totalValue; + weights[symbol] = marketValue / totalValue; } else { diff --git a/AssetManager.Models/DTOs/MarketDTO.cs b/AssetManager.Models/DTOs/MarketDTO.cs index 413fd32..0aed729 100644 --- a/AssetManager.Models/DTOs/MarketDTO.cs +++ b/AssetManager.Models/DTOs/MarketDTO.cs @@ -15,6 +15,11 @@ public class MarketPriceResponse /// public decimal Price { get; set; } + /// + /// 上一交易日收盘价 + /// + public decimal PreviousClose { get; set; } + /// /// 时间戳 /// diff --git a/AssetManager.Services/PortfolioService.cs b/AssetManager.Services/PortfolioService.cs index a9d7288..0a18601 100644 --- a/AssetManager.Services/PortfolioService.cs +++ b/AssetManager.Services/PortfolioService.cs @@ -9,11 +9,13 @@ public class PortfolioService : IPortfolioService { private readonly ISqlSugarClient _db; private readonly IMarketDataService _marketDataService; + private readonly IExchangeRateService _exchangeRateService; - public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService) + public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService, IExchangeRateService exchangeRateService) { _db = db; _marketDataService = marketDataService; + _exchangeRateService = exchangeRateService; } public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId) @@ -118,23 +120,83 @@ public class PortfolioService : IPortfolioService }).ToList(); } - public TotalAssetsResponse GetTotalAssets(string userId) + public async Task GetTotalAssetsAsync(string userId) { - var totalValue = _db.Queryable() - .Where(p => p.UserId == userId) - .Sum(p => p.TotalValue); + // 获取用户信息(包含默认本位币) + var user = _db.Queryable() + .Where(u => u.Id == userId) + .First(); + + if (user == null) + { + throw new Exception("User not found"); + } + + string targetCurrency = user.DefaultCurrency; + decimal totalValueInTargetCurrency = 0; + decimal totalCostInTargetCurrency = 0; + decimal totalTodayProfitInTargetCurrency = 0; + + // 获取用户所有投资组合 + var portfolios = _db.Queryable() + .Where(p => p.UserId == userId) + .ToList(); + + foreach (var portfolio in portfolios) + { + // 获取该组合的所有持仓 + var positions = _db.Queryable() + .Where(pos => pos.PortfolioId == portfolio.Id) + .ToList(); + + foreach (var pos in positions) + { + // 获取实时价格 + MarketPriceResponse priceResponse; + if (pos.AssetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) + { + priceResponse = await _marketDataService.GetCryptoPriceAsync(pos.StockCode); + } + else + { + priceResponse = await _marketDataService.GetStockPriceAsync(pos.StockCode); + } + + decimal currentPrice = priceResponse.Price; + decimal previousClose = priceResponse.PreviousClose; + decimal currentPositionValue = pos.Shares * currentPrice; + decimal costPositionValue = pos.Shares * pos.AvgPrice; + decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0; + + // 换算到目标本位币 + decimal currentInTarget = await _exchangeRateService.ConvertAmountAsync(currentPositionValue, pos.Currency, targetCurrency); + decimal costInTarget = await _exchangeRateService.ConvertAmountAsync(costPositionValue, pos.Currency, targetCurrency); + decimal todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(todayProfit, pos.Currency, targetCurrency); + + totalValueInTargetCurrency += currentInTarget; + totalCostInTargetCurrency += costInTarget; + totalTodayProfitInTargetCurrency += todayProfitInTarget; + } + } + + double totalReturnRate = totalCostInTargetCurrency > 0 ? (double)((totalValueInTargetCurrency - totalCostInTargetCurrency) / totalCostInTargetCurrency * 100) : 0; - // 这里简化处理,实际应该计算今日盈亏和总收益率 return new TotalAssetsResponse { - totalValue = (double)totalValue, - currency = "CNY", // 假设统一转换为CNY - todayProfit = 0, - todayProfitCurrency = "CNY", - totalReturnRate = 0 + totalValue = (double)totalValueInTargetCurrency, + currency = targetCurrency, + todayProfit = (double)totalTodayProfitInTargetCurrency, + todayProfitCurrency = targetCurrency, + totalReturnRate = totalReturnRate }; } + // 保留同步方法作为兼容 + public TotalAssetsResponse GetTotalAssets(string userId) + { + return GetTotalAssetsAsync(userId).GetAwaiter().GetResult(); + } + public async Task GetPortfolioByIdAsync(string id, string userId) { var portfolio = _db.Queryable() @@ -153,6 +215,7 @@ public class PortfolioService : IPortfolioService // 获取每个持仓的实时价格并计算 decimal totalPortfolioValue = 0; decimal totalCost = 0; + decimal totalTodayProfit = 0; var positionItems = new List(); foreach (var pos in positions) @@ -169,13 +232,16 @@ public class PortfolioService : IPortfolioService } decimal currentPrice = priceResponse.Price; + decimal previousClose = priceResponse.PreviousClose; decimal positionValue = pos.Shares * currentPrice; decimal cost = pos.Shares * pos.AvgPrice; decimal profit = positionValue - cost; double profitRate = cost > 0 ? (double)(profit / cost * 100) : 0; + decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0; totalPortfolioValue += positionValue; totalCost += cost; + totalTodayProfit += todayProfit; positionItems.Add(new PositionItem { @@ -189,7 +255,7 @@ public class PortfolioService : IPortfolioService totalValue = (double)positionValue, profit = (double)profit, profitRate = profitRate, - changeAmount = 0, // 今日盈亏后续实现 + changeAmount = (double)todayProfit, ratio = 0, // 后面统一计算比例 deviationRatio = 0, // 后续实现 currency = pos.Currency @@ -219,7 +285,7 @@ public class PortfolioService : IPortfolioService }, portfolioValue = (double)totalPortfolioValue, totalReturn = (double)totalReturn, - todayProfit = 0, // 后续 P1-1 实现 + todayProfit = (double)totalTodayProfit, historicalChange = totalReturnRate, dailyVolatility = 0, // 后续实现 todayProfitCurrency = portfolio.Currency, @@ -371,12 +437,30 @@ public class PortfolioService : IPortfolioService _db.Insertable(position).ExecuteCommand(); } - // 更新投资组合总价值 - var totalValue = _db.Queryable() + // 更新投资组合总价值(使用实时市值而不是成本价) + var positions = _db.Queryable() .Where(pos => pos.PortfolioId == request.portfolioId) - .Sum(pos => pos.Shares * pos.AvgPrice); + .ToList(); - portfolio.TotalValue = totalValue; + decimal totalPortfolioValue = 0; + foreach (var pos in positions) + { + // 获取实时价格 + MarketPriceResponse priceResponse; + if (pos.AssetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) + { + priceResponse = _marketDataService.GetCryptoPriceAsync(pos.StockCode).GetAwaiter().GetResult(); + } + else + { + priceResponse = _marketDataService.GetStockPriceAsync(pos.StockCode).GetAwaiter().GetResult(); + } + + decimal currentPrice = priceResponse.Price; + totalPortfolioValue += pos.Shares * currentPrice; + } + + portfolio.TotalValue = totalPortfolioValue; portfolio.UpdatedAt = DateTime.Now; _db.Updateable(portfolio).ExecuteCommand();