feat(ledger): 重构盈亏计算引擎,实装摊薄/平均成本双重指标与持仓天数统计

This commit is contained in:
2026-04-28 16:39:22 +08:00
parent bf57002313
commit e093b94157
3 changed files with 154 additions and 46 deletions
+49 -21
View File
@@ -15,11 +15,13 @@ const CHART_COLORS = [
];
export default async function DashboardPage() {
const { positions, totalCnyValue, chartData, totalPnlCny } = await getPortfolioSummary();
const { positions, totalCnyValue, chartData, totalPnlCny, unrealizedPnlCny } = await getPortfolioSummary();
const formattedTotal = formatAmount(totalCnyValue);
const formattedPnl = formatAmount(totalPnlCny);
const pnlIsPositive = new Big(totalPnlCny).gte(0);
const formattedTotalPnl = formatAmount(totalPnlCny);
const formattedUnrealized = formatAmount(unrealizedPnlCny);
const totalPnlIsPositive = new Big(totalPnlCny).gte(0);
const unrealizedIsPositive = new Big(unrealizedPnlCny).gte(0);
const displayChartData = chartData.map((item) => ({
...item,
@@ -50,11 +52,19 @@ export default async function DashboardPage() {
(CNY)
</span>
</div>
<div className="mt-2 flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<span className={`text-lg font-semibold ${pnlIsPositive ? 'text-green-500' : 'text-red-500'}`}>
{pnlIsPositive ? '+' : ''}{formattedPnl}
</span>
<div className="mt-3 flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<span className={`text-lg font-semibold ${unrealizedIsPositive ? 'text-green-500' : 'text-red-500'}`}>
{unrealizedIsPositive ? '+' : ''}{formattedUnrealized}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<span className={`text-lg font-semibold ${totalPnlIsPositive ? 'text-green-500' : 'text-red-500'}`}>
{totalPnlIsPositive ? '+' : ''}{formattedTotalPnl}
</span>
</div>
</div>
</CardContent>
</Card>
@@ -72,6 +82,12 @@ export default async function DashboardPage() {
const posPnlPositive = posPnl.gte(0);
const formattedPosPnl = formatAmount(pos.pnlCny);
const posPnlNative = new Big(pos.pnlNative);
const posPnlNativePositive = posPnlNative.gte(0);
const avgCostFormatted = new Big(pos.avgCost).gt(0) ? formatAmount(pos.avgCost) : '-';
const dilutedCostFormatted = new Big(pos.dilutedCost).gt(0) ? formatAmount(pos.dilutedCost) : '-';
return (
<Card key={pos.assetId}>
<CardHeader>
@@ -97,24 +113,36 @@ export default async function DashboardPage() {
<span className="text-muted-foreground"></span>
<span className="font-medium">{pos.baseCurrency}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
¥{avgCostFormatted}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
¥{dilutedCostFormatted}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
{pos.holdingDays}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"> ({pos.baseCurrency})</span>
<span className="font-medium">
{formatAmount(pos.totalCostNative)}
</span>
</div>
{(() => {
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">
<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="font-medium">
@@ -124,7 +152,7 @@ export default async function DashboardPage() {
<div className="flex justify-between opacity-50">
<span className="text-muted-foreground"> (CNY)</span>
<span className={`font-semibold ${posPnlPositive ? 'text-green-500' : 'text-red-500'}`}>
{posPnlPositive ? '+' : ''}{formatAmount(pos.pnlCny)}
{posPnlPositive ? '+' : ''}{formattedPosPnl}
</span>
</div>
</div>