feat: close / close-all buttons in the terminal 'Current positions · live' panel

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.
This commit is contained in:
tinkle-community
2026-07-24 15:40:06 +09:00
parent 48d7835c48
commit 05899d5afe

View File

@@ -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<string | null>(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({
<span className="tm-px" style={{ fontSize: 11 }}>Positions</span>
<span className="tm-sc">Current positions · live</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{positions?.length ?? 0} open</span>
{traderId && !on && positions && positions.length > 0 && (
<button
type="button"
onClick={() => void closeAllPositions(positions)}
disabled={closing !== null}
className="tm-mono"
style={{
background: 'transparent',
border: '1px solid var(--tm-dn)',
color: 'var(--tm-dn)',
borderRadius: 3,
fontSize: 9,
padding: '1px 6px',
cursor: closing ? 'not-allowed' : 'pointer',
opacity: closing ? 0.5 : 1,
}}
>
{closing === '__all__' ? 'closing…' : 'close all'}
</button>
)}
</div>
{positions && positions.length > 0 ? (
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
@@ -523,6 +595,7 @@ export function TerminalDashboard({
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>size</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>PnL</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>return%</td>
{traderId && !on && <td style={{ padding: '0 0 3px' }} />}
</tr>
</thead>
<tbody>
@@ -538,6 +611,29 @@ export function TerminalDashboard({
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-ink-2)' }}>{fmtUsd(notional)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.unrealized_pnl, true)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{(p.unrealized_pnl_pct ?? 0).toFixed(2)}%</td>
{traderId && !on && (
<td style={{ padding: '5px 0 5px 8px', textAlign: 'right', width: 1 }}>
<button
type="button"
onClick={() => void closePositionRow(p.symbol, long ? 'LONG' : 'SHORT')}
disabled={closing !== null}
title={`Close ${p.symbol}`}
className="tm-mono"
style={{
background: 'transparent',
border: '1px solid var(--tm-dn)',
color: 'var(--tm-dn)',
borderRadius: 3,
fontSize: 9,
padding: '1px 5px',
cursor: closing ? 'not-allowed' : 'pointer',
opacity: closing ? 0.5 : 1,
}}
>
{closing === p.symbol ? '…' : 'close'}
</button>
</td>
)}
</tr>
)
})}