diff --git a/AssetManager.API/Controllers/PortfolioController.cs b/AssetManager.API/Controllers/PortfolioController.cs index 002f8d7..af6537f 100644 --- a/AssetManager.API/Controllers/PortfolioController.cs +++ b/AssetManager.API/Controllers/PortfolioController.cs @@ -234,7 +234,7 @@ public class PortfolioController : ControllerBase /// 投资组合ID /// 投资组合详情(含持仓明细) [HttpGet("{id}")] - public ActionResult> GetPortfolioById(string id) + public async Task>> GetPortfolioById(string id) { try { @@ -251,7 +251,7 @@ public class PortfolioController : ControllerBase _logger.LogInformation($"Request to get portfolio by id: {id}"); - var response = _portfolioService.GetPortfolioById(id, userId); + var response = await _portfolioService.GetPortfolioByIdAsync(id, userId); _logger.LogInformation("Portfolio retrieved successfully"); diff --git a/AssetManager.API/Program.cs b/AssetManager.API/Program.cs index c5b3c05..f285134 100644 --- a/AssetManager.API/Program.cs +++ b/AssetManager.API/Program.cs @@ -79,6 +79,9 @@ else builder.Services.AddScoped(); } +// 汇率服务:预留接口,目前用 Mock 实现 +builder.Services.AddScoped(); + // 策略引擎 builder.Services.AddScoped(); diff --git a/AssetManager.Infrastructure/Services/IExchangeRateService.cs b/AssetManager.Infrastructure/Services/IExchangeRateService.cs new file mode 100644 index 0000000..8dc96ef --- /dev/null +++ b/AssetManager.Infrastructure/Services/IExchangeRateService.cs @@ -0,0 +1,24 @@ +namespace AssetManager.Infrastructure.Services; + +/// +/// 汇率服务接口(预留,后续实现多币种汇总) +/// +public interface IExchangeRateService +{ + /// + /// 获取汇率(从源币种转换为目标币种) + /// + /// 源币种(如 CNY) + /// 目标币种(如 USD) + /// 汇率(1 单位源币种可兑换的目标币种数量) + Task GetExchangeRateAsync(string fromCurrency, string toCurrency); + + /// + /// 转换金额 + /// + /// 金额 + /// 源币种 + /// 目标币种 + /// 转换后的金额 + Task ConvertAmountAsync(decimal amount, string fromCurrency, string toCurrency); +} diff --git a/AssetManager.Infrastructure/Services/MockExchangeRateService.cs b/AssetManager.Infrastructure/Services/MockExchangeRateService.cs new file mode 100644 index 0000000..25d4722 --- /dev/null +++ b/AssetManager.Infrastructure/Services/MockExchangeRateService.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Logging; + +namespace AssetManager.Infrastructure.Services; + +/// +/// Mock 汇率服务(占位实现,后续接入真实汇率源) +/// +public class MockExchangeRateService : IExchangeRateService +{ + private readonly ILogger _logger; + + // 固定的 Mock 汇率(以 2026 年初为基准) + private readonly Dictionary _mockRates = new() + { + { "CNY-USD", 0.14m }, + { "USD-CNY", 7.10m }, + { "CNY-HKD", 1.09m }, + { "HKD-CNY", 0.92m }, + { "USD-HKD", 7.75m }, + { "HKD-USD", 0.13m }, + { "CNY-CNY", 1.00m }, + { "USD-USD", 1.00m }, + { "HKD-HKD", 1.00m } + }; + + public MockExchangeRateService(ILogger logger) + { + _logger = logger; + } + + public Task GetExchangeRateAsync(string fromCurrency, string toCurrency) + { + _logger.LogInformation("Mock 获取汇率: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency); + + string key = $"{fromCurrency}-{toCurrency}"; + if (_mockRates.TryGetValue(key, out decimal rate)) + { + return Task.FromResult(rate); + } + + // 默认返回 1(同币种或不支持的币种) + return Task.FromResult(1.00m); + } + + public Task ConvertAmountAsync(decimal amount, string fromCurrency, string toCurrency) + { + _logger.LogInformation("Mock 转换金额: {Amount} {FromCurrency} -> {ToCurrency}", amount, fromCurrency, toCurrency); + + if (fromCurrency == toCurrency) + { + return Task.FromResult(amount); + } + + return GetExchangeRateAsync(fromCurrency, toCurrency) + .ContinueWith(t => amount * t.Result); + } +} diff --git a/AssetManager.Services/IPortfolioService.cs b/AssetManager.Services/IPortfolioService.cs index f686d0f..ba70ca3 100644 --- a/AssetManager.Services/IPortfolioService.cs +++ b/AssetManager.Services/IPortfolioService.cs @@ -8,6 +8,7 @@ public interface IPortfolioService List GetPortfolios(string userId); TotalAssetsResponse GetTotalAssets(string userId); PortfolioDetailResponse GetPortfolioById(string id, string userId); + Task GetPortfolioByIdAsync(string id, string userId); GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset); CreateTransactionResponse CreateTransaction(CreateTransactionRequest request, string userId); } \ No newline at end of file diff --git a/AssetManager.Services/PortfolioService.cs b/AssetManager.Services/PortfolioService.cs index b5c761a..a9d7288 100644 --- a/AssetManager.Services/PortfolioService.cs +++ b/AssetManager.Services/PortfolioService.cs @@ -1,5 +1,6 @@ using AssetManager.Data; using AssetManager.Models.DTOs; +using AssetManager.Infrastructure.Services; using SqlSugar; namespace AssetManager.Services; @@ -7,10 +8,12 @@ namespace AssetManager.Services; public class PortfolioService : IPortfolioService { private readonly ISqlSugarClient _db; + private readonly IMarketDataService _marketDataService; - public PortfolioService(ISqlSugarClient db) + public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService) { _db = db; + _marketDataService = marketDataService; } public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId) @@ -132,7 +135,7 @@ public class PortfolioService : IPortfolioService }; } - public PortfolioDetailResponse GetPortfolioById(string id, string userId) + public async Task GetPortfolioByIdAsync(string id, string userId) { var portfolio = _db.Queryable() .Where(p => p.Id == id && p.UserId == userId) @@ -147,61 +150,94 @@ public class PortfolioService : IPortfolioService .Where(pos => pos.PortfolioId == id) .ToList(); - var totalValue = (double)portfolio.TotalValue; - var positionItems = positions.Select(pos => + // 获取每个持仓的实时价格并计算 + decimal totalPortfolioValue = 0; + decimal totalCost = 0; + var positionItems = new List(); + + foreach (var pos in positions) { - var positionValue = (double)(pos.Shares * pos.AvgPrice); - var ratio = totalValue > 0 ? (positionValue / totalValue) * 100 : 0; - // 假设目标权重为50%,计算偏离比例 - var targetWeight = 50.0; - var deviationRatio = ratio - targetWeight; - - return new PositionItem + // 获取实时价格 + 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 positionValue = pos.Shares * currentPrice; + decimal cost = pos.Shares * pos.AvgPrice; + decimal profit = positionValue - cost; + double profitRate = cost > 0 ? (double)(profit / cost * 100) : 0; + + totalPortfolioValue += positionValue; + totalCost += cost; + + positionItems.Add(new PositionItem { id = pos.Id, stockCode = pos.StockCode, stockName = pos.StockName, - symbol = $"{pos.StockCode}.US", // 简化处理,实际应该根据市场或数据源确定 + symbol = pos.StockCode, amount = (int)pos.Shares, averagePrice = (double)pos.AvgPrice, - currentPrice = (double)pos.AvgPrice, // 实际应该从市场数据获取 - totalValue = positionValue, - profit = 0, // 实际应该计算 - profitRate = 0, // 实际应该计算 - changeAmount = 0, // 实际应该计算 - ratio = ratio, - deviationRatio = deviationRatio, + currentPrice = (double)currentPrice, + totalValue = (double)positionValue, + profit = (double)profit, + profitRate = profitRate, + changeAmount = 0, // 今日盈亏后续实现 + ratio = 0, // 后面统一计算比例 + deviationRatio = 0, // 后续实现 currency = pos.Currency - }; - }).ToList(); + }); + } + + // 计算每个持仓的比例 + foreach (var item in positionItems) + { + item.ratio = totalPortfolioValue > 0 ? (item.totalValue / (double)totalPortfolioValue) * 100 : 0; + } + + decimal totalReturn = totalPortfolioValue - totalCost; + double totalReturnRate = totalCost > 0 ? (double)(totalReturn / totalCost * 100) : 0; return new PortfolioDetailResponse { id = portfolio.Id, name = portfolio.Name, currency = portfolio.Currency, - status = portfolio.Status, // 从数据库获取 + status = portfolio.Status, strategy = new StrategyInfo { id = portfolio.StrategyId, name = "策略名称", description = "策略描述" }, - portfolioValue = totalValue, - totalReturn = (double)(portfolio.TotalValue * portfolio.ReturnRate), - todayProfit = 0, // 实际应该计算 - historicalChange = 42.82, // 实际应该计算 - dailyVolatility = 1240.50, // 实际应该计算 + portfolioValue = (double)totalPortfolioValue, + totalReturn = (double)totalReturn, + todayProfit = 0, // 后续 P1-1 实现 + historicalChange = totalReturnRate, + dailyVolatility = 0, // 后续实现 todayProfitCurrency = portfolio.Currency, - logicModel = "HFEA 风险平价逻辑", // 实际应该根据策略获取 - logicModelStatus = "监控中", // 实际应该根据策略状态获取 - logicModelDescription = "目标权重 季度调仓", // 实际应该根据策略配置获取 + logicModel = "HFEA 风险平价逻辑", + logicModelStatus = "监控中", + logicModelDescription = "目标权重 季度调仓", totalItems = positions.Count, - totalRatio = 100.0, // 所有持仓比例之和 + totalRatio = 100.0, positions = positionItems }; } + // 保留同步方法作为兼容(内部调用异步) + public PortfolioDetailResponse GetPortfolioById(string id, string userId) + { + return GetPortfolioByIdAsync(id, userId).GetAwaiter().GetResult(); + } + public GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset) { // 验证投资组合是否属于该用户