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)
|
.OrderByDescending(n => n.NavDate)
|
||||||
.FirstAsync();
|
.FirstAsync();
|
||||||
|
|
||||||
// 计算累计投入成本(从交易记录汇总)
|
// 计算累计投入成本(从交易记录汇总,卖出时按比例减少成本)
|
||||||
var transactions = await _db.Queryable<Transaction>()
|
var transactions = await _db.Queryable<Transaction>()
|
||||||
.Where(t => t.PortfolioId == portfolioId && t.TransactionTime.Date <= today)
|
.Where(t => t.PortfolioId == portfolioId && t.TransactionTime.Date <= today)
|
||||||
|
.OrderBy(t => t.TransactionTime)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
decimal totalCost = 0;
|
decimal totalCost = 0;
|
||||||
|
var holdingsCost = new Dictionary<string, decimal>(); // 每个标的的累计成本
|
||||||
|
|
||||||
foreach (var tx in transactions)
|
foreach (var tx in transactions)
|
||||||
{
|
{
|
||||||
|
if (tx.StockCode == null) continue;
|
||||||
|
|
||||||
if (tx.Type == "buy")
|
if (tx.Type == "buy")
|
||||||
{
|
{
|
||||||
decimal txAmount = tx.TotalAmount;
|
decimal txAmount = tx.TotalAmount;
|
||||||
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
||||||
txAmount, tx.Currency, targetCurrency);
|
txAmount, tx.Currency, targetCurrency);
|
||||||
totalCost += txAmountInTarget;
|
totalCost += txAmountInTarget;
|
||||||
|
|
||||||
|
// 更新该标的的累计成本
|
||||||
|
if (holdingsCost.ContainsKey(tx.StockCode))
|
||||||
|
{
|
||||||
|
holdingsCost[tx.StockCode] += txAmountInTarget;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
holdingsCost[tx.StockCode] = txAmountInTarget;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (tx.Type == "sell")
|
else if (tx.Type == "sell")
|
||||||
{
|
{
|
||||||
decimal txAmount = tx.TotalAmount;
|
// 卖出时按比例减少该标的的累计成本
|
||||||
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
if (holdingsCost.ContainsKey(tx.StockCode) && tx.Amount > 0)
|
||||||
txAmount, tx.Currency, targetCurrency);
|
{
|
||||||
totalCost -= txAmountInTarget;
|
// 需要知道当时该标的的总数量来计算比例
|
||||||
|
// 从 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);
|
holdings[tx.StockCode] = (remainingShares, remainingCost, existing.currency, existing.assetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 减少累计投入成本
|
// 按比例减少累计投入成本(关键修复:不再用卖出金额)
|
||||||
decimal txAmountInTarget = await _exchangeRateService.ConvertAmountAsync(
|
decimal costToReduce = cumulativeCost * (decimal)soldRatio;
|
||||||
tx.TotalAmount, tx.Currency, targetCurrency);
|
cumulativeCost -= costToReduce;
|
||||||
cumulativeCost -= txAmountInTarget;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -560,10 +586,11 @@ public class PortfolioNavService : IPortfolioNavService
|
|||||||
var maxReturn = returns.Any() ? returns.Max() : 0;
|
var maxReturn = returns.Any() ? returns.Max() : 0;
|
||||||
var minReturn = returns.Any() ? returns.Min() : 0;
|
var minReturn = returns.Any() ? returns.Min() : 0;
|
||||||
|
|
||||||
// 计算最大回撤
|
// 计算最大回撤(修复:用第一条记录的 Nav 作为初始 peak)
|
||||||
double maxDrawdown = 0;
|
double maxDrawdown = 0;
|
||||||
double peak = 1.0;
|
var orderedHistory = history.OrderBy(h => h.NavDate).ToList();
|
||||||
foreach (var item in history.OrderBy(h => h.NavDate))
|
double peak = (double)orderedHistory.First().Nav;
|
||||||
|
foreach (var item in orderedHistory)
|
||||||
{
|
{
|
||||||
var nav = (double)item.Nav;
|
var nav = (double)item.Nav;
|
||||||
if (nav > peak) peak = nav;
|
if (nav > peak) peak = nav;
|
||||||
@ -571,15 +598,21 @@ public class PortfolioNavService : IPortfolioNavService
|
|||||||
if (drawdown > maxDrawdown) maxDrawdown = drawdown;
|
if (drawdown > maxDrawdown) maxDrawdown = drawdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算夏普比率(简化版,假设无风险利率=3%年化)
|
// 计算夏普比率(修复:收益率从百分比转为小数)
|
||||||
double sharpeRatio = 0;
|
double sharpeRatio = 0;
|
||||||
double volatility = 0;
|
double volatility = 0;
|
||||||
if (returns.Any())
|
if (returns.Any())
|
||||||
{
|
{
|
||||||
var avgReturn = returns.Average();
|
// 收益率从百分比形式转换为小数形式(如 0.5% -> 0.005)
|
||||||
var stdDev = Math.Sqrt(returns.Sum(r => Math.Pow(r - avgReturn, 2)) / returns.Count);
|
var decimalReturns = returns.Select(r => r / 100.0).ToList();
|
||||||
volatility = stdDev * Math.Sqrt(252); // 年化波动率
|
var avgReturn = decimalReturns.Average();
|
||||||
sharpeRatio = stdDev > 0 ? (avgReturn * 252 - 3) / (stdDev * Math.Sqrt(252)) : 0;
|
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