feat: 完成 P0-1 实时价格/盈亏计算和 P0-2 汇率服务接口预留
This commit is contained in:
parent
1ec23bef3d
commit
4816980d62
@ -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");
|
||||
|
||||
|
||||
@ -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>();
|
||||
|
||||
|
||||
24
AssetManager.Infrastructure/Services/IExchangeRateService.cs
Normal file
24
AssetManager.Infrastructure/Services/IExchangeRateService.cs
Normal 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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
// 验证投资组合是否属于该用户
|
||||
|
||||
Loading…
Reference in New Issue
Block a user