From 03e8e982607ae584cd91ca5be2a991befd72a003 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Tue, 28 Apr 2026 18:51:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(ledger):=20=E4=BF=AE=E5=BE=A9=E5=B9=B3?= =?UTF-8?q?=E5=9D=87=E6=88=90=E6=9C=AC=E9=A1=AF=E7=A4=BA=20Bug=EF=BC=8C?= =?UTF-8?q?=E4=B8=A6=E5=84=AA=E5=8C=96=E5=88=86=E7=B4=85=E7=8D=A8=E7=AB=8B?= =?UTF-8?q?=E7=B5=B1=E8=A8=88=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 7 ++++++- src/actions/portfolio.ts | 17 +++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Memory.md b/Memory.md index c780040..47ab059 100644 --- a/Memory.md +++ b/Memory.md @@ -54,4 +54,9 @@ - 重構了分紅的會計處理邏輯,將其正確計入已實現盈虧:DIVIDEND 不再增加持倉數量,而是按 `quantity * price * exchangeRate` 計算分紅金額並累加至 `realizedPnlCny`。 - 新增 `totalDividendCny` 字段追蹤累計分紅金額。 - 修正攤薄成本算法:`dilutedCost = (totalBuyCostCny - realizedPnlCny - totalDividendCny) / currentQuantity`,確保極端情況下攤薄成本為負數時精確返回負數,絕不兜底為 0。 -- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。 \ No newline at end of file +- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。 + +## 修復平均成本顯示 Bug與分紅獨立統計 (Task 35) +- 修復了平均成本在前端顯示為空的問題:`totalBuyQuantity` 在 BUY 交易處理中從未累加,導致 `avgCost` 計算時除數為零而永遠返回 0。現在在 BUY 時正確執行 `totalBuyQuantity += quantity`。 +- 將分紅邏輯從已實現盈虧中剝離,建立獨立的累計分紅統計維度:新增 `accumulatedDividendsCny` 字段,DIVIDEND 交易不再混入 `realizedPnlCny`,而是獨立累加至 `accumulatedDividendsCny`。 +- 重新定義總盈虧公式:`totalPnlCny = unrealizedPnlCny + realizedPnlCny + accumulatedDividendsCny`,確保分紅有獨立的統計維度且不會干擾平均成本計算。 \ No newline at end of file diff --git a/src/actions/portfolio.ts b/src/actions/portfolio.ts index b50e3c5..73757ed 100644 --- a/src/actions/portfolio.ts +++ b/src/actions/portfolio.ts @@ -25,7 +25,7 @@ interface Position { dilutedCost: string; holdingDays: number; exchange: string; - totalDividendCny: string; + accumulatedDividendsCny: string; } interface RawRate { @@ -158,6 +158,7 @@ export async function getPortfolioPositions(): Promise { totalBuyQuantity: Big; // 已实现盈亏 realizedPnlCny: Big; + accumulatedDividendsCny: Big; // 首次买入日期 firstBuyDate: Date | null; }>(); @@ -180,7 +181,7 @@ export async function getPortfolioPositions(): Promise { totalBuyCostNative: new Big('0'), totalBuyQuantity: new Big('0'), realizedPnlCny: new Big('0'), - totalDividendCny: new Big('0'), + accumulatedDividendsCny: new Big('0'), firstBuyDate: null, }); } @@ -200,6 +201,7 @@ export async function getPortfolioPositions(): Promise { } const costCny = costPerUnit.times(new Big(appliedRate || '1')); holding.totalBuyCostCny = holding.totalBuyCostCny.plus(costCny); + holding.totalBuyQuantity = holding.totalBuyQuantity.plus(new Big(tx.quantity)); // 记录首次买入日期 if (!holding.firstBuyDate && tx.executedAt) { @@ -232,8 +234,7 @@ export async function getPortfolioPositions(): Promise { } else if (tx.txType === 'DIVIDEND') { const dividendAmountNative = new Big(tx.quantity).times(new Big(tx.price)); const dividendCny = dividendAmountNative.times(new Big(tx.exchangeRate || '1')); - holding.realizedPnlCny = holding.realizedPnlCny.plus(dividendCny); - holding.totalDividendCny = holding.totalDividendCny.plus(dividendCny); + holding.accumulatedDividendsCny = holding.accumulatedDividendsCny.plus(dividendCny); } if (tx.assetLatestPrice) { @@ -256,8 +257,8 @@ export async function getPortfolioPositions(): Promise { // 未实现盈亏 = 当前市值 - 总买入成本 const unrealizedPnlCny = cnyValue.minus(holding.totalBuyCostCny); - // 总盈亏 = 未实现盈亏 + 已实现盈亏 - const totalPnlCny = unrealizedPnlCny.plus(holding.realizedPnlCny); + // 总盈亏 = 当前市值 - 总买入成本 + 已实现盈亏 + 累计分红 + const totalPnlCny = unrealizedPnlCny.plus(holding.realizedPnlCny).plus(holding.accumulatedDividendsCny); const currentNativeValue = new Big(holding.latestPrice).times(holding.quantity); const pnlNative = currentNativeValue.minus(holding.totalBuyCostNative); @@ -271,7 +272,7 @@ export async function getPortfolioPositions(): Promise { // 摊薄成本 = (总买入成本 - 已实现盈亏 - 总分红) / 当前持仓数量 let dilutedCost = new Big('0'); if (holding.quantity.gt(0)) { - dilutedCost = holding.totalBuyCostCny.minus(holding.realizedPnlCny).minus(holding.totalDividendCny).div(holding.quantity); + dilutedCost = holding.totalBuyCostCny.minus(holding.realizedPnlCny).minus(holding.accumulatedDividendsCny).div(holding.quantity); } // 持仓天数 @@ -300,7 +301,7 @@ export async function getPortfolioPositions(): Promise { dilutedCost: dilutedCost.toString(), holdingDays, exchange: holding.exchange, - totalDividendCny: holding.totalDividendCny.toString(), + accumulatedDividendsCny: holding.accumulatedDividendsCny.toString(), }); }