feat: 完成 P1 任务 - 今日盈亏、风险平价补全、Mock 数据 PreviousClose
This commit is contained in:
parent
4816980d62
commit
567504119c
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user