feat(ledger): 接入计算引擎,实现 Dashboard 指标的精确数据对齐
This commit is contained in:
+19
-37
@@ -4,6 +4,7 @@ import { db } from '@/db';
|
||||
import { transactions, assets, exchangeRates } from '@/db/schema';
|
||||
import Big from 'big.js';
|
||||
import { asc, eq } from 'drizzle-orm';
|
||||
import { calculateAssetMetrics } from '@/utils/finance';
|
||||
|
||||
interface Position {
|
||||
assetId: string;
|
||||
@@ -312,54 +313,35 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
||||
// 总盈亏 (CNY)
|
||||
const totalPnlCny = unrealizedPnlCny.plus(holding.realizedPnlCny).plus(holding.accumulatedDividendsCny);
|
||||
|
||||
// Native 原生币种计算
|
||||
const marketValueNative = new Big(holding.latestPrice).times(holding.quantity);
|
||||
const currentNativeValue = marketValueNative;
|
||||
const metrics = calculateAssetMetrics(
|
||||
holding.transactions.map(tx => ({
|
||||
date: tx.executedAt ?? new Date(),
|
||||
txType: tx.txType,
|
||||
quantity: tx.quantity,
|
||||
price: tx.price,
|
||||
fee: tx.fee,
|
||||
})),
|
||||
holding.latestPrice
|
||||
);
|
||||
|
||||
// 平均成本 (Native) = 总买入成本 (Native) / 总买入数量
|
||||
let avgCostNative = new Big('0');
|
||||
if (holding.totalBuyQuantity.gt(0)) {
|
||||
avgCostNative = holding.totalBuyCostNative.div(holding.totalBuyQuantity);
|
||||
}
|
||||
const holdingNative = new Big(metrics.holdings);
|
||||
const avgCostNative = new Big(metrics.averageCost);
|
||||
const dilutedCostNative = new Big(metrics.dilutedCost);
|
||||
const floatingPnlNative = new Big(metrics.floatingPnl);
|
||||
const cumulativePnlNative = new Big(metrics.accumulatedPnl);
|
||||
const marketValueNative = new Big(metrics.marketValue);
|
||||
|
||||
// 摊薄成本 (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);
|
||||
const avgCostBasisNative = avgCostNative.times(holdingNative);
|
||||
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);
|
||||
}
|
||||
|
||||
// 持仓天数
|
||||
let holdingDays = 0;
|
||||
if (holding.firstBuyDate) {
|
||||
const diffMs = today.getTime() - holding.firstBuyDate.getTime();
|
||||
@@ -374,7 +356,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
||||
symbol: holding.symbol,
|
||||
name: holding.name,
|
||||
type: holding.type,
|
||||
quantity: holding.quantity.toString(),
|
||||
quantity: holdingNative.toString(),
|
||||
baseCurrency: holding.baseCurrency,
|
||||
cnyValue: cnyValue.toString(),
|
||||
totalCostCny: holding.totalBuyCostCny.toString(),
|
||||
|
||||
Reference in New Issue
Block a user