fix(ledger): 修复 T+0 时间戳碰撞导致聚合乱序,实装清仓归零阻断机制
This commit is contained in:
@@ -194,7 +194,7 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
|
||||
})
|
||||
.from(transactions)
|
||||
.leftJoin(assets, eq(assets.id, transactions.assetId))
|
||||
.orderBy(asc(transactions.executedAt));
|
||||
.orderBy(asc(transactions.executedAt), asc(transactions.createdAt), asc(transactions.id));
|
||||
|
||||
const dynamicRateMap = await getLatestRatesMap();
|
||||
|
||||
@@ -232,6 +232,11 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
|
||||
for (const tx of allTransactions) {
|
||||
if (!tx.assetId) continue;
|
||||
|
||||
// [架构红线] 强制标准化交易类型:大写 + 去空格,兼容中文脏数据
|
||||
const txType = String(tx.txType).toUpperCase().trim();
|
||||
const isBuy = txType === 'BUY' || txType === '\u5165\u4e70';
|
||||
const isSell = txType === 'SELL' || txType === '\u5356\u51fa';
|
||||
|
||||
const existing = holdings.get(tx.assetId);
|
||||
if (!existing) {
|
||||
holdings.set(tx.assetId, {
|
||||
@@ -257,7 +262,7 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
|
||||
|
||||
const holding = holdings.get(tx.assetId)!;
|
||||
|
||||
if (tx.txType === 'BUY') {
|
||||
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);
|
||||
@@ -276,7 +281,7 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
|
||||
if (!holding.firstBuyDate && tx.executedAt) {
|
||||
holding.firstBuyDate = new Date(tx.executedAt);
|
||||
}
|
||||
} else if (tx.txType === 'SELL') {
|
||||
} else if (isSell) {
|
||||
// 计算卖出时的平均成本 (Native)
|
||||
let avgCostPerUnitNative = new Big('0');
|
||||
if (holding.totalBuyQuantity.gt(0)) {
|
||||
@@ -307,9 +312,17 @@ export async function getPortfolioPositions(includeCleared: boolean = false): Pr
|
||||
holding.realizedPnlCny = holding.realizedPnlCny.plus(realizedPnlCny);
|
||||
|
||||
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
|
||||
} else if (tx.txType === 'AIRDROP') {
|
||||
|
||||
// [核心阻断器] 防浮点数灰尘与清仓重置:一旦清仓,强制清零所有持仓成本
|
||||
if (holding.quantity.lte(new Big('1e-8'))) {
|
||||
holding.quantity = new Big(0);
|
||||
holding.totalBuyCostCny = new Big(0);
|
||||
holding.totalBuyCostNative = new Big(0);
|
||||
holding.totalBuyQuantity = new Big(0);
|
||||
}
|
||||
} else if (txType === 'AIRDROP') {
|
||||
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
||||
} else if (tx.txType === 'DIVIDEND') {
|
||||
} else if (txType === 'DIVIDEND') {
|
||||
const dividendAmountNative = new Big(tx.quantity).times(new Big(tx.price));
|
||||
const dividendCny = dividendAmountNative.times(new Big(tx.exchangeRate || '1'));
|
||||
holding.accumulatedDividendsCny = holding.accumulatedDividendsCny.plus(dividendCny);
|
||||
|
||||
Reference in New Issue
Block a user