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)
{
// 验证投资组合是否属于该用户