fix(api): 實裝 portfolio 匯率兜底邏輯,並在 transaction 錄入層自動攔截寫入歷史匯率
This commit is contained in:
parent
67ceb63b08
commit
effa84fe14
@ -73,12 +73,13 @@ function calculateCnyValueFromPrice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPortfolioPositions(): Promise<Position[]> {
|
export async function getPortfolioPositions(): Promise<Position[]> {
|
||||||
const allTransactions = await db
|
const allTransactions = await db
|
||||||
.select({
|
.select({
|
||||||
txType: transactions.txType,
|
txType: transactions.txType,
|
||||||
quantity: transactions.quantity,
|
quantity: transactions.quantity,
|
||||||
price: transactions.price,
|
price: transactions.price,
|
||||||
exchangeRate: transactions.exchangeRate,
|
exchangeRate: transactions.exchangeRate,
|
||||||
|
txCurrency: transactions.txCurrency,
|
||||||
assetId: transactions.assetId,
|
assetId: transactions.assetId,
|
||||||
assetSymbol: assets.symbol,
|
assetSymbol: assets.symbol,
|
||||||
assetType: assets.type,
|
assetType: assets.type,
|
||||||
@ -89,6 +90,14 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
.leftJoin(assets, eq(assets.id, transactions.assetId))
|
.leftJoin(assets, eq(assets.id, transactions.assetId))
|
||||||
.orderBy(desc(transactions.executedAt));
|
.orderBy(desc(transactions.executedAt));
|
||||||
|
|
||||||
|
const rates = await db.select({
|
||||||
|
fromCurrency: exchangeRates.fromCurrency,
|
||||||
|
toCurrency: exchangeRates.toCurrency,
|
||||||
|
rate: exchangeRates.rate,
|
||||||
|
}).from(exchangeRates);
|
||||||
|
|
||||||
|
const rateMap = buildRateMap(rates);
|
||||||
|
|
||||||
const holdings = new Map<string, {
|
const holdings = new Map<string, {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@ -123,13 +132,27 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
||||||
const costPerUnit = new Big(tx.quantity).times(new Big(tx.price));
|
const costPerUnit = new Big(tx.quantity).times(new Big(tx.price));
|
||||||
holding.totalCostNative = holding.totalCostNative.plus(costPerUnit);
|
holding.totalCostNative = holding.totalCostNative.plus(costPerUnit);
|
||||||
const costCny = costPerUnit.times(new Big(tx.exchangeRate || '1'));
|
let appliedRate = tx.exchangeRate;
|
||||||
|
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
|
||||||
|
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
|
||||||
|
if (fallbackRate) {
|
||||||
|
appliedRate = fallbackRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const costCny = costPerUnit.times(new Big(appliedRate || '1'));
|
||||||
holding.totalCostCny = holding.totalCostCny.plus(costCny);
|
holding.totalCostCny = holding.totalCostCny.plus(costCny);
|
||||||
} else if (tx.txType === 'SELL') {
|
} else if (tx.txType === 'SELL') {
|
||||||
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
|
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
|
||||||
const sellCostPerUnit = new Big(tx.quantity).times(new Big(tx.price));
|
const sellCostPerUnit = new Big(tx.quantity).times(new Big(tx.price));
|
||||||
holding.totalCostNative = holding.totalCostNative.minus(sellCostPerUnit);
|
holding.totalCostNative = holding.totalCostNative.minus(sellCostPerUnit);
|
||||||
const sellCostCny = sellCostPerUnit.times(new Big(tx.exchangeRate || '1'));
|
let appliedRate = tx.exchangeRate;
|
||||||
|
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
|
||||||
|
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
|
||||||
|
if (fallbackRate) {
|
||||||
|
appliedRate = fallbackRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sellCostCny = sellCostPerUnit.times(new Big(appliedRate || '1'));
|
||||||
holding.totalCostCny = holding.totalCostCny.minus(sellCostCny);
|
holding.totalCostCny = holding.totalCostCny.minus(sellCostCny);
|
||||||
} else if (tx.txType === 'AIRDROP') {
|
} else if (tx.txType === 'AIRDROP') {
|
||||||
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
|
||||||
@ -142,14 +165,6 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rates = await db.select({
|
|
||||||
fromCurrency: exchangeRates.fromCurrency,
|
|
||||||
toCurrency: exchangeRates.toCurrency,
|
|
||||||
rate: exchangeRates.rate,
|
|
||||||
}).from(exchangeRates);
|
|
||||||
|
|
||||||
const rateMap = buildRateMap(rates);
|
|
||||||
|
|
||||||
const result: Position[] = [];
|
const result: Position[] = [];
|
||||||
let totalCnyValue = new Big('0');
|
let totalCnyValue = new Big('0');
|
||||||
let totalPnlCny = new Big('0');
|
let totalPnlCny = new Big('0');
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { transactions, transactionTypeEnum } from '@/db/schema';
|
import { transactions, transactionTypeEnum, exchangeRates } from '@/db/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { eq, desc } from 'drizzle-orm';
|
import { eq, desc, and } from 'drizzle-orm';
|
||||||
|
|
||||||
const createTransactionSchema = z.object({
|
const createTransactionSchema = z.object({
|
||||||
assetId: z.string().uuid(),
|
assetId: z.string().uuid(),
|
||||||
@ -23,7 +23,21 @@ export async function createTransaction(params: z.infer<typeof createTransaction
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [transaction] = await db.insert(transactions).values(validation.data).returning();
|
const data = { ...validation.data };
|
||||||
|
if (data.txCurrency !== 'CNY' && (!data.exchangeRate || data.exchangeRate === '1' || data.exchangeRate === '1.00000000')) {
|
||||||
|
const [latestRate] = await db
|
||||||
|
.select({ rate: exchangeRates.rate })
|
||||||
|
.from(exchangeRates)
|
||||||
|
.where(and(
|
||||||
|
eq(exchangeRates.fromCurrency, data.txCurrency),
|
||||||
|
eq(exchangeRates.toCurrency, 'CNY')
|
||||||
|
))
|
||||||
|
.limit(1);
|
||||||
|
if (latestRate) {
|
||||||
|
data.exchangeRate = latestRate.rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [transaction] = await db.insert(transactions).values(data).returning();
|
||||||
return { success: true, data: transaction };
|
return { success: true, data: transaction };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (
|
if (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user