Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions src/api/history.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { get } from 'svelte/store'

import { DEFAULT_HISTORY_COUNT } from '@lib/config'
import { address, history, lastHistoryItemsCount, historySortKey, historyOrderStatusToShow } from '@lib/stores'
import { getLabelForAsset, getChainData } from '@lib/utils'
import { address, history, lastHistoryItemsCount, historyOrderStatusToShow } from '@lib/stores'
import { getChainData } from '@lib/utils'

export async function getUserHistory(params) {
function getHistoryRequestUrl(params) {

const dataEndpoint = getChainData('dataEndpoint');

Expand All @@ -17,23 +17,41 @@ export async function getUserHistory(params) {

let {
first,
skip,
diff
skip
} = params;

if (!first) first = DEFAULT_HISTORY_COUNT;
if (!skip) skip = 0;

const statusesToShow = get(historyOrderStatusToShow);

const sortKey = get(historySortKey); // [columnName, isDesc]

let sortBy = 'timestamp';
let sortDirection = 'desc';

return `${dataEndpoint}/history/${_address}?chain=arbitrum&limit=${first}&skip=${skip}&sortBy=${sortBy}&sortDirection=${sortDirection}&status=${statusesToShow.join(',')}`;
}

export async function fetchUserHistoryPage(params) {
const url = getHistoryRequestUrl(params);
if (!url) return [];

const response = await fetch(url);
if (!response.ok) throw new Error(`History request failed with status ${response.status}`);

const orders = await response.json() || [];
return Array.isArray(orders) ? orders : [];
}

export async function getUserHistory(params) {

try {
const response = await fetch(`${dataEndpoint}/history/${_address}?chain=arbitrum&limit=${first}&skip=${skip}&sortBy=${sortBy}&sortDirection=${sortDirection}&status=${statusesToShow.join(',')}`);
const orders = await response.json() || [];
if (!params) params = {};
let { first, skip, diff } = params;

if (!first) first = DEFAULT_HISTORY_COUNT;
if (!skip) skip = 0;

const orders = await fetchUserHistoryPage({first, skip});

lastHistoryItemsCount.set(orders.length);

Expand Down Expand Up @@ -66,4 +84,33 @@ export async function getUserHistory(params) {
}

return true;
}
}

export async function getUserHistoryForExport(params) {
if (!params) params = {};

const first = params.first || 250;
const fromTimestamp = params.fromTimestamp;
const toTimestamp = params.toTimestamp;
let skip = 0;
let results = [];

while (true) {
const orders = await fetchUserHistoryPage({first, skip});
if (!orders.length) break;

for (const order of orders) {
const timestamp = order.timestamp * 1;
if (toTimestamp && timestamp > toTimestamp) continue;
if (fromTimestamp && timestamp < fromTimestamp) continue;
results.push(order);
}

if (orders.length < first) break;
if (fromTimestamp && orders.some((order) => order.timestamp * 1 < fromTimestamp)) break;

skip += first;
}

return results;
}
234 changes: 230 additions & 4 deletions src/components/trade/account/History.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Cell from '@components/layout/table/Cell.svelte'

import { onMount, onDestroy } from 'svelte'
import { LOADING_ICON } from '@lib/icons'
import { DOWNLOAD_ICON, LOADING_ICON } from '@lib/icons'

import { DEFAULT_HISTORY_COUNT, DEFAULT_HISTORY_SORT_KEY } from '@lib/config'
import {
Expand All @@ -20,10 +20,11 @@
formatPriceForDisplay
} from '@lib/formatters'
import { address, historySortKey, historySorted, historyColumnsToShow, lastHistoryItemsCount, orders } from '@lib/stores'
import { showModal } from '@lib/ui'
import tooltip from '@lib/tooltip'
import { showToast } from '@lib/ui'
import { saveUserSetting } from '@lib/utils'

import { getUserHistory } from '@api/history'
import { getUserHistory, getUserHistoryForExport } from '@api/history'

export let allColumns;

Expand Down Expand Up @@ -117,6 +118,147 @@
return item.status;
}

let exportFrom = '';
let exportTo = '';
let isExporting = false;

const exportHeaders = [
'Order ID',
'Time',
'Side',
'Market',
'Price',
'Size',
'Margin',
'Leverage',
'Type',
'Reduce-Only',
'Status',
'Reason',
'P/L',
'P/L %',
'Fee',
'Expiry',
'OCO'
];

function getDayStartTimestamp(value) {
if (!value) return;
return Math.floor(new Date(`${value}T00:00:00`).getTime() / 1000);
}

function getDayEndTimestamp(value) {
if (!value) return;
return Math.floor(new Date(`${value}T23:59:59`).getTime() / 1000);
}

function hasExportValue(value) {
return value !== undefined && value !== null && value !== '';
}

function getExportRow(rawItem) {
const item = formatHistoryItem(rawItem);
const pnlValue = item.pnl * 1;
const marginValue = item.margin * 1;
const pnlPercent = Number.isFinite(pnlValue) && Number.isFinite(marginValue) && marginValue !== 0 ? 100 * pnlValue / marginValue : '';

Comment on lines +159 to +164
return [
item.status == 'liquidated' ? 'liq' : item.orderId,
formatDate(item.timestamp) || '',
formatSide(item.isLong, item.isReduceOnly),
formatMarketName(item.market),
item.price * 1 > 0 ? formatPriceForDisplay(item.price) : '',
`${formatForDisplay(item.size)} ${item.asset}`,
`${formatForDisplay(item.margin)} ${item.asset}`,
item.leverage ? `${formatForDisplay(item.leverage)}x` : '',
formatOrderType(item.orderType),
item.isReduceOnly ? 'Yes' : 'No',
item.status,
item.reason || '',
hasExportValue(item.pnl) ? `${formatForDisplay(item.pnl)} ${item.asset}` : '',
hasExportValue(pnlPercent) ? `${formatForDisplay(pnlPercent, true)}%` : '',
hasExportValue(item.fee) ? `${formatForDisplay(item.fee)} ${item.asset}` : '',
formatDate(item.expiry) || '',
Comment on lines +176 to +181
item.cancelOrderId * 1 > 0 ? item.cancelOrderId : ''
];
}

function buildCsvInWorker(headers, rows) {
return new Promise((resolve, reject) => {
const workerSource = `
self.onmessage = function(event) {
const escapeCell = function(value) {
if (value === undefined || value === null) return '';
const text = String(value);
const safeText = /^[=+\\-@\\t\\r]/.test(text) ? "'" + text : text;
if (/[",\\n\\r]/.test(safeText)) return '"' + safeText.replace(/"/g, '""') + '"';
return safeText;
};
const csvRows = [event.data.headers].concat(event.data.rows);
self.postMessage(csvRows.map(function(row) {
return row.map(escapeCell).join(',');
}).join('\\n'));
};
`;
const workerUrl = URL.createObjectURL(new Blob([workerSource], {type: 'text/javascript'}));
const worker = new Worker(workerUrl);
worker.onmessage = (event) => {
worker.terminate();
URL.revokeObjectURL(workerUrl);
resolve(event.data);
};
worker.onerror = (event) => {
worker.terminate();
URL.revokeObjectURL(workerUrl);
reject(event.message);
};
worker.postMessage({headers, rows});
});
}

function downloadCsv(csv) {
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const range = exportFrom || exportTo ? `${exportFrom || 'start'}-${exportTo || 'today'}` : 'all';
link.href = url;
link.download = `cap-trading-history-${range}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}

async function exportHistory() {
if (isExporting) return;

const fromTimestamp = getDayStartTimestamp(exportFrom);
const toTimestamp = getDayEndTimestamp(exportTo);

if (fromTimestamp && toTimestamp && fromTimestamp > toTimestamp) {
showToast('Start date must be before end date.');
return;
}

isExporting = true;
try {
const rows = await getUserHistoryForExport({fromTimestamp, toTimestamp});
if (!rows.length) {
showToast('No history found for this timeframe.', 2);
return;
}

const csv = await buildCsvInWorker(exportHeaders, rows.map(getExportRow));
downloadCsv(csv);
showToast('Trading history exported.', 1);
} catch(e) {
console.error('/history export error', e);
showToast('Could not export trading history.');
} finally {
isExporting = false;
}
}

</script>

<style>
Expand Down Expand Up @@ -234,13 +376,95 @@
.wrapper {
max-height: calc(var(--account-height) - 50px - 39px);
}
.history-panel {
height: 100%;
display: flex;
flex-direction: column;
}
.export-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 25px;
border-bottom: 1px solid var(--layer100);
background-color: var(--layer25);
}
.export-toolbar label {
display: flex;
align-items: center;
gap: 7px;
color: var(--text2);
font-size: 80%;
}
.export-toolbar input {
height: 28px;
border: 1px solid var(--layer200);
border-radius: 5px;
background-color: var(--layer0);
color: var(--text0);
padding: 0 8px;
font-size: 12px;
}
.export-toolbar button {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
min-width: 30px;
border: none;
border-radius: 5px;
background-color: var(--layer100);
color: var(--text0);
cursor: pointer;
}
.export-toolbar button:hover:not(:disabled) {
background-color: var(--layer200);
}
.export-toolbar button:disabled {
cursor: default;
opacity: .6;
}
.export-toolbar button :global(svg) {
width: 15px;
fill: currentColor;
}
.table-shell {
flex: 1;
min-height: 0;
}
@media all and (max-width: 600px) {
.export-toolbar {
align-items: stretch;
flex-wrap: wrap;
}
.export-toolbar label {
flex: 1 1 120px;
}
.export-toolbar input {
width: 100%;
}
.wrapper {
max-height: 100%;
}
}
</style>

<div class='history-panel'>
<div class='export-toolbar'>
<label>From <input type='date' bind:value={exportFrom} aria-label='Export history from date'></label>
<label>To <input type='date' bind:value={exportTo} aria-label='Export history to date'></label>
<button
on:click={exportHistory}
disabled={isExporting || isLoading}
aria-label={isExporting ? 'Exporting history as CSV' : 'Export history as CSV'}
aria-busy={isExporting}
use:tooltip={{content: 'Export history as CSV'}}
>
{#if isExporting}{@html LOADING_ICON}{:else}{@html DOWNLOAD_ICON}{/if}
</button>
</div>

<div class='table-shell'>
<Table
defaultSortKey={DEFAULT_HISTORY_SORT_KEY}
bind:sortKey={$historySortKey}
Expand Down Expand Up @@ -331,6 +555,8 @@
</div>

</Table>
</div>
</div>

<!-- {#if !isLoading && formattedHistory.length == 0}
<div class='empty'>Nothing to show.</div>
Expand Down Expand Up @@ -426,4 +652,4 @@
{#if loadingMore}
<div class='loading-more'>{@html LOADING_ICON}</div>
{/if}
-->
-->
7 changes: 6 additions & 1 deletion src/lib/icons.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.