From 2a297081b01f995cf802879c57df5e5e04b4339c Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 25 Mar 2026 04:03:37 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E9=87=91=E8=9E=8D=E8=AE=A1=E7=AE=97bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 卖出时累计成本计算:改为按比例减少成本,而非用卖出金额抵扣 2. 夏普比率计算:收益率从百分比转为小数后计算,修正年化公式 3. 最大回撤初始值:使用首条记录的净值作为初始peak,而非硬编码1.0 --- AssetManager.Services/PortfolioNavService.cs | 67 +++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/AssetManager.Services/PortfolioNavService.cs b/AssetManager.Services/PortfolioNavService.cs index 7c71547..e8f6b95 100644 --- a/AssetManager.Services/PortfolioNavService.cs +++ b/AssetManager.Services/PortfolioNavService.cs @@ -182,27 +182,54 @@ public class PortfolioNavService : IPortfolioNavService .OrderByDescending(n => n.NavDate) .FirstAsync(); - // 计算累计投入成本(从交易记录汇总) + // 计算累计投入成本(从交易记录汇总,卖出时按比例减少成本) var transactions = await _db.Queryable() .Where(t => t.PortfolioId == portfolioId && t.TransactionTime.Date <= today) + .OrderBy(t => t.TransactionTime) .ToListAsync(); decimal totalCost = 0; + var holdingsCost = new Dictionary(); // 每个标的的累计成本 + 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% 无风险利率 } // 总收益率