From 05899d5afe174cc7f0bd35d18d85100bb89f01f0 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Fri, 24 Jul 2026 15:40:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20close=20/=20close-all=20buttons=20in=20?= =?UTF-8?q?the=20terminal=20'Current=20positions=20=C2=B7=20live'=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one-click close only existed on the legacy dashboard's positions table; the terminal dashboard the user actually sees had a read-only positions panel. Adds a per-row 'close' button (confirm -> market close via the existing /close-position API) and a 'close all' button in the panel header (single confirm, sequential closes to avoid nonce races, partial-failure report). Hidden in demo mode and when no trader is selected; SWR keys refresh positions and account after closing. --- .../components/terminal/TerminalDashboard.tsx | 98 ++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/web/src/components/terminal/TerminalDashboard.tsx b/web/src/components/terminal/TerminalDashboard.tsx index 151ec58d..42208f6c 100644 --- a/web/src/components/terminal/TerminalDashboard.tsx +++ b/web/src/components/terminal/TerminalDashboard.tsx @@ -1,8 +1,9 @@ import { useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import type { CSSProperties } from 'react' -import useSWR from 'swr' +import useSWR, { mutate } from 'swr' import { api } from '../../lib/api' +import { confirmToast, notify } from '../../lib/notify' import type { SystemStatus, AccountInfo, @@ -116,6 +117,57 @@ export function TerminalDashboard({ const traderId = selectedTrader?.trader_id || selectedTraderId useTick(1000) const clock = new Date().toLocaleTimeString('en-GB', { hour12: false }) + const [closing, setClosing] = useState(null) + + async function closePositionRow(symbol: string, side: 'LONG' | 'SHORT') { + if (!traderId || closing) return + const ok = await confirmToast(`Market-close ${symbol} ${side}?`, { + title: 'Close position', + okText: 'Close', + cancelText: 'Cancel', + }) + if (!ok) return + setClosing(symbol) + try { + await api.closePosition(traderId, symbol, side) + notify.success(`${symbol} ${side} closed`) + await Promise.all([ + mutate(`positions-${traderId}`), + mutate(`account-${traderId}`), + ]) + } catch (err) { + notify.error(err instanceof Error ? err.message : 'Close failed') + } finally { + setClosing(null) + } + } + + async function closeAllPositions(open: Position[]) { + if (!traderId || closing || open.length === 0) return + const ok = await confirmToast( + `Market-close ALL ${open.length} open positions?`, + { title: 'Flatten book', okText: 'Close all', cancelText: 'Cancel' } + ) + if (!ok) return + setClosing('__all__') + let failed = 0 + // Sequential: parallel closes race on exchange nonces / rate limits. + for (const p of open) { + const side = /long|buy/i.test(p.side) ? 'LONG' : 'SHORT' + try { + await api.closePosition(traderId, p.symbol, side) + } catch { + failed++ + } + } + await Promise.all([ + mutate(`positions-${traderId}`), + mutate(`account-${traderId}`), + ]) + if (failed === 0) notify.success('All positions closed') + else notify.error(`${failed}/${open.length} closes failed`) + setClosing(null) + } const { data: realFullStats } = useSWR( traderId ? ['full-stats', traderId] : null, @@ -512,6 +564,26 @@ export function TerminalDashboard({ Positions Current positions · live {positions?.length ?? 0} open + {traderId && !on && positions && positions.length > 0 && ( + + )} {positions && positions.length > 0 ? ( @@ -523,6 +595,7 @@ export function TerminalDashboard({ + {traderId && !on && @@ -538,6 +611,29 @@ export function TerminalDashboard({ + {traderId && !on && ( + + )} ) })}
size PnL return%}
{fmtUsd(notional)} {fmtUsd(p.unrealized_pnl, true)} {(p.unrealized_pnl_pct ?? 0).toFixed(2)}% + +