feat: 完成 P1 任务 - 今日盈亏、风险平价补全、Mock 数据 PreviousClose

This commit is contained in:
虾球 2026-03-06 06:00:38 +00:00
parent 4816980d62
commit 567504119c
4 changed files with 153 additions and 32 deletions

View File

@ -15,33 +15,43 @@ public class MockMarketDataService : IMarketDataService
_logger = logger; _logger = logger;
} }
public Task<MarketPriceResponse> GetStockPriceAsync(string symbol) public async Task<MarketPriceResponse> GetStockPriceAsync(string symbol)
{ {
_logger.LogInformation("Mock 获取股票价格: {Symbol}", 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 价格:基于标的代码生成一个稳定的价格 // Mock 价格:基于标的代码生成一个稳定的价格
decimal basePrice = symbol.GetHashCode() % 1000 + 50; decimal basePrice = symbol.GetHashCode() % 1000 + 50;
return Task.FromResult(new MarketPriceResponse return new MarketPriceResponse
{ {
Symbol = symbol, Symbol = symbol,
Price = basePrice, Price = basePrice,
PreviousClose = previousClose,
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
AssetType = "Stock" AssetType = "Stock"
}); };
} }
public Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol) public async Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol)
{ {
_logger.LogInformation("Mock 获取加密货币价格: {Symbol}", 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; decimal basePrice = symbol.GetHashCode() % 50000 + 10000;
return Task.FromResult(new MarketPriceResponse return new MarketPriceResponse
{ {
Symbol = symbol, Symbol = symbol,
Price = basePrice, Price = basePrice,
PreviousClose = previousClose,
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
AssetType = "Crypto" AssetType = "Crypto"
}); };
} }
public Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) public Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit)

View File

@ -86,8 +86,8 @@ public class RiskParityCalculator : IStrategyCalculator
targetWeights[kvp.Key] = kvp.Value; targetWeights[kvp.Key] = kvp.Value;
} }
// 计算当前权重 // 计算当前权重(使用实时市值)
var currentWeights = CalculateCurrentWeights(positions, targetAssets); var currentWeights = await CalculateCurrentWeightsAsync(positions, targetAssets, cancellationToken);
// 根据偏差决定是否再平衡 // 根据偏差决定是否再平衡
var maxDeviation = 0m; var maxDeviation = 0m;
@ -203,21 +203,43 @@ public class RiskParityCalculator : IStrategyCalculator
} }
/// <summary> /// <summary>
/// 计算当前持仓权重 /// 计算当前持仓权重(使用实时市值)
/// </summary> /// </summary>
private Dictionary<string, decimal> CalculateCurrentWeights(List<Position> positions, List<string> targetAssets) private async Task<Dictionary<string, decimal>> CalculateCurrentWeightsAsync(
List<Position> positions,
List<string> targetAssets,
CancellationToken cancellationToken)
{ {
var weights = new Dictionary<string, decimal>(); var weights = new Dictionary<string, decimal>();
var marketValues = new Dictionary<string, decimal>();
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) foreach (var symbol in targetAssets)
{ {
var position = positions.FirstOrDefault(p => p.StockCode == symbol); if (marketValues.TryGetValue(symbol, out var marketValue) && totalValue > 0)
if (position != null && totalValue > 0)
{ {
weights[symbol] = (position.Shares * position.AvgPrice) / totalValue; weights[symbol] = marketValue / totalValue;
} }
else else
{ {

View File

@ -15,6 +15,11 @@ public class MarketPriceResponse
/// </summary> /// </summary>
public decimal Price { get; set; } public decimal Price { get; set; }
/// <summary>
/// 上一交易日收盘价
/// </summary>
public decimal PreviousClose { get; set; }
/// <summary> /// <summary>
/// 时间戳 /// 时间戳
/// </summary> /// </summary>

View File

@ -9,11 +9,13 @@ public class PortfolioService : IPortfolioService
{ {
private readonly ISqlSugarClient _db; private readonly ISqlSugarClient _db;
private readonly IMarketDataService _marketDataService; private readonly IMarketDataService _marketDataService;
private readonly IExchangeRateService _exchangeRateService;
public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService) public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService, IExchangeRateService exchangeRateService)
{ {
_db = db; _db = db;
_marketDataService = marketDataService; _marketDataService = marketDataService;
_exchangeRateService = exchangeRateService;
} }
public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId) public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId)
@ -118,23 +120,83 @@ public class PortfolioService : IPortfolioService
}).ToList(); }).ToList();
} }
public TotalAssetsResponse GetTotalAssets(string userId) public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
{ {
var totalValue = _db.Queryable<Portfolio>() // 获取用户信息(包含默认本位币)
.Where(p => p.UserId == userId) var user = _db.Queryable<User>()
.Sum(p => p.TotalValue); .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<Portfolio>()
.Where(p => p.UserId == userId)
.ToList();
foreach (var portfolio in portfolios)
{
// 获取该组合的所有持仓
var positions = _db.Queryable<Position>()
.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 return new TotalAssetsResponse
{ {
totalValue = (double)totalValue, totalValue = (double)totalValueInTargetCurrency,
currency = "CNY", // 假设统一转换为CNY currency = targetCurrency,
todayProfit = 0, todayProfit = (double)totalTodayProfitInTargetCurrency,
todayProfitCurrency = "CNY", todayProfitCurrency = targetCurrency,
totalReturnRate = 0 totalReturnRate = totalReturnRate
}; };
} }
// 保留同步方法作为兼容
public TotalAssetsResponse GetTotalAssets(string userId)
{
return GetTotalAssetsAsync(userId).GetAwaiter().GetResult();
}
public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId) public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
{ {
var portfolio = _db.Queryable<Portfolio>() var portfolio = _db.Queryable<Portfolio>()
@ -153,6 +215,7 @@ public class PortfolioService : IPortfolioService
// 获取每个持仓的实时价格并计算 // 获取每个持仓的实时价格并计算
decimal totalPortfolioValue = 0; decimal totalPortfolioValue = 0;
decimal totalCost = 0; decimal totalCost = 0;
decimal totalTodayProfit = 0;
var positionItems = new List<PositionItem>(); var positionItems = new List<PositionItem>();
foreach (var pos in positions) foreach (var pos in positions)
@ -169,13 +232,16 @@ public class PortfolioService : IPortfolioService
} }
decimal currentPrice = priceResponse.Price; decimal currentPrice = priceResponse.Price;
decimal previousClose = priceResponse.PreviousClose;
decimal positionValue = pos.Shares * currentPrice; decimal positionValue = pos.Shares * currentPrice;
decimal cost = pos.Shares * pos.AvgPrice; decimal cost = pos.Shares * pos.AvgPrice;
decimal profit = positionValue - cost; decimal profit = positionValue - cost;
double profitRate = cost > 0 ? (double)(profit / cost * 100) : 0; double profitRate = cost > 0 ? (double)(profit / cost * 100) : 0;
decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
totalPortfolioValue += positionValue; totalPortfolioValue += positionValue;
totalCost += cost; totalCost += cost;
totalTodayProfit += todayProfit;
positionItems.Add(new PositionItem positionItems.Add(new PositionItem
{ {
@ -189,7 +255,7 @@ public class PortfolioService : IPortfolioService
totalValue = (double)positionValue, totalValue = (double)positionValue,
profit = (double)profit, profit = (double)profit,
profitRate = profitRate, profitRate = profitRate,
changeAmount = 0, // 今日盈亏后续实现 changeAmount = (double)todayProfit,
ratio = 0, // 后面统一计算比例 ratio = 0, // 后面统一计算比例
deviationRatio = 0, // 后续实现 deviationRatio = 0, // 后续实现
currency = pos.Currency currency = pos.Currency
@ -219,7 +285,7 @@ public class PortfolioService : IPortfolioService
}, },
portfolioValue = (double)totalPortfolioValue, portfolioValue = (double)totalPortfolioValue,
totalReturn = (double)totalReturn, totalReturn = (double)totalReturn,
todayProfit = 0, // 后续 P1-1 实现 todayProfit = (double)totalTodayProfit,
historicalChange = totalReturnRate, historicalChange = totalReturnRate,
dailyVolatility = 0, // 后续实现 dailyVolatility = 0, // 后续实现
todayProfitCurrency = portfolio.Currency, todayProfitCurrency = portfolio.Currency,
@ -371,12 +437,30 @@ public class PortfolioService : IPortfolioService
_db.Insertable(position).ExecuteCommand(); _db.Insertable(position).ExecuteCommand();
} }
// 更新投资组合总价值 // 更新投资组合总价值(使用实时市值而不是成本价)
var totalValue = _db.Queryable<Position>() var positions = _db.Queryable<Position>()
.Where(pos => pos.PortfolioId == request.portfolioId) .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; portfolio.UpdatedAt = DateTime.Now;
_db.Updateable(portfolio).ExecuteCommand(); _db.Updateable(portfolio).ExecuteCommand();