feat(dashboard): 构建基于 Big.js 的持仓聚合引擎与总览卡片

This commit is contained in:
2026-04-27 22:52:15 +08:00
parent 978d8a591e
commit 796889754e
2 changed files with 116 additions and 9 deletions
+68
View File
@@ -0,0 +1,68 @@
'use server';
import { db } from '@/db';
import { transactions, assets } from '@/db/schema';
import Big from 'big.js';
import { desc } from 'drizzle-orm';
export async function getPortfolioPositions() {
const allTransactions = await db
.select({
txType: transactions.txType,
quantity: transactions.quantity,
assetId: transactions.assetId,
assetSymbol: assets.symbol,
assetType: assets.type,
assetBaseCurrency: assets.baseCurrency,
})
.from(transactions)
.leftJoin(assets, assets.id.eq(transactions.assetId))
.orderBy(desc(transactions.executedAt));
const holdings = new Map<string, {
assetId: string;
symbol: string;
type: string;
quantity: Big;
baseCurrency: string;
}>();
for (const tx of allTransactions) {
if (!tx.assetId) continue;
const existing = holdings.get(tx.assetId);
if (!existing) {
holdings.set(tx.assetId, {
assetId: tx.assetId,
symbol: tx.assetSymbol || tx.assetId,
type: tx.assetType || 'CASH',
quantity: new Big('0'),
baseCurrency: tx.assetBaseCurrency || '',
});
}
const holding = holdings.get(tx.assetId)!;
if (tx.txType === 'BUY' || tx.txType === 'AIRDROP') {
holding.quantity = holding.quantity.plus(tx.quantity);
} else if (tx.txType === 'SELL') {
holding.quantity = holding.quantity.minus(tx.quantity);
} else if (tx.txType === 'DIVIDEND') {
holding.quantity = holding.quantity.plus(tx.quantity);
}
}
const result = [];
for (const [_, holding] of holdings) {
if (holding.quantity.lte(0)) continue;
result.push({
assetId: holding.assetId,
symbol: holding.symbol,
type: holding.type,
quantity: holding.quantity.toString(),
baseCurrency: holding.baseCurrency,
});
}
return result;
}