fix(api): 重构实时持仓引擎,统一法币成本的历史汇率核算标准

This commit is contained in:
2026-05-03 02:36:14 +08:00
parent ef412b366a
commit b8666f6dd1
2 changed files with 35 additions and 30 deletions
+25 -30
View File
@@ -264,17 +264,13 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
if (isBuy) {
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
const costPerUnit = new Big(tx.quantity).times(new Big(tx.price));
holding.totalBuyCostNative = holding.totalBuyCostNative.plus(costPerUnit);
let appliedRate = tx.exchangeRate;
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
if (fallbackRate) {
appliedRate = fallbackRate;
}
}
const costCny = costPerUnit.times(new Big(appliedRate || '1'));
holding.totalBuyCostCny = holding.totalBuyCostCny.plus(costCny);
// [架构红线] 买入法币成本 = 数量 * 价格 * 该笔交易历史汇率,禁止在最后乘以当前汇率
const txFx = new Big(tx.exchangeRate || '1');
const fiatCost = new Big(tx.quantity).times(new Big(tx.price)).times(txFx);
holding.totalBuyCostNative = holding.totalBuyCostNative.plus(new Big(tx.quantity).times(new Big(tx.price)));
holding.totalBuyCostCny = holding.totalBuyCostCny.plus(fiatCost);
holding.totalBuyQuantity = holding.totalBuyQuantity.plus(new Big(tx.quantity));
// 记录首次买入日期
@@ -282,38 +278,37 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
holding.firstBuyDate = new Date(tx.executedAt);
}
} else if (isSell) {
// 计算卖出时的平均成本 (Native)
// 已实现盈亏 (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);
}
// 已实现盈亏 = (卖出价 - 平均成本) * 卖出数量 (Native)
const sellRevenueNative = new Big(tx.quantity).times(new Big(tx.price));
const costBasisNative = avgCostPerUnitNative.times(new Big(tx.quantity));
const realizedPnlNative = sellRevenueNative.minus(costBasisNative);
holding.realizedPnlNative = holding.realizedPnlNative.plus(realizedPnlNative);
holding.realizedPnlNative = holding.realizedPnlNative.plus(sellRevenueNative.minus(costBasisNative));
// 已实现盈亏 (CNY) 保留兼容
let appliedRate = tx.exchangeRate;
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
if (fallbackRate) {
appliedRate = fallbackRate;
}
}
const sellRevenueCny = new Big(tx.quantity).times(new Big(tx.price)).times(new Big(appliedRate || '1'));
// 已实现盈亏 (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);
}
const costBasisCny = avgCostPerUnitCny.times(new Big(tx.quantity));
const realizedPnlCny = sellRevenueCny.minus(costBasisCny);
holding.realizedPnlCny = holding.realizedPnlCny.plus(realizedPnlCny);
holding.realizedPnlCny = holding.realizedPnlCny.plus(sellRevenueCny.minus(avgCostPerUnitCny.times(new Big(tx.quantity))));
// [核心阻断器] 卖出时按法币成本比例扣减,保持汇率一致性
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)
);
}
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
// [核心阻断器] 防浮点数灰尘与清仓重置:一旦清仓,强制清零所有持仓成本
// 清仓重置
if (holding.quantity.lte(new Big('1e-8'))) {
holding.quantity = new Big(0);
holding.totalBuyCostCny = new Big(0);