feat: 完成 P1 任务 - 今日盈亏、风险平价补全、Mock 数据 PreviousClose
This commit is contained in:
parent
4816980d62
commit
567504119c
@ -15,33 +15,43 @@ public class MockMarketDataService : IMarketDataService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<MarketPriceResponse> GetStockPriceAsync(string symbol)
|
||||
public async Task<MarketPriceResponse> GetStockPriceAsync(string 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 价格:基于标的代码生成一个稳定的价格
|
||||
decimal basePrice = symbol.GetHashCode() % 1000 + 50;
|
||||
return Task.FromResult(new MarketPriceResponse
|
||||
return new MarketPriceResponse
|
||||
{
|
||||
Symbol = symbol,
|
||||
Price = basePrice,
|
||||
PreviousClose = previousClose,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
AssetType = "Stock"
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol)
|
||||
public async Task<MarketPriceResponse> GetCryptoPriceAsync(string 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;
|
||||
return Task.FromResult(new MarketPriceResponse
|
||||
return new MarketPriceResponse
|
||||
{
|
||||
Symbol = symbol,
|
||||
Price = basePrice,
|
||||
PreviousClose = previousClose,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
AssetType = "Crypto"
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit)
|
||||
|
||||
@ -86,8 +86,8 @@ public class RiskParityCalculator : IStrategyCalculator
|
||||
targetWeights[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
// 计算当前权重
|
||||
var currentWeights = CalculateCurrentWeights(positions, targetAssets);
|
||||
// 计算当前权重(使用实时市值)
|
||||
var currentWeights = await CalculateCurrentWeightsAsync(positions, targetAssets, cancellationToken);
|
||||
|
||||
// 根据偏差决定是否再平衡
|
||||
var maxDeviation = 0m;
|
||||
@ -203,21 +203,43 @@ public class RiskParityCalculator : IStrategyCalculator
|
||||
}
|
||||
|
||||
/// <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 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)
|
||||
{
|
||||
var position = positions.FirstOrDefault(p => p.StockCode == symbol);
|
||||
if (position != null && totalValue > 0)
|
||||
if (marketValues.TryGetValue(symbol, out var marketValue) && totalValue > 0)
|
||||
{
|
||||
weights[symbol] = (position.Shares * position.AvgPrice) / totalValue;
|
||||
weights[symbol] = marketValue / totalValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -15,6 +15,11 @@ public class MarketPriceResponse
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上一交易日收盘价
|
||||
/// </summary>
|
||||
public decimal PreviousClose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时间戳
|
||||
/// </summary>
|
||||
|
||||
@ -9,11 +9,13 @@ public class PortfolioService : IPortfolioService
|
||||
{
|
||||
private readonly ISqlSugarClient _db;
|
||||
private readonly IMarketDataService _marketDataService;
|
||||
private readonly IExchangeRateService _exchangeRateService;
|
||||
|
||||
public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService)
|
||||
public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService, IExchangeRateService exchangeRateService)
|
||||
{
|
||||
_db = db;
|
||||
_marketDataService = marketDataService;
|
||||
_exchangeRateService = exchangeRateService;
|
||||
}
|
||||
|
||||
public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId)
|
||||
@ -118,23 +120,83 @@ public class PortfolioService : IPortfolioService
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public TotalAssetsResponse GetTotalAssets(string userId)
|
||||
public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
|
||||
{
|
||||
var totalValue = _db.Queryable<Portfolio>()
|
||||
.Where(p => p.UserId == userId)
|
||||
.Sum(p => p.TotalValue);
|
||||
// 获取用户信息(包含默认本位币)
|
||||
var user = _db.Queryable<User>()
|
||||
.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
|
||||
{
|
||||
totalValue = (double)totalValue,
|
||||
currency = "CNY", // 假设统一转换为CNY
|
||||
todayProfit = 0,
|
||||
todayProfitCurrency = "CNY",
|
||||
totalReturnRate = 0
|
||||
totalValue = (double)totalValueInTargetCurrency,
|
||||
currency = targetCurrency,
|
||||
todayProfit = (double)totalTodayProfitInTargetCurrency,
|
||||
todayProfitCurrency = targetCurrency,
|
||||
totalReturnRate = totalReturnRate
|
||||
};
|
||||
}
|
||||
|
||||
// 保留同步方法作为兼容
|
||||
public TotalAssetsResponse GetTotalAssets(string userId)
|
||||
{
|
||||
return GetTotalAssetsAsync(userId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
|
||||
{
|
||||
var portfolio = _db.Queryable<Portfolio>()
|
||||
@ -153,6 +215,7 @@ public class PortfolioService : IPortfolioService
|
||||
// 获取每个持仓的实时价格并计算
|
||||
decimal totalPortfolioValue = 0;
|
||||
decimal totalCost = 0;
|
||||
decimal totalTodayProfit = 0;
|
||||
var positionItems = new List<PositionItem>();
|
||||
|
||||
foreach (var pos in positions)
|
||||
@ -169,13 +232,16 @@ public class PortfolioService : IPortfolioService
|
||||
}
|
||||
|
||||
decimal currentPrice = priceResponse.Price;
|
||||
decimal previousClose = priceResponse.PreviousClose;
|
||||
decimal positionValue = pos.Shares * currentPrice;
|
||||
decimal cost = pos.Shares * pos.AvgPrice;
|
||||
decimal profit = positionValue - cost;
|
||||
double profitRate = cost > 0 ? (double)(profit / cost * 100) : 0;
|
||||
decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
|
||||
|
||||
totalPortfolioValue += positionValue;
|
||||
totalCost += cost;
|
||||
totalTodayProfit += todayProfit;
|
||||
|
||||
positionItems.Add(new PositionItem
|
||||
{
|
||||
@ -189,7 +255,7 @@ public class PortfolioService : IPortfolioService
|
||||
totalValue = (double)positionValue,
|
||||
profit = (double)profit,
|
||||
profitRate = profitRate,
|
||||
changeAmount = 0, // 今日盈亏后续实现
|
||||
changeAmount = (double)todayProfit,
|
||||
ratio = 0, // 后面统一计算比例
|
||||
deviationRatio = 0, // 后续实现
|
||||
currency = pos.Currency
|
||||
@ -219,7 +285,7 @@ public class PortfolioService : IPortfolioService
|
||||
},
|
||||
portfolioValue = (double)totalPortfolioValue,
|
||||
totalReturn = (double)totalReturn,
|
||||
todayProfit = 0, // 后续 P1-1 实现
|
||||
todayProfit = (double)totalTodayProfit,
|
||||
historicalChange = totalReturnRate,
|
||||
dailyVolatility = 0, // 后续实现
|
||||
todayProfitCurrency = portfolio.Currency,
|
||||
@ -371,12 +437,30 @@ public class PortfolioService : IPortfolioService
|
||||
_db.Insertable(position).ExecuteCommand();
|
||||
}
|
||||
|
||||
// 更新投资组合总价值
|
||||
var totalValue = _db.Queryable<Position>()
|
||||
// 更新投资组合总价值(使用实时市值而不是成本价)
|
||||
var positions = _db.Queryable<Position>()
|
||||
.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;
|
||||
_db.Updateable(portfolio).ExecuteCommand();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user