feat: 完成 P0-1 实时价格/盈亏计算和 P0-2 汇率服务接口预留

This commit is contained in:
fanfpy 2026-03-05 11:28:49 +00:00
parent 1ec23bef3d
commit 4816980d62
6 changed files with 154 additions and 33 deletions

View File

@ -234,7 +234,7 @@ public class PortfolioController : ControllerBase
/// <param name="id">投资组合ID</param>
/// <returns>投资组合详情(含持仓明细)</returns>
[HttpGet("{id}")]
public ActionResult<ApiResponse<PortfolioDetailResponse>> GetPortfolioById(string id)
public async Task<ActionResult<ApiResponse<PortfolioDetailResponse>>> 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");

View File

@ -79,6 +79,9 @@ else
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IMarketDataService, AssetManager.Infrastructure.Services.MarketDataService>();
}
// 汇率服务:预留接口,目前用 Mock 实现
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IExchangeRateService, AssetManager.Infrastructure.Services.MockExchangeRateService>();
// 策略引擎
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyEngine, AssetManager.Infrastructure.StrategyEngine.StrategyEngine>();

View File

@ -0,0 +1,24 @@
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// 汇率服务接口(预留,后续实现多币种汇总)
/// </summary>
public interface IExchangeRateService
{
/// <summary>
/// 获取汇率(从源币种转换为目标币种)
/// </summary>
/// <param name="fromCurrency">源币种(如 CNY</param>
/// <param name="toCurrency">目标币种(如 USD</param>
/// <returns>汇率1 单位源币种可兑换的目标币种数量)</returns>
Task<decimal> GetExchangeRateAsync(string fromCurrency, string toCurrency);
/// <summary>
/// 转换金额
/// </summary>
/// <param name="amount">金额</param>
/// <param name="fromCurrency">源币种</param>
/// <param name="toCurrency">目标币种</param>
/// <returns>转换后的金额</returns>
Task<decimal> ConvertAmountAsync(decimal amount, string fromCurrency, string toCurrency);
}

View File

@ -0,0 +1,57 @@
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// Mock 汇率服务(占位实现,后续接入真实汇率源)
/// </summary>
public class MockExchangeRateService : IExchangeRateService
{
private readonly ILogger<MockExchangeRateService> _logger;
// 固定的 Mock 汇率(以 2026 年初为基准)
private readonly Dictionary<string, decimal> _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<MockExchangeRateService> logger)
{
_logger = logger;
}
public Task<decimal> 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<decimal> 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);
}
}

View File

@ -8,6 +8,7 @@ public interface IPortfolioService
List<PortfolioListItem> GetPortfolios(string userId);
TotalAssetsResponse GetTotalAssets(string userId);
PortfolioDetailResponse GetPortfolioById(string id, string userId);
Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId);
GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset);
CreateTransactionResponse CreateTransaction(CreateTransactionRequest request, string userId);
}

View File

@ -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<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
{
var portfolio = _db.Queryable<Portfolio>()
.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<PositionItem>();
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)
{
// 验证投资组合是否属于该用户