fix(ui): 修复价格尾零、增加币种下拉框,并重构持仓引擎实现原币种/本位币双轨制盈亏计算
This commit is contained in:
parent
746be06840
commit
67ceb63b08
@ -10,6 +10,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
export default async function AssetsPage() {
|
export default async function AssetsPage() {
|
||||||
const assets = await getAssets();
|
const assets = await getAssets();
|
||||||
@ -53,14 +54,14 @@ export default async function AssetsPage() {
|
|||||||
<TableCell className="font-medium">{asset.symbol}</TableCell>
|
<TableCell className="font-medium">{asset.symbol}</TableCell>
|
||||||
<TableCell>{typeLabels[asset.type] || asset.type}</TableCell>
|
<TableCell>{typeLabels[asset.type] || asset.type}</TableCell>
|
||||||
<TableCell>{asset.baseCurrency}</TableCell>
|
<TableCell>{asset.baseCurrency}</TableCell>
|
||||||
<TableCell>{asset.latestPrice}</TableCell>
|
<TableCell>{asset.latestPrice ? new Big(asset.latestPrice).toString() : '-'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{asset.createdAt
|
{asset.createdAt
|
||||||
? new Date(asset.createdAt).toLocaleString('zh-CN')
|
? new Date(asset.createdAt).toLocaleString('zh-CN')
|
||||||
: '-'}
|
: '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<UpdatePriceDialog assetId={asset.id} currentPrice={asset.latestPrice || '0'} />
|
<UpdatePriceDialog assetId={asset.id} currentPrice={asset.latestPrice ? new Big(asset.latestPrice).toString() : '0'} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -90,21 +90,33 @@ export default async function DashboardPage() {
|
|||||||
<span className="font-medium">{pos.baseCurrency}</span>
|
<span className="font-medium">{pos.baseCurrency}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">CNY 估值</span>
|
<span className="text-muted-foreground">持仓成本 ({pos.baseCurrency})</span>
|
||||||
<span className="font-medium text-green-600">
|
<span className="font-medium">
|
||||||
¥{formatAmount(pos.cnyValue)}
|
{formatAmount(pos.totalCostNative)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
{(() => {
|
||||||
|
const posPnlNative = new Big(pos.pnlNative);
|
||||||
|
const posPnlNativePositive = posPnlNative.gte(0);
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">当前盈亏 ({pos.baseCurrency})</span>
|
||||||
|
<span className={`font-semibold ${posPnlNativePositive ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
{posPnlNativePositive ? '+' : ''}{formatAmount(pos.pnlNative)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<div className="flex justify-between opacity-50">
|
||||||
<span className="text-muted-foreground">成本 (CNY)</span>
|
<span className="text-muted-foreground">成本 (CNY)</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
¥{formatAmount(pos.totalCostCny)}
|
¥{formatAmount(pos.totalCostCny)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between opacity-50">
|
||||||
<span className="text-muted-foreground">盈亏</span>
|
<span className="text-muted-foreground">盈亏 (CNY)</span>
|
||||||
<span className={`font-semibold ${posPnlPositive ? 'text-green-500' : 'text-red-500'}`}>
|
<span className={`font-semibold ${posPnlPositive ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
{posPnlPositive ? '+' : ''}{formattedPosPnl}
|
{posPnlPositive ? '+' : ''}{formatAmount(pos.pnlCny)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,8 @@ interface Position {
|
|||||||
cnyValue: string;
|
cnyValue: string;
|
||||||
totalCostCny: string;
|
totalCostCny: string;
|
||||||
pnlCny: string;
|
pnlCny: string;
|
||||||
|
totalCostNative: string;
|
||||||
|
pnlNative: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawRate {
|
interface RawRate {
|
||||||
@ -95,6 +97,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
latestPrice: string;
|
latestPrice: string;
|
||||||
totalCostCny: Big;
|
totalCostCny: Big;
|
||||||
|
totalCostNative: Big;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
for (const tx of allTransactions) {
|
for (const tx of allTransactions) {
|
||||||
@ -110,6 +113,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
baseCurrency: tx.assetBaseCurrency || '',
|
baseCurrency: tx.assetBaseCurrency || '',
|
||||||
latestPrice: tx.assetLatestPrice || '0',
|
latestPrice: tx.assetLatestPrice || '0',
|
||||||
totalCostCny: new Big('0'),
|
totalCostCny: new Big('0'),
|
||||||
|
totalCostNative: new Big('0'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,10 +122,15 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
if (tx.txType === 'BUY') {
|
if (tx.txType === 'BUY') {
|
||||||
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);
|
||||||
const costCny = costPerUnit.times(new Big(tx.exchangeRate || '1'));
|
const costCny = costPerUnit.times(new Big(tx.exchangeRate || '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));
|
||||||
|
holding.totalCostNative = holding.totalCostNative.minus(sellCostPerUnit);
|
||||||
|
const sellCostCny = sellCostPerUnit.times(new Big(tx.exchangeRate || '1'));
|
||||||
|
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));
|
||||||
} else if (tx.txType === 'DIVIDEND') {
|
} else if (tx.txType === 'DIVIDEND') {
|
||||||
@ -160,6 +169,9 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
const pnlCny = cnyValue.minus(holding.totalCostCny);
|
const pnlCny = cnyValue.minus(holding.totalCostCny);
|
||||||
totalPnlCny = totalPnlCny.plus(pnlCny);
|
totalPnlCny = totalPnlCny.plus(pnlCny);
|
||||||
|
|
||||||
|
const currentNativeValue = new Big(holding.latestPrice).times(holding.quantity);
|
||||||
|
const pnlNative = currentNativeValue.minus(holding.totalCostNative);
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
assetId: holding.assetId,
|
assetId: holding.assetId,
|
||||||
symbol: holding.symbol,
|
symbol: holding.symbol,
|
||||||
@ -169,6 +181,8 @@ export async function getPortfolioPositions(): Promise<Position[]> {
|
|||||||
cnyValue: cnyValue.toString(),
|
cnyValue: cnyValue.toString(),
|
||||||
totalCostCny: holding.totalCostCny.toString(),
|
totalCostCny: holding.totalCostCny.toString(),
|
||||||
pnlCny: pnlCny.toString(),
|
pnlCny: pnlCny.toString(),
|
||||||
|
totalCostNative: holding.totalCostNative.toString(),
|
||||||
|
pnlNative: pnlNative.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -218,9 +218,18 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>交易币种</FormLabel>
|
<FormLabel>交易币种</FormLabel>
|
||||||
<FormControl>
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
<Input placeholder="USD" type="text" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择币种" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="USD">USD</SelectItem>
|
||||||
|
<SelectItem value="CNY">CNY</SelectItem>
|
||||||
|
<SelectItem value="HKD">HKD</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user