From 556f705f754b1e443b0621f3259060f413dd9edf Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Tue, 28 Apr 2026 17:36:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(ledger):=20=E4=BF=AE=E5=BE=A9=E5=88=86?= =?UTF-8?q?=E7=B4=85=E6=A5=AD=E5=8B=99=E9=82=8F=E8=BC=AF=E8=88=87=E6=88=90?= =?UTF-8?q?=E6=9C=AC=E7=AE=97=E6=B3=95=EF=BC=8C=E6=94=AF=E6=8C=81=E6=94=A4?= =?UTF-8?q?=E8=96=84=E6=88=90=E6=9C=AC=E7=82=BA=E8=B2=A0=E6=95=B8=E7=9A=84?= =?UTF-8?q?=E6=A5=B5=E7=AB=AF=E5=A0=B4=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 8 +++++++- src/actions/portfolio.ts | 12 +++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Memory.md b/Memory.md index 9845d09..012fa5c 100644 --- a/Memory.md +++ b/Memory.md @@ -48,4 +48,10 @@ - 引入摊薄成本与平均成本双重指标:`avgCost = totalBuyCost / totalBuyQuantity`(平均成本);`dilutedCost = (totalBuyCost - realizedPnlCny) / currentQuantity`(摊薄成本,已实现盈亏会摊低持仓成本)。 - 新增持仓天数统计:`holdingDays = today - 第一次 BUY 的日期`(基于上海时区)。 - Dashboard 首页总览区分展示『持仓盈亏 (Unrealized P&L)』和『总盈亏 (Total P&L)』。 -- 持仓卡片新增『平均成本』、『摊薄成本』和『持仓天数』展示。 \ No newline at end of file +- 持仓卡片新增『平均成本』、『摊薄成本』和『持仓天数』展示。 + +## 分紅業務邏輯與成本算法修復 (Task 33) +- 重構了分紅的會計處理邏輯,將其正確計入已實現盈虧:DIVIDEND 不再增加持倉數量,而是按 `quantity * price * exchangeRate` 計算分紅金額並累加至 `realizedPnlCny`。 +- 新增 `totalDividendCny` 字段追蹤累計分紅金額。 +- 修正攤薄成本算法:`dilutedCost = (totalBuyCostCny - realizedPnlCny - totalDividendCny) / currentQuantity`,確保極端情況下攤薄成本為負數時精確返回負數,絕不兜底為 0。 +- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。 \ No newline at end of file diff --git a/src/actions/portfolio.ts b/src/actions/portfolio.ts index d01054c..b50e3c5 100644 --- a/src/actions/portfolio.ts +++ b/src/actions/portfolio.ts @@ -25,6 +25,7 @@ interface Position { dilutedCost: string; holdingDays: number; exchange: string; + totalDividendCny: string; } interface RawRate { @@ -179,6 +180,7 @@ export async function getPortfolioPositions(): Promise { totalBuyCostNative: new Big('0'), totalBuyQuantity: new Big('0'), realizedPnlCny: new Big('0'), + totalDividendCny: new Big('0'), firstBuyDate: null, }); } @@ -228,7 +230,10 @@ export async function getPortfolioPositions(): Promise { } else if (tx.txType === 'AIRDROP') { holding.quantity = holding.quantity.plus(new Big(tx.quantity)); } else if (tx.txType === 'DIVIDEND') { - holding.quantity = holding.quantity.plus(new Big(tx.quantity)); + 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); } if (tx.assetLatestPrice) { @@ -263,10 +268,10 @@ export async function getPortfolioPositions(): Promise { avgCost = holding.totalBuyCostCny.div(holding.totalBuyQuantity); } - // 摊薄成本 = (总买入成本 - 已实现盈亏) / 当前持仓数量 + // 摊薄成本 = (总买入成本 - 已实现盈亏 - 总分红) / 当前持仓数量 let dilutedCost = new Big('0'); if (holding.quantity.gt(0)) { - dilutedCost = holding.totalBuyCostCny.minus(holding.realizedPnlCny).div(holding.quantity); + dilutedCost = holding.totalBuyCostCny.minus(holding.realizedPnlCny).minus(holding.totalDividendCny).div(holding.quantity); } // 持仓天数 @@ -295,6 +300,7 @@ export async function getPortfolioPositions(): Promise { dilutedCost: dilutedCost.toString(), holdingDays, exchange: holding.exchange, + totalDividendCny: holding.totalDividendCny.toString(), }); }