fix: 修复多个金融计算bug
1. 卖出时累计成本计算:改为按比例减少成本,而非用卖出金额抵扣 2. 夏普比率计算:收益率从百分比转为小数后计算,修正年化公式 3. 最大回撤初始值:使用首条记录的净值作为初始peak,而非硬编码1.0
This commit is contained in:
parent
650d59aaff
commit
2a297081b0
@ -182,27 +182,54 @@ public class PortfolioNavService : IPortfolioNavService
|
||||
.OrderByDescending(n => n.NavDate)
|
||||
.FirstAsync();
|
||||
|
||||
// 计算累计投入成本(从交易记录汇总)
|
||||
// 计算累计投入成本(从交易记录汇总,卖出时按比例减少成本)
|
||||
var transactions = await _db.Queryable<Transaction>()
|
||||
.Where(t => t.PortfolioId == portfolioId && t.TransactionTime.Date <= today)
|
||||
.OrderBy(t => t.TransactionTime)
|
||||
.ToListAsync();
|
||||
|
||||
decimal totalCost = 0;
|
||||
var holdingsCost = new Dictionary<string, decimal>(); // 每个标的的累计成本
|
||||
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
if (tx.StockCode == null) continue;
|
||||
|
||||
if (tx.Type == "buy")
|
||||
{
|
||||
decimal txAmount = tx.TotalAmount;
|
||||
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
||||
txAmount, tx.Currency, targetCurrency);
|
||||
totalCost += txAmountInTarget;
|
||||
|
||||
// 更新该标的的累计成本
|
||||
if (holdingsCost.ContainsKey(tx.StockCode))
|
||||
{
|
||||
holdingsCost[tx.StockCode] += txAmountInTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
holdingsCost[tx.StockCode] = txAmountInTarget;
|
||||
}
|
||||
}
|
||||
else if (tx.Type == "sell")
|
||||
{
|
||||
decimal txAmount = tx.TotalAmount;
|
||||
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
||||
txAmount, tx.Currency, targetCurrency);
|
||||
totalCost -= txAmountInTarget;
|
||||
// 卖出时按比例减少该标的的累计成本
|
||||
if (holdingsCost.ContainsKey(tx.StockCode) && tx.Amount > 0)
|
||||
{
|
||||
// 需要知道当时该标的的总数量来计算比例
|
||||
// 从 Position 表获取当前持仓数量(不精确,但作为近似)
|
||||
var position = positions.FirstOrDefault(p => p.StockCode == tx.StockCode);
|
||||
if (position != null && position.Shares > 0)
|
||||
{
|
||||
// 近似:用当前持仓数量 + 卖出数量 作为原来数量
|
||||
decimal originalShares = position.Shares + tx.Amount;
|
||||
decimal soldRatio = tx.Amount / originalShares;
|
||||
decimal costToReduce = holdingsCost[tx.StockCode] * soldRatio;
|
||||
holdingsCost[tx.StockCode] -= costToReduce;
|
||||
totalCost -= costToReduce;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,10 +408,9 @@ public class PortfolioNavService : IPortfolioNavService
|
||||
holdings[tx.StockCode] = (remainingShares, remainingCost, existing.currency, existing.assetType);
|
||||
}
|
||||
|
||||
// 减少累计投入成本
|
||||
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
||||
tx.TotalAmount, tx.Currency, targetCurrency);
|
||||
cumulativeCost -= txAmountInTarget;
|
||||
// 按比例减少累计投入成本(关键修复:不再用卖出金额)
|
||||
decimal costToReduce = cumulativeCost * (decimal)soldRatio;
|
||||
cumulativeCost -= costToReduce;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -560,10 +586,11 @@ public class PortfolioNavService : IPortfolioNavService
|
||||
var maxReturn = returns.Any() ? returns.Max() : 0;
|
||||
var minReturn = returns.Any() ? returns.Min() : 0;
|
||||
|
||||
// 计算最大回撤
|
||||
// 计算最大回撤(修复:用第一条记录的 Nav 作为初始 peak)
|
||||
double maxDrawdown = 0;
|
||||
double peak = 1.0;
|
||||
foreach (var item in history.OrderBy(h => h.NavDate))
|
||||
var orderedHistory = history.OrderBy(h => h.NavDate).ToList();
|
||||
double peak = (double)orderedHistory.First().Nav;
|
||||
foreach (var item in orderedHistory)
|
||||
{
|
||||
var nav = (double)item.Nav;
|
||||
if (nav > peak) peak = nav;
|
||||
@ -571,15 +598,21 @@ public class PortfolioNavService : IPortfolioNavService
|
||||
if (drawdown > maxDrawdown) maxDrawdown = drawdown;
|
||||
}
|
||||
|
||||
// 计算夏普比率(简化版,假设无风险利率=3%年化)
|
||||
// 计算夏普比率(修复:收益率从百分比转为小数)
|
||||
double sharpeRatio = 0;
|
||||
double volatility = 0;
|
||||
if (returns.Any())
|
||||
{
|
||||
var avgReturn = returns.Average();
|
||||
var stdDev = Math.Sqrt(returns.Sum(r => Math.Pow(r - avgReturn, 2)) / returns.Count);
|
||||
volatility = stdDev * Math.Sqrt(252); // 年化波动率
|
||||
sharpeRatio = stdDev > 0 ? (avgReturn * 252 - 3) / (stdDev * Math.Sqrt(252)) : 0;
|
||||
// 收益率从百分比形式转换为小数形式(如 0.5% -> 0.005)
|
||||
var decimalReturns = returns.Select(r => r / 100.0).ToList();
|
||||
var avgReturn = decimalReturns.Average();
|
||||
var stdDev = Math.Sqrt(decimalReturns.Sum(r => Math.Pow(r - avgReturn, 2)) / decimalReturns.Count);
|
||||
|
||||
// 年化
|
||||
double annualizedReturn = avgReturn * 252;
|
||||
double annualizedVol = stdDev * Math.Sqrt(252);
|
||||
volatility = annualizedVol * 100; // 转回百分比形式显示
|
||||
sharpeRatio = annualizedVol > 0 ? (annualizedReturn - 0.03) / annualizedVol : 0; // 3% 无风险利率
|
||||
}
|
||||
|
||||
// 总收益率
|
||||
|
||||
Loading…
Reference in New Issue
Block a user