fix(ledger): 修復平均成本顯示 Bug,並優化分紅獨立統計邏輯
This commit is contained in:
parent
a04c573cd3
commit
03e8e98260
@ -55,3 +55,8 @@
|
|||||||
- 新增 `totalDividendCny` 字段追蹤累計分紅金額。
|
- 新增 `totalDividendCny` 字段追蹤累計分紅金額。
|
||||||
- 修正攤薄成本算法:`dilutedCost = (totalBuyCostCny - realizedPnlCny - totalDividendCny) / currentQuantity`,確保極端情況下攤薄成本為負數時精確返回負數,絕不兜底為 0。
|
- 修正攤薄成本算法:`dilutedCost = (totalBuyCostCny - realizedPnlCny - totalDividendCny) / currentQuantity`,確保極端情況下攤薄成本為負數時精確返回負數,絕不兜底為 0。
|
||||||
- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。
|
- 平均成本 `avgCost = totalBuyCostCny / totalBuyQuantity` 保持不變,僅在遍歷結束後計算。
|
||||||
|
|
||||||
|
## 修復平均成本顯示 Bug與分紅獨立統計 (Task 35)
|
||||||
|
- 修復了平均成本在前端顯示為空的問題:`totalBuyQuantity` 在 BUY 交易處理中從未累加,導致 `avgCost` 計算時除數為零而永遠返回 0。現在在 BUY 時正確執行 `totalBuyQuantity += quantity`。
|
||||||
|
- 將分紅邏輯從已實現盈虧中剝離,建立獨立的累計分紅統計維度:新增 `accumulatedDividendsCny` 字段,DIVIDEND 交易不再混入 `realizedPnlCny`,而是獨立累加至 `accumulatedDividendsCny`。
|
||||||
|
- 重新定義總盈虧公式:`totalPnlCny = unrealizedPnlCny + realizedPnlCny + accumulatedDividendsCny`,確保分紅有獨立的統計維度且不會干擾平均成本計算。
|
||||||
@ -25,7 +25,7 @@ interface Position {
|
|||||||
dilutedCost: string;
|
dilutedCost: string;
|
||||||
holdingDays: number;
|
holdingDays: number;
|
||||||
exchange: string;
|
exchange: string;
|
||||||
totalDividendCny: string;
|
accumulatedDividendsCny: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawRate {
|
interface RawRate {
|
||||||
@ -158,6 +158,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
totalBuyQuantity: Big;
|
totalBuyQuantity: Big;
|
||||||
// 已实现盈亏
|
// 已实现盈亏
|
||||||
realizedPnlCny: Big;
|
realizedPnlCny: Big;
|
||||||
|
accumulatedDividendsCny: Big;
|
||||||
// 首次买入日期
|
// 首次买入日期
|
||||||
firstBuyDate: Date | null;
|
firstBuyDate: Date | null;
|
||||||
}>();
|
}>();
|
||||||
@ -180,7 +181,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'),
|
accumulatedDividendsCny: new Big('0'),
|
||||||
firstBuyDate: null,
|
firstBuyDate: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -200,6 +201,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
}
|
}
|
||||||
const costCny = costPerUnit.times(new Big(appliedRate || '1'));
|
const costCny = costPerUnit.times(new Big(appliedRate || '1'));
|
||||||
holding.totalBuyCostCny = holding.totalBuyCostCny.plus(costCny);
|
holding.totalBuyCostCny = holding.totalBuyCostCny.plus(costCny);
|
||||||
|
holding.totalBuyQuantity = holding.totalBuyQuantity.plus(new Big(tx.quantity));
|
||||||
|
|
||||||
// 记录首次买入日期
|
// 记录首次买入日期
|
||||||
if (!holding.firstBuyDate && tx.executedAt) {
|
if (!holding.firstBuyDate && tx.executedAt) {
|
||||||
@ -232,8 +234,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
} else if (tx.txType === 'DIVIDEND') {
|
} else if (tx.txType === 'DIVIDEND') {
|
||||||
const dividendAmountNative = new Big(tx.quantity).times(new Big(tx.price));
|
const dividendAmountNative = new Big(tx.quantity).times(new Big(tx.price));
|
||||||
const dividendCny = dividendAmountNative.times(new Big(tx.exchangeRate || '1'));
|
const dividendCny = dividendAmountNative.times(new Big(tx.exchangeRate || '1'));
|
||||||
holding.realizedPnlCny = holding.realizedPnlCny.plus(dividendCny);
|
holding.accumulatedDividendsCny = holding.accumulatedDividendsCny.plus(dividendCny);
|
||||||
holding.totalDividendCny = holding.totalDividendCny.plus(dividendCny);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tx.assetLatestPrice) {
|
if (tx.assetLatestPrice) {
|
||||||
@ -256,8 +257,8 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
|
|
||||||
// 未实现盈亏 = 当前市值 - 总买入成本
|
// 未实现盈亏 = 当前市值 - 总买入成本
|
||||||
const unrealizedPnlCny = cnyValue.minus(holding.totalBuyCostCny);
|
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 currentNativeValue = new Big(holding.latestPrice).times(holding.quantity);
|
||||||
const pnlNative = currentNativeValue.minus(holding.totalBuyCostNative);
|
const pnlNative = currentNativeValue.minus(holding.totalBuyCostNative);
|
||||||
@ -271,7 +272,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
// 摊薄成本 = (总买入成本 - 已实现盈亏 - 总分红) / 当前持仓数量
|
// 摊薄成本 = (总买入成本 - 已实现盈亏 - 总分红) / 当前持仓数量
|
||||||
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).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<Position[]> {
|
|||||||
dilutedCost: dilutedCost.toString(),
|
dilutedCost: dilutedCost.toString(),
|
||||||
holdingDays,
|
holdingDays,
|
||||||
exchange: holding.exchange,
|
exchange: holding.exchange,
|
||||||
totalDividendCny: holding.totalDividendCny.toString(),
|
accumulatedDividendsCny: holding.accumulatedDividendsCny.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user