fix(api): 修复卖出核算的分母逻辑,彻底对齐持仓与快照本金

This commit is contained in:
2026-05-03 03:11:59 +08:00
parent b8666f6dd1
commit ab8b49ca23
2 changed files with 30 additions and 24 deletions
+23 -24
View File
@@ -278,37 +278,36 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
holding.firstBuyDate = new Date(tx.executedAt);
}
} else if (isSell) {
// 已实现盈亏 (Native)
const sellRevenueNative = new Big(tx.quantity).times(new Big(tx.price));
let avgCostPerUnitNative = new Big('0');
if (holding.totalBuyQuantity.gt(0)) {
avgCostPerUnitNative = holding.totalBuyCostNative.div(holding.totalBuyQuantity);
const sellQty = new Big(tx.quantity);
const sellPrice = new Big(tx.price);
const txFx = new Big(tx.exchangeRate || '1');
// 1. Native 维度的平均成本与已实现盈亏计算
let avgCostNative = new Big('0');
if (holding.quantity.gt(0)) {
avgCostNative = holding.totalBuyCostNative.div(holding.quantity); // 核心修复:使用当前真实持仓量
}
const costBasisNative = avgCostPerUnitNative.times(new Big(tx.quantity));
const costBasisNative = avgCostNative.times(sellQty);
const sellRevenueNative = sellQty.times(sellPrice);
holding.realizedPnlNative = holding.realizedPnlNative.plus(sellRevenueNative.minus(costBasisNative));
// 已实现盈亏 (CNY) — 使用累计法币成本计算平均成本
const sellRevenueCny = new Big(tx.quantity).times(new Big(tx.price)).times(new Big(tx.exchangeRate || '1'));
let avgCostPerUnitCny = new Big('0');
if (holding.totalBuyQuantity.gt(0)) {
avgCostPerUnitCny = holding.totalBuyCostCny.div(holding.totalBuyQuantity);
// 2. CNY (法币) 维度的平均成本与已实现盈亏计算
let avgCostCny = new Big('0');
if (holding.quantity.gt(0)) {
avgCostCny = holding.totalBuyCostCny.div(holding.quantity); // 核心修复:使用当前真实持仓量
}
holding.realizedPnlCny = holding.realizedPnlCny.plus(sellRevenueCny.minus(avgCostPerUnitCny.times(new Big(tx.quantity))));
const costBasisCny = avgCostCny.times(sellQty);
const sellRevenueCny = sellRevenueNative.times(txFx);
holding.realizedPnlCny = holding.realizedPnlCny.plus(sellRevenueCny.minus(costBasisCny));
// [核心阻断器] 卖出时按法币成本比例扣减,保持汇率一致性
if (holding.totalBuyCostCny.gt(0) && holding.totalBuyQuantity.gt(0)) {
const sellRatio = new Big(tx.quantity).div(holding.totalBuyQuantity);
holding.totalBuyCostCny = holding.totalBuyCostCny.minus(
holding.totalBuyCostCny.times(sellRatio)
);
holding.totalBuyCostNative = holding.totalBuyCostNative.minus(
holding.totalBuyCostNative.times(sellRatio)
);
}
// 3. 扣减本金余额 (确保法币与外币同步等比下降)
holding.totalBuyCostNative = holding.totalBuyCostNative.minus(costBasisNative);
holding.totalBuyCostCny = holding.totalBuyCostCny.minus(costBasisCny);
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
// 4. 更新真实持仓数量
holding.quantity = holding.quantity.minus(sellQty);
// 清仓重置
// 清仓重置兜底逻辑 (防御浮点数精度残留)
if (holding.quantity.lte(new Big('1e-8'))) {
holding.quantity = new Big(0);
holding.totalBuyCostCny = new Big(0);