refactor(ledger): 时光机接入全新财务引擎并清洗脏快照,修复历史成本断层
This commit is contained in:
+40
-16
@@ -5,6 +5,7 @@ import { portfolioSnapshots, transactions, assetPricesHistory, assets, exchangeR
|
||||
import { getPortfolioPositions } from './portfolio';
|
||||
import { and, asc, desc, eq, gte, lte, sql } from 'drizzle-orm';
|
||||
import Big from 'big.js';
|
||||
import { calculateAssetMetrics } from '@/utils/finance';
|
||||
|
||||
function formatDateString(date: Date): string {
|
||||
const yyyy = date.getFullYear();
|
||||
@@ -282,35 +283,58 @@ export async function reconstructPortfolioHistory() {
|
||||
return price;
|
||||
}
|
||||
|
||||
await db.delete(portfolioSnapshots);
|
||||
|
||||
let daysReconstructed = 0;
|
||||
|
||||
while (formatDateString(currentDate) <= todayStr) {
|
||||
const dateStr = formatDateString(currentDate);
|
||||
|
||||
const positions = await getHistoricalPositions(currentDate);
|
||||
const historicalTx = await db
|
||||
.select({
|
||||
assetId: transactions.assetId,
|
||||
executedAt: transactions.executedAt,
|
||||
txType: transactions.txType,
|
||||
quantity: transactions.quantity,
|
||||
price: transactions.price,
|
||||
fee: transactions.fee,
|
||||
})
|
||||
.from(transactions)
|
||||
.where(lte(transactions.executedAt, currentDate))
|
||||
.orderBy(asc(transactions.executedAt));
|
||||
|
||||
let totalValueCny = new Big('0');
|
||||
let totalCostCny = new Big('0');
|
||||
|
||||
for (const pos of positions) {
|
||||
const priceStr = await getEffectivePrice(pos.assetId, currentDate);
|
||||
const baseCurrency = assetBaseCurrencyMap.get(pos.assetId) || 'USD';
|
||||
const uniqueAssetIds = [...new Set(historicalTx.filter(t =>
|
||||
t.txType === 'BUY' || t.txType === 'SELL' || t.txType === 'DIVIDEND'
|
||||
).map(t => t.assetId))];
|
||||
|
||||
for (const assetId of uniqueAssetIds) {
|
||||
const assetTxs = historicalTx
|
||||
.filter(t => t.assetId === assetId && (t.txType === 'BUY' || t.txType === 'SELL' || t.txType === 'DIVIDEND'))
|
||||
.map(t => ({
|
||||
date: new Date(t.executedAt).toISOString().split('T')[0],
|
||||
txType: t.txType,
|
||||
quantity: t.quantity.toString(),
|
||||
price: t.price.toString(),
|
||||
fee: t.fee.toString(),
|
||||
}));
|
||||
|
||||
const priceStr = await getEffectivePrice(assetId, currentDate);
|
||||
const baseCurrency = assetBaseCurrencyMap.get(assetId) || 'USD';
|
||||
|
||||
let cnyPrice: string;
|
||||
if (!priceStr) {
|
||||
const fallbackPrice = assetLatestPriceMap.get(pos.assetId) || '0';
|
||||
const cnyPrice = convertPriceToCny(fallbackPrice, baseCurrency);
|
||||
const price = new Big(cnyPrice);
|
||||
const qty = new Big(pos.quantity);
|
||||
totalValueCny = totalValueCny.plus(price.times(qty));
|
||||
totalCostCny = totalCostCny.plus(pos.totalCost);
|
||||
continue;
|
||||
cnyPrice = convertPriceToCny(assetLatestPriceMap.get(assetId) || '0', baseCurrency);
|
||||
} else {
|
||||
cnyPrice = convertPriceToCny(priceStr, baseCurrency);
|
||||
}
|
||||
|
||||
const cnyPrice = convertPriceToCny(priceStr, baseCurrency);
|
||||
const price = new Big(cnyPrice);
|
||||
const qty = new Big(pos.quantity);
|
||||
totalValueCny = totalValueCny.plus(price.times(qty));
|
||||
totalCostCny = totalCostCny.plus(pos.totalCost);
|
||||
const metrics = calculateAssetMetrics(assetTxs, cnyPrice);
|
||||
|
||||
totalValueCny = totalValueCny.plus(metrics.marketValue);
|
||||
totalCostCny = totalCostCny.plus(metrics.totalInvested);
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
|
||||
@@ -62,6 +62,7 @@ export function calculateAssetMetrics(transactions: TxRecord[], currentPrice: st
|
||||
dilutedCost: dilutedCost.toString(),
|
||||
floatingPnl: floatingPnl.toString(),
|
||||
accumulatedPnl: accumulatedPnl.toString(),
|
||||
marketValue: currentMarketValue.toString()
|
||||
marketValue: currentMarketValue.toString(),
|
||||
totalInvested: totalInvested.toString()
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user