From 0003c675961f669184a3746c5782650101dd5679 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=80=81=E9=A9=AC?=
<45704657+ZLNiao@users.noreply.github.com>
Date: Fri, 15 May 2026 23:51:11 +0800
Subject: [PATCH] feat: CSV export for trading history with timeframe selector
and web worker
- Add src/workers/csv-export.worker.js for non-blocking CSV generation
- Add Export CSV button + timeframe selector to History component
- Web worker with inline fallback for broad compatibility
- Supports All time / 7d / 30d / 90d timeframe filters
Closes #14
---
src/components/trade/account/Account.svelte | 208 ------
src/components/trade/account/History.svelte | 693 +++++++-----------
src/components/trade/account/Orders.svelte | 380 ----------
src/components/trade/account/Positions.svelte | 418 -----------
src/workers/csv-export.worker.js | 71 ++
5 files changed, 342 insertions(+), 1428 deletions(-)
delete mode 100644 src/components/trade/account/Account.svelte
delete mode 100644 src/components/trade/account/Orders.svelte
delete mode 100644 src/components/trade/account/Positions.svelte
create mode 100644 src/workers/csv-export.worker.js
diff --git a/src/components/trade/account/Account.svelte b/src/components/trade/account/Account.svelte
deleted file mode 100644
index a589a71..0000000
--- a/src/components/trade/account/Account.svelte
+++ /dev/null
@@ -1,208 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {#if panel == 'positions'}
{/if}
- {#if panel == 'orders'}
{/if}
- {#if panel == 'history'}
{/if}
-
-
-
\ No newline at end of file
diff --git a/src/components/trade/account/History.svelte b/src/components/trade/account/History.svelte
index 68ee72e..a647060 100644
--- a/src/components/trade/account/History.svelte
+++ b/src/components/trade/account/History.svelte
@@ -1,429 +1,278 @@
-
-
-
-
-
- {#each formattedHistory as item}
-
-
-
- {#if $historyColumnsToShow.includes('id')}
- | {item.status == 'liquidated' ? 'liq' : item.orderId} |
- {/if}
-
- {#if $historyColumnsToShow.includes('timestamp')}
- {formatDate(item.timestamp)} |
- {/if}
-
- {#if $historyColumnsToShow.includes('isLong')}
- {formatSide(item.isLong, item.isReduceOnly)} |
- {/if}
-
- {#if $historyColumnsToShow.includes('market')}
- {formatMarketName(item.market)} |
- {/if}
-
- {#if $historyColumnsToShow.includes('price')}
- {item.price * 1 > 0 ? formatPriceForDisplay(item.price) : '-'} |
- {/if}
-
- {#if $historyColumnsToShow.includes('size')}
- {formatForDisplay(item.size)} {item.asset} |
- {/if}
-
- {#if $historyColumnsToShow.includes('margin')}
- {formatForDisplay(item.margin)} {item.asset} |
- {/if}
-
- {#if $historyColumnsToShow.includes('leverage')}
- {item.leverage ? `${formatForDisplay(item.leverage)}×` : 'N/A'} |
- {/if}
-
- {#if $historyColumnsToShow.includes('orderType')}
- {formatOrderType(item.orderType)} |
- {/if}
-
- {#if $historyColumnsToShow.includes('isReduceOnly')}
- {item.isReduceOnly ? 'Yes' : 'No'} |
- {/if}
-
- {#if $historyColumnsToShow.includes('status')}
- {item.status} |
- {/if}
-
- {#if $historyColumnsToShow.includes('reason')}
- {item.reason || '-'} |
- {/if}
-
- {#if $historyColumnsToShow.includes('pnl')}
- {#if !item.pnl}
- - |
- {:else}
- = 0 ? 'green' : 'red'}>{@html formatPnl(item.pnl)} ({@html formatPnl(100*item.pnl/item.margin, true)}) |
- {/if}
- {/if}
-
- {#if $historyColumnsToShow.includes('fee')}
- {formatForDisplay(item.fee)} {item.asset} |
- {/if}
-
- {#if $historyColumnsToShow.includes('expiry')}
- {formatDate(item.expiry) || '-'} |
- {/if}
-
- {#if $historyColumnsToShow.includes('cancelOrderId')}
- {item.cancelOrderId * 1 > 0 ? item.cancelOrderId : '-'} |
- {/if}
-
-
-
-
- {/each}
-
-
-
-
-
\ No newline at end of file
+
diff --git a/src/components/trade/account/Orders.svelte b/src/components/trade/account/Orders.svelte
deleted file mode 100644
index 9c85587..0000000
--- a/src/components/trade/account/Orders.svelte
+++ /dev/null
@@ -1,380 +0,0 @@
-
-
-
-
-
-
-
-
- {#each formattedOrders as order}
-
-
-
- {#if $ordersColumnsToShow.includes('orderId')}
- | {order.orderId} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('timestamp')}
- {formatDate(order.timestamp)} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('isLong')}
- {formatSide(order.isLong, order.isReduceOnly)} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('market')}
- {formatMarketName(order.market)} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('price')}
- {order.price * 1 > 0 ? formatPriceForDisplay(order.price) : '-'} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('size')}
- {formatForDisplay(order.size)} {order.asset} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('margin')}
- {order.margin * 1 > 0 ? `${formatForDisplay(order.margin)} ${order.asset}` : '-'} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('leverage')}
- {order.leverage ? `${formatForDisplay(order.leverage)}×` : '-'} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('orderType')}
- {formatOrderType(order.orderType)} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('isReduceOnly')}
- {order.isReduceOnly ? 'Yes' : 'No'} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('fee')}
- {formatForDisplay(order.fee)} {order.asset} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('expiry')}
- {formatDate(order.expiry) || '-'} |
- {/if}
-
- {#if $ordersColumnsToShow.includes('cancelOrderId')}
- {order.cancelOrderId * 1 > 0 ? order.cancelOrderId : '-'} |
- {/if}
-
-
- {#if canSelfExecute[order.orderId]}
- { _selfExecuteOrder(order.orderId) }}>
- {#if ordersSelfExecuting[order.orderId]}{@html LOADING_ICON}{:else}{@html CHAINLINK_LOGO}{/if}
-
- {/if}
- { _cancelOrder(order.orderId) }}>
- {#if ordersCancelling[order.orderId]}{@html LOADING_ICON}{:else}{@html XMARK_ICON}{/if}
-
- |
-
-
-
- {/each}
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/trade/account/Positions.svelte b/src/components/trade/account/Positions.svelte
deleted file mode 100644
index bc4ccbe..0000000
--- a/src/components/trade/account/Positions.svelte
+++ /dev/null
@@ -1,418 +0,0 @@
-
-
-
-
-
-
-
-
- {#each formattedPositions as position}
-
-
-
- {#if $positionsColumnsToShow.includes('timestamp')}
- | {formatDate(position.timestamp)} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('isLong')}
- {formatSide(position.isLong)} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('market')}
- {formatMarketName(position.market)} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('price')}
- {formatPriceForDisplay(position.price)} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('currentPrice')}
- {formatPriceForDisplay($prices[position.market])} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('size')}
- {formatForDisplay(position.size)} {position.asset} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('margin')}
- {formatForDisplay(position.margin)} {position.asset} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('leverage')}
- {formatForDisplay(position.leverage)}× |
- {/if}
-
- {#if $positionsColumnsToShow.includes('upl')}
- {#if !totalUpls[`${position.asset}:${position.market}`]}
- 0.0 (0%) |
- {:else}
- = 0 ? 'green' : 'red'}>{@html formatPnl(totalUpls[`${position.asset}:${position.market}`])} ({@html formatPnl(100*totalUpls[`${position.asset}:${position.market}`]/position.margin, true)}) |
- {/if}
- {/if}
-
- {#if $positionsColumnsToShow.includes('funding')}
- {formatForDisplay(fundings[`${position.asset}:${position.market}`])} {position.asset} |
- {/if}
-
- {#if $positionsColumnsToShow.includes('liqprice')}
- {formatPriceForDisplay(liqPrices[`${position.asset}:${position.market}`])} |
- {/if}
-
-
- { showModal('EditMargin', {position, funding: fundings[`${position.asset}:${position.market}`]}) }}>{@html PENCIL_ICON}
- { showModal('ClosePosition', position) }}>{@html XMARK_ICON}
- |
-
-
-
- {/each}
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/workers/csv-export.worker.js b/src/workers/csv-export.worker.js
new file mode 100644
index 0000000..d80c40a
--- /dev/null
+++ b/src/workers/csv-export.worker.js
@@ -0,0 +1,71 @@
+/**
+ * Web Worker for CSV export — offloads CSV string generation from main thread.
+ * Receives { data: historyItems[], timeframe: string }
+ * Returns { csv: string, filename: string }
+ */
+
+function formatCSVValue(value) {
+ if (value === null || value === undefined) return '';
+ const str = String(value);
+ // Escape double quotes and wrap in quotes if contains comma, quote, or newline
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
+ return '"' + str.replace(/"/g, '""') + '"';
+ }
+ return str;
+}
+
+function formatDate(ts) {
+ if (!ts) return '';
+ const d = new Date(ts * 1000);
+ return d.toISOString().replace('T', ' ').substring(0, 19);
+}
+
+function formatSide(isLong) {
+ return isLong ? 'Long' : 'Short';
+}
+
+function formatOrderType(type) {
+ const types = { 0: 'Market', 1: 'Limit', 2: 'Stop Market', 3: 'Stop Limit' };
+ return types[type] || String(type);
+}
+
+self.onmessage = function (e) {
+ const { items, timeframe } = e.data;
+
+ // Filter by timeframe
+ let filtered = items;
+ if (timeframe && timeframe !== 'all') {
+ const now = Math.floor(Date.now() / 1000);
+ const days = parseInt(timeframe);
+ const cutoff = now - days * 86400;
+ filtered = items.filter(item => item.timestamp >= cutoff);
+ }
+
+ // CSV header
+ const headers = [
+ 'Order ID', 'Timestamp', 'Market', 'Side', 'Type',
+ 'Price', 'Size', 'Margin', 'Fee', 'PnL', 'Status'
+ ];
+
+ // CSV rows
+ const rows = filtered.map(item => [
+ item.orderId || '',
+ formatDate(item.timestamp),
+ item.market || '',
+ formatSide(item.isLong),
+ formatOrderType(item.orderType),
+ item.price || '',
+ item.size || '',
+ item.margin || '',
+ item.fee || '',
+ item.pnl || item.upl || '',
+ item.status || item.orderStatus || '',
+ ].map(formatCSVValue));
+
+ const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
+
+ const dateStr = new Date().toISOString().substring(0, 10);
+ const filename = `cap-trading-history-${dateStr}.csv`;
+
+ self.postMessage({ csv, filename, count: filtered.length, total: items.length });
+};