fix: 修复多个金融计算bug

1. 卖出时累计成本计算:改为按比例减少成本,而非用卖出金额抵扣
2. 夏普比率计算:收益率从百分比转为小数后计算,修正年化公式
3. 最大回撤初始值:使用首条记录的净值作为初始peak,而非硬编码1.0
This commit is contained in:
OpenClaw Agent 2026-03-25 04:03:37 +00:00
parent 650d59aaff
commit 2a297081b0

View File

@ -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% 无风险利率
}
// 总收益率