feat(ledger): 编写时光机核心辅助函数,支持历史持仓计算与价格断点结转
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { portfolioSnapshots } from '@/db/schema';
|
||||
import { portfolioSnapshots, transactions, assetPricesHistory } from '@/db/schema';
|
||||
import { getPortfolioPositions } from './portfolio';
|
||||
import { desc, eq, gte, sql } from 'drizzle-orm';
|
||||
import { asc, desc, eq, gte, lte, sql } from 'drizzle-orm';
|
||||
import Big from 'big.js';
|
||||
|
||||
function getTodayInShanghai(): string {
|
||||
@@ -105,3 +105,95 @@ export async function getSnapshots(params?: {
|
||||
|
||||
return snapshots.reverse();
|
||||
}
|
||||
|
||||
interface HistoricalPosition {
|
||||
assetId: string;
|
||||
quantity: string;
|
||||
totalCost: string;
|
||||
}
|
||||
|
||||
export async function getHistoricalPositions(targetDate: Date): Promise<HistoricalPosition[]> {
|
||||
const dateStr = targetDate.toISOString().split('T')[0];
|
||||
|
||||
const allTransactions = await db
|
||||
.select({
|
||||
assetId: transactions.assetId,
|
||||
txType: transactions.txType,
|
||||
quantity: transactions.quantity,
|
||||
price: transactions.price,
|
||||
exchangeRate: transactions.exchangeRate,
|
||||
executedAt: transactions.executedAt,
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
transactions.executedAt.lte(targetDate)
|
||||
)
|
||||
.orderBy(asc(transactions.executedAt));
|
||||
|
||||
const holdings = new Map<string, {
|
||||
quantity: Big;
|
||||
totalCost: Big;
|
||||
}>();
|
||||
|
||||
for (const tx of allTransactions) {
|
||||
if (!tx.assetId) continue;
|
||||
|
||||
const existing = holdings.get(tx.assetId);
|
||||
if (!existing) {
|
||||
holdings.set(tx.assetId, {
|
||||
quantity: new Big('0'),
|
||||
totalCost: new Big('0'),
|
||||
});
|
||||
}
|
||||
|
||||
const holding = holdings.get(tx.assetId)!;
|
||||
const qty = new Big(tx.quantity);
|
||||
|
||||
if (tx.txType === 'BUY') {
|
||||
holding.quantity = holding.quantity.plus(qty);
|
||||
const cost = qty.times(new Big(tx.price)).times(new Big(tx.exchangeRate || '1'));
|
||||
holding.totalCost = holding.totalCost.plus(cost);
|
||||
} else if (tx.txType === 'SELL') {
|
||||
let avgCostPerUnit = new Big('0');
|
||||
if (holding.quantity.gt(0)) {
|
||||
avgCostPerUnit = holding.totalCost.div(holding.quantity);
|
||||
}
|
||||
const sellCost = avgCostPerUnit.times(qty);
|
||||
holding.quantity = holding.quantity.minus(qty);
|
||||
holding.totalCost = holding.totalCost.minus(sellCost);
|
||||
} else if (tx.txType === 'AIRDROP') {
|
||||
holding.quantity = holding.quantity.plus(qty);
|
||||
}
|
||||
}
|
||||
|
||||
const result: HistoricalPosition[] = [];
|
||||
for (const [assetId, holding] of holdings) {
|
||||
if (holding.quantity.lte(0)) continue;
|
||||
result.push({
|
||||
assetId,
|
||||
quantity: holding.quantity.toString(),
|
||||
totalCost: holding.totalCost.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getEffectivePrice(
|
||||
assetId: string,
|
||||
targetDate: Date
|
||||
): Promise<string | null> {
|
||||
const dateStr = targetDate.toISOString().split('T')[0];
|
||||
|
||||
const [record] = await db
|
||||
.select({
|
||||
price: assetPricesHistory.price,
|
||||
})
|
||||
.from(assetPricesHistory)
|
||||
.where(eq(assetPricesHistory.assetId, assetId))
|
||||
.where(lte(assetPricesHistory.date, dateStr))
|
||||
.orderBy(desc(assetPricesHistory.date))
|
||||
.limit(1);
|
||||
|
||||
return record?.price ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user