mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-28 06:22:49 +08:00
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:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user