refactor(ledger): 抛弃单一个股的 CNY 折算,全面重构基于 Native 原生币种的成本与盈亏算法

This commit is contained in:
2026-04-28 19:37:36 +08:00
parent 7d7a7804a6
commit dd87eadbf4
2 changed files with 92 additions and 17 deletions
+82 -16
View File
@@ -27,6 +27,16 @@ interface Position {
exchange: string;
accumulatedDividendsCny: string;
accumulatedDividendsNative: string;
// Native 原生币种盈亏指标
totalBuyCostNative: string;
realizedPnlNative: string;
avgCostNative: string;
dilutedCostNative: string;
marketValueNative: string;
floatingPnlNative: string;
floatingPnlPercent: string;
cumulativePnlNative: string;
cumulativePnlPercent: string;
}
interface RawRate {
@@ -159,6 +169,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
totalBuyQuantity: Big;
// 已实现盈亏
realizedPnlCny: Big;
realizedPnlNative: Big;
accumulatedDividendsCny: Big;
accumulatedDividendsNative: Big;
// 首次买入日期
@@ -183,6 +194,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
totalBuyCostNative: new Big('0'),
totalBuyQuantity: new Big('0'),
realizedPnlCny: new Big('0'),
realizedPnlNative: new Big('0'),
accumulatedDividendsCny: new Big('0'),
accumulatedDividendsNative: new Big('0'),
firstBuyDate: null,
@@ -211,14 +223,19 @@ export async function getPortfolioPositions(): Promise<Position[]> {
holding.firstBuyDate = new Date(tx.executedAt);
}
} else if (tx.txType === 'SELL') {
// 计算卖出时的平均成本
let avgCostPerUnit = new Big('0');
// 计算卖出时的平均成本 (Native)
let avgCostPerUnitNative = new Big('0');
if (holding.totalBuyQuantity.gt(0)) {
avgCostPerUnit = holding.totalBuyCostCny.div(holding.totalBuyQuantity);
avgCostPerUnitNative = holding.totalBuyCostNative.div(holding.totalBuyQuantity);
}
// 已实现盈亏 = (卖出价 - 平均成本) * 卖出数量 * 汇率
const sellRevenueCny = new Big(tx.quantity).times(new Big(tx.price));
// 已实现盈亏 = (卖出价 - 平均成本) * 卖出数量 (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);
// 已实现盈亏 (CNY) 保留兼容
let appliedRate = tx.exchangeRate;
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
@@ -226,10 +243,14 @@ export async function getPortfolioPositions(): Promise<Position[]> {
appliedRate = fallbackRate;
}
}
const sellRevenueCnyAdjusted = sellRevenueCny.times(new Big(appliedRate || '1'));
const costBasisCny = avgCostPerUnit.times(new Big(tx.quantity));
const realizedPnl = sellRevenueCnyAdjusted.minus(costBasisCny);
holding.realizedPnlCny = holding.realizedPnlCny.plus(realizedPnl);
const sellRevenueCny = new Big(tx.quantity).times(new Big(tx.price)).times(new Big(appliedRate || '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.quantity = holding.quantity.minus(new Big(tx.quantity));
} else if (tx.txType === 'AIRDROP') {
@@ -259,21 +280,53 @@ export async function getPortfolioPositions(): Promise<Position[]> {
rateMap
);
// 未实现盈亏 = 当前市值 - 总买入成本
// 未实现盈亏 (CNY)
const unrealizedPnlCny = cnyValue.minus(holding.totalBuyCostCny);
// 总盈亏 = 当前市值 - 总买入成本 + 已实现盈亏 + 累计分红
// 总盈亏 (CNY)
const totalPnlCny = unrealizedPnlCny.plus(holding.realizedPnlCny).plus(holding.accumulatedDividendsCny);
const currentNativeValue = new Big(holding.latestPrice).times(holding.quantity);
const pnlNative = currentNativeValue.minus(holding.totalBuyCostNative).plus(holding.accumulatedDividendsNative);
// Native 原生币种计算
const marketValueNative = new Big(holding.latestPrice).times(holding.quantity);
const currentNativeValue = marketValueNative;
// 平均成本 = 总买入成本 / 总买入数量
// 平均成本 (Native) = 总买入成本 (Native) / 总买入数量
let avgCostNative = new Big('0');
if (holding.totalBuyQuantity.gt(0)) {
avgCostNative = holding.totalBuyCostNative.div(holding.totalBuyQuantity);
}
// 摊薄成本 (Native) = (总买入成本 - 已实现盈亏 - 累计分红) / 当前持仓数量
let dilutedCostNative = new Big('0');
if (holding.quantity.gt(0)) {
dilutedCostNative = holding.totalBuyCostNative.minus(holding.realizedPnlNative).minus(holding.accumulatedDividendsNative).div(holding.quantity);
}
// 浮动盈亏 (Native) = 市值 - (平均成本 * 当前持仓数量)
const floatingPnlNative = marketValueNative.minus(avgCostNative.times(holding.quantity));
// 浮动盈亏百分比 (Native)
let floatingPnlPercent = new Big('0');
const avgCostBasisNative = avgCostNative.times(holding.quantity);
if (avgCostBasisNative.gt(0)) {
floatingPnlPercent = floatingPnlNative.div(avgCostBasisNative).times(new Big('100'));
}
// 累计盈亏 (Native) = 浮动盈亏 + 已实现盈亏 + 累计分红
const cumulativePnlNative = floatingPnlNative.plus(holding.realizedPnlNative).plus(holding.accumulatedDividendsNative);
// 累计盈亏百分比 (Native) = 累计盈亏 / 总买入成本 * 100
let cumulativePnlPercent = new Big('0');
if (holding.totalBuyCostNative.gt(0)) {
cumulativePnlPercent = cumulativePnlNative.div(holding.totalBuyCostNative).times(new Big('100'));
}
// 平均成本 (CNY) 保留兼容
let avgCost = new Big('0');
if (holding.totalBuyQuantity.gt(0)) {
avgCost = holding.totalBuyCostCny.div(holding.totalBuyQuantity);
}
// 摊薄成本 = (总买入成本 - 已实现盈亏 - 总分红) / 当前持仓数量
// 摊薄成本 (CNY) 保留兼容
let dilutedCost = new Big('0');
if (holding.quantity.gt(0)) {
dilutedCost = holding.totalBuyCostCny.minus(holding.realizedPnlCny).minus(holding.accumulatedDividendsCny).div(holding.quantity);
@@ -286,6 +339,9 @@ export async function getPortfolioPositions(): Promise<Position[]> {
holdingDays = Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
}
// Native 原生币种总盈亏 (保留兼容)
const pnlNative = marketValueNative.minus(holding.totalBuyCostNative).plus(holding.accumulatedDividendsNative);
result.push({
assetId: holding.assetId,
symbol: holding.symbol,
@@ -304,9 +360,19 @@ export async function getPortfolioPositions(): Promise<Position[]> {
avgCost: avgCost.toString(),
dilutedCost: dilutedCost.toString(),
holdingDays,
exchange: holding.exchange,
exchange: holding.exchange,
accumulatedDividendsCny: holding.accumulatedDividendsCny.toString(),
accumulatedDividendsNative: holding.accumulatedDividendsNative.toString(),
// Native 原生币种盈亏指标
totalBuyCostNative: holding.totalBuyCostNative.toString(),
realizedPnlNative: holding.realizedPnlNative.toString(),
avgCostNative: avgCostNative.toString(),
dilutedCostNative: dilutedCostNative.toString(),
marketValueNative: marketValueNative.toString(),
floatingPnlNative: floatingPnlNative.toString(),
floatingPnlPercent: floatingPnlPercent.toString(),
cumulativePnlNative: cumulativePnlNative.toString(),
cumulativePnlPercent: cumulativePnlPercent.toString(),
});
}