fix(ledger): 修復分紅業務邏輯與成本算法,支持攤薄成本為負數的極端場景
This commit is contained in:
parent
9342e46aad
commit
556f705f75
@ -49,3 +49,9 @@
|
|||||||
- 新增持仓天数统计:`holdingDays = today - 第一次 BUY 的日期`(基于上海时区)。
|
- 新增持仓天数统计:`holdingDays = today - 第一次 BUY 的日期`(基于上海时区)。
|
||||||
- Dashboard 首页总览区分展示『持仓盈亏 (Unrealized P&L)』和『总盈亏 (Total P&L)』。
|
- Dashboard 首页总览区分展示『持仓盈亏 (Unrealized P&L)』和『总盈亏 (Total P&L)』。
|
||||||
- 持仓卡片新增『平均成本』、『摊薄成本』和『持仓天数』展示。
|
- 持仓卡片新增『平均成本』、『摊薄成本』和『持仓天数』展示。
|
||||||
|
|
||||||
|
## 分紅業務邏輯與成本算法修復 (Task 33)
|
||||||
|
- 重構了分紅的會計處理邏輯,將其正確計入已實現盈虧:DIVIDEND 不再增加持倉數量,而是按 `quantity * price * exchangeRate` 計算分紅金額並累加至 `realizedPnlCny`。
|
||||||
|
- 新增 `totalDividendCny` 字段追蹤累計分紅金額。
|
||||||
|
- 修正攤薄成本算法:`dilutedCost = (totalBuyCostCny - realizedPnlCny - totalDividendCny) / currentQuantity`,確保極端情況下攤薄成本為負數時精確返回負數,絕不兜底為 0。
|
||||||
|
- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。
|
||||||
@ -25,6 +25,7 @@ interface Position {
|
|||||||
dilutedCost: string;
|
dilutedCost: string;
|
||||||
holdingDays: number;
|
holdingDays: number;
|
||||||
exchange: string;
|
exchange: string;
|
||||||
|
totalDividendCny: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawRate {
|
interface RawRate {
|
||||||
@ -179,6 +180,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
totalBuyCostNative: new Big('0'),
|
totalBuyCostNative: new Big('0'),
|
||||||
totalBuyQuantity: new Big('0'),
|
totalBuyQuantity: new Big('0'),
|
||||||
realizedPnlCny: new Big('0'),
|
realizedPnlCny: new Big('0'),
|
||||||
|
totalDividendCny: new Big('0'),
|
||||||
firstBuyDate: null,
|
firstBuyDate: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -228,7 +230,10 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
} else if (tx.txType === 'AIRDROP') {
|
} else if (tx.txType === 'AIRDROP') {
|
||||||
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
||||||
} else if (tx.txType === 'DIVIDEND') {
|
} 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) {
|
if (tx.assetLatestPrice) {
|
||||||
@ -263,10 +268,10 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
avgCost = holding.totalBuyCostCny.div(holding.totalBuyQuantity);
|
avgCost = holding.totalBuyCostCny.div(holding.totalBuyQuantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 摊薄成本 = (总买入成本 - 已实现盈亏) / 当前持仓数量
|
// 摊薄成本 = (总买入成本 - 已实现盈亏 - 总分红) / 当前持仓数量
|
||||||
let dilutedCost = new Big('0');
|
let dilutedCost = new Big('0');
|
||||||
if (holding.quantity.gt(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<Position[]> {
|
|||||||
dilutedCost: dilutedCost.toString(),
|
dilutedCost: dilutedCost.toString(),
|
||||||
holdingDays,
|
holdingDays,
|
||||||
exchange: holding.exchange,
|
exchange: holding.exchange,
|
||||||
|
totalDividendCny: holding.totalDividendCny.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user