Skip to content

Commit 439ebcf

Browse files
committed
Improve vault returns and fees table
1 parent 983dbc2 commit 439ebcf

1 file changed

Lines changed: 127 additions & 87 deletions

File tree

src/routes/trading-view/vaults/[vault=slug]/VaultMetrics.svelte

Lines changed: 127 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
<!--
2+
@component
3+
Displays supplementary vault information, including transaction status, performance, fees, and risk metrics.
4+
-->
15
<script lang="ts">
2-
import type { VaultFees, VaultInfo } from '$lib/top-vaults/schemas';
6+
import type { VaultInfo } from '$lib/top-vaults/schemas';
37
import MetricsBox from '$lib/components/MetricsBox.svelte';
48
import Tooltip from '$lib/components/Tooltip.svelte';
59
import Risk from '$lib/top-vaults/Risk.svelte';
610
import Metric from './Metric.svelte';
7-
import IconQuestionCircle from '~icons/local/question-circle';
811
import { getFormattedLockup, isGoodVaultStatus } from '$lib/top-vaults/helpers';
912
import { formatNumber, formatPercent, formatPercentProfit } from '$lib/helpers/formatters';
1013
import { getChain } from '$lib/helpers/chain';
@@ -52,6 +55,48 @@
5255
let lifetimeMaxDrawdown = $derived(
5356
vault.period_results?.find((p) => p.period.toLowerCase() === 'lifetime')?.max_drawdown ?? null
5457
);
58+
let periodResults = $derived(new Map(vault.period_results.map((metrics) => [metrics.period.toLowerCase(), metrics])));
59+
let returnPeriods = $derived([
60+
{ label: '1M', gross: vault.one_month_returns, net: vault.one_month_returns_net },
61+
{ label: '3M', gross: vault.three_months_returns, net: vault.three_months_returns_net },
62+
{
63+
label: '6M',
64+
gross: periodResults.get('6m')?.returns_gross ?? null,
65+
net: periodResults.get('6m')?.returns_net ?? null
66+
},
67+
{
68+
label: '1Y',
69+
gross: periodResults.get('1y')?.returns_gross ?? null,
70+
net: periodResults.get('1y')?.returns_net ?? null
71+
},
72+
{ label: 'Lifetime', gross: vault.lifetime_return, net: vault.lifetime_return_net }
73+
]);
74+
let feeRows = $derived([
75+
{
76+
label: 'Performance fee',
77+
mobileLabel: 'Performance',
78+
value: vault.net_fees?.performance,
79+
tooltip: '<strong>Performance fee</strong> is charged against investment profits.'
80+
},
81+
{
82+
label: 'Management fee',
83+
mobileLabel: 'Management',
84+
value: vault.net_fees?.management,
85+
tooltip: '<strong>Management fee</strong> is charged annually for managing the vault.'
86+
},
87+
{
88+
label: 'Deposit fee',
89+
mobileLabel: 'Deposit',
90+
value: vault.net_fees?.deposit,
91+
tooltip: '<strong>Deposit fee</strong> is a one-time fee applied when entering the vault.'
92+
},
93+
{
94+
label: 'Withdrawal fee',
95+
mobileLabel: 'Withdrawal',
96+
value: vault.net_fees?.withdraw,
97+
tooltip: '<strong>Withdrawal fee</strong> is a one-time fee applied when exiting the vault.'
98+
}
99+
]);
55100
</script>
56101

57102
<div class="additional-metrics">
@@ -156,96 +201,65 @@
156201
</div>
157202
</MetricsBox>
158203

159-
<MetricsBox class="returns" title="Returns">
160-
<table class="vault-metrics-table">
161-
<thead>
162-
<tr>
163-
<th></th>
164-
<th>1 month</th>
165-
<th>3 month</th>
166-
<th>Lifetime</th>
167-
</tr>
168-
</thead>
169-
<tbody>
170-
{#snippet returnsCell(ann: MaybeNumber, abs: MaybeNumber)}
171-
<td class="returns-cell">
172-
<div class="ann">{formatPercentProfit(ann)} ann</div>
173-
<div class="abs">{formatPercentProfit(abs)} abs</div>
174-
</td>
175-
{/snippet}
176-
<tr>
177-
<td>Gross</td>
178-
{@render returnsCell(vault.one_month_cagr, vault.one_month_returns)}
179-
{@render returnsCell(vault.three_months_cagr, vault.three_months_returns)}
180-
{@render returnsCell(vault.cagr, vault.lifetime_return)}
181-
</tr>
182-
<tr>
183-
<td>
204+
<MetricsBox class="performance" title="Returns and fees">
205+
<div class="table-scroll">
206+
<table class="vault-metrics-table">
207+
<thead>
208+
<tr>
209+
<th></th>
210+
{#each returnPeriods as period}
211+
<th scope="col">{period.label}</th>
212+
{/each}
213+
</tr>
214+
</thead>
215+
<tbody>
216+
{#snippet metricLabel(tooltip: string, label: string, mobileLabel?: string)}
184217
<Tooltip>
185218
<span slot="trigger">
186-
<span class="underline">Net</span>
187-
<IconQuestionCircle />
219+
<span class="underline">
220+
<span class="label-desktop">{label}</span>
221+
<span class="label-mobile">{mobileLabel ?? label}</span>
222+
</span>
188223
</span>
189224
<svelte:fragment slot="popup">
190-
For comparing the profitability of vaults, the vault share price is reduced by the calculated net fees
191-
for the investment period.
225+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
226+
{@html tooltip}
192227
</svelte:fragment>
193228
</Tooltip>
194-
</td>
195-
{@render returnsCell(vault.one_month_cagr_net, vault.one_month_returns_net)}
196-
{@render returnsCell(vault.three_months_cagr_net, vault.three_months_returns_net)}
197-
{@render returnsCell(vault.cagr_net, vault.lifetime_return_net)}
198-
</tr>
199-
</tbody>
200-
</table>
201-
</MetricsBox>
202-
203-
<MetricsBox class="fees" title="Fees">
204-
<table class="vault-metrics-table">
205-
<thead>
206-
<tr>
207-
<th></th>
208-
<th>Manage&shy;ment</th>
209-
<th>Perform&shy;ance</th>
210-
<th>Deposit</th>
211-
<th>Withdraw</th>
212-
</tr>
213-
</thead>
214-
<tbody>
215-
{#snippet feeRow(label: string, tooltip: string, fees: VaultFees | null)}
229+
{/snippet}
216230
<tr>
217-
<td class="fee-type-cell">
218-
<Tooltip>
219-
<span slot="trigger">
220-
<span class="underline">{label}</span>
221-
<IconQuestionCircle />
222-
</span>
223-
<svelte:fragment slot="popup">
224-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
225-
{@html tooltip}
226-
</svelte:fragment>
227-
</Tooltip>
231+
<td>
232+
{@render metricLabel(
233+
'<strong>Gross returns</strong> reflect the change in the vault share price before investor-facing fees are deducted.',
234+
'Gross'
235+
)}
236+
</td>
237+
{#each returnPeriods as period}
238+
<td>{formatPercentProfit(period.gross)}</td>
239+
{/each}
240+
</tr>
241+
{#each feeRows as fee}
242+
<tr class="fee-row">
243+
<td>{@render metricLabel(fee.tooltip, fee.label, fee.mobileLabel)}</td>
244+
{#each returnPeriods as _}
245+
<td>{formatPercent(fee.value)}</td>
246+
{/each}
247+
</tr>
248+
{/each}
249+
<tr>
250+
<td>
251+
{@render metricLabel(
252+
'<strong>Net returns</strong> are the gross returns after all investor-facing fees have been deducted.',
253+
'Net'
254+
)}
228255
</td>
229-
<td>{formatPercent(fees?.management)}</td>
230-
<td>{formatPercent(fees?.performance)}</td>
231-
<td>{formatPercent(fees?.deposit)}</td>
232-
<td>{formatPercent(fees?.withdraw)}</td>
256+
{#each returnPeriods as period}
257+
<td>{formatPercentProfit(period.net)}</td>
258+
{/each}
233259
</tr>
234-
{/snippet}
235-
236-
{@render feeRow(
237-
'Gross',
238-
'<strong>Gross fees</strong> are what vaults track internally. They are not exposed to an investor, and only useful for internal profit calculations of the vault. Gross fees have already been deducted when the vault share price is updated.',
239-
vault.gross_fees
240-
)}
241-
242-
{@render feeRow(
243-
'Net',
244-
'<strong>Net fees</strong> are deduced at a redemption. A vault investor receives less than the value of their shares back.',
245-
vault.net_fees
246-
)}
247-
</tbody>
248-
</table>
260+
</tbody>
261+
</table>
262+
</div>
249263
</MetricsBox>
250264
</div>
251265

@@ -259,7 +273,7 @@
259273
@media (--viewport-lg-up) {
260274
grid-template-columns: 1fr 1fr;
261275
262-
:global(:is(.other-metrics, .transaction-status)) {
276+
:global(:is(.other-metrics, .transaction-status, .performance)) {
263277
grid-column: span 2;
264278
}
265279
}
@@ -308,6 +322,7 @@
308322
309323
.vault-metrics-table {
310324
width: 100%;
325+
min-width: 44rem;
311326
border-collapse: collapse;
312327
font: var(--f-ui-md-roman);
313328
--cell-padding: 0.5rem;
@@ -343,12 +358,23 @@
343358
}
344359
}
345360
346-
.abs {
347-
padding-top: 0.25em;
348-
color: var(--c-text-extra-light);
361+
@media (--viewport-sm-down) {
362+
:is(th, td):first-child {
363+
width: 8.75rem;
364+
min-width: 8.75rem;
365+
white-space: nowrap;
366+
}
367+
368+
td:first-child [slot='trigger'] {
369+
min-width: 0;
370+
}
349371
}
350372
}
351373
374+
.table-scroll {
375+
overflow-x: auto;
376+
}
377+
352378
@media (--viewport-md-up) {
353379
:global(.popup) {
354380
max-width: 30rem;
@@ -361,6 +387,20 @@
361387
gap: 0.75ex;
362388
}
363389
390+
.label-mobile {
391+
display: none;
392+
}
393+
394+
@media (--viewport-sm-down) {
395+
.label-desktop {
396+
display: none;
397+
}
398+
399+
.label-mobile {
400+
display: inline;
401+
}
402+
}
403+
364404
.risk-link,
365405
a.denomination-link,
366406
.glossary-link {

0 commit comments

Comments
 (0)