feat: close-all button on the dashboard positions panel

Per-position one-click close already existed (Action column, with
confirm), but flattening the whole book took one confirm per position.
Adds a 'Close All' button next to the position count: single confirm,
then closes every position sequentially (parallel closes can race on
exchange nonces/rate limits), refreshes via SWR and reports a partial-
failure count. zh/en/id translations included.
This commit is contained in:
tinkle-community
2026-07-24 15:23:14 +09:00
parent 4806ee83f7
commit 48d7835c48
2 changed files with 79 additions and 2 deletions

View File

@@ -1015,6 +1015,11 @@ export const translations = {
cancel: 'Cancel',
positionClosed: 'Position closed successfully',
closeFailed: 'Failed to close position',
closeAll: 'Close All',
confirmCloseAllPositions:
'Market-close ALL {count} open positions?',
allPositionsClosed: 'All positions closed',
closeAllPartial: '{failed} of {count} positions failed to close',
hideAddress: 'Hide address',
showFullAddress: 'Show full address',
copyAddress: 'Copy address',
@@ -2181,6 +2186,10 @@ export const translations = {
cancel: '取消',
positionClosed: '平仓成功',
closeFailed: '平仓失败',
closeAll: '一键全平',
confirmCloseAllPositions: '确定要市价平掉全部 {count} 个持仓吗?',
allPositionsClosed: '全部持仓已平',
closeAllPartial: '{count} 个持仓中有 {failed} 个平仓失败',
hideAddress: '隐藏地址',
showFullAddress: '显示完整地址',
copyAddress: '复制地址',
@@ -3293,6 +3302,11 @@ export const translations = {
cancel: 'Batal',
positionClosed: 'Posisi berhasil ditutup',
closeFailed: 'Gagal menutup posisi',
closeAll: 'Tutup Semua',
confirmCloseAllPositions:
'Tutup SEMUA {count} posisi terbuka dengan harga pasar?',
allPositionsClosed: 'Semua posisi ditutup',
closeAllPartial: '{failed} dari {count} posisi gagal ditutup',
hideAddress: 'Sembunyikan alamat',
showFullAddress: 'Tampilkan alamat lengkap',
copyAddress: 'Salin alamat',

View File

@@ -137,6 +137,7 @@ export function TraderDashboardPage({
exchanges,
}: TraderDashboardPageProps) {
const [closingPosition, setClosingPosition] = useState<string | null>(null)
const [closingAll, setClosingAll] = useState(false)
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>(undefined)
const [chartUpdateKey, setChartUpdateKey] = useState<number>(0)
const chartSectionRef = useRef<HTMLDivElement>(null)
@@ -231,6 +232,52 @@ export function TraderDashboardPage({
}
}
const handleCloseAllPositions = async () => {
if (!selectedTraderId || !positions || positions.length === 0) return
const count = String(positions.length)
const confirmed = await confirmToast(
t('traderDashboard.confirmCloseAllPositions', language, { count }),
{
title: t('traderDashboard.confirmClose', language),
okText: t('traderDashboard.confirm', language),
cancelText: t('traderDashboard.cancel', language),
}
)
if (!confirmed) return
setClosingAll(true)
let failed = 0
// Sequential on purpose: parallel closes on the same account can race
// on exchange nonces (Hyperliquid) and rate limits.
for (const pos of positions) {
try {
await api.closePosition(
selectedTraderId,
pos.symbol,
pos.side.toUpperCase()
)
} catch {
failed++
}
}
await Promise.all([
mutate(`positions-${selectedTraderId}`),
mutate(`account-${selectedTraderId}`),
])
if (failed === 0) {
notify.success(t('traderDashboard.allPositionsClosed', language))
} else {
notify.error(
t('traderDashboard.closeAllPartial', language, {
failed: String(failed),
count,
})
)
}
setClosingAll(false)
}
// If API failed with error, show empty state (likely backend not running)
if (tradersError) {
return (
@@ -588,8 +635,24 @@ export function TraderDashboardPage({
<span className="text-blue-500"></span> {t('currentPositions', language)}
</h2>
{positions && positions.length > 0 && (
<div className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 font-mono">
{positions.length} {t('active', language)}
<div className="flex items-center gap-2">
<div className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 font-mono">
{positions.length} {t('active', language)}
</div>
<button
type="button"
onClick={() => void handleCloseAllPositions()}
disabled={closingAll || closingPosition !== null}
className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed bg-nofx-danger/10 text-nofx-danger border border-nofx-danger/30 hover:bg-nofx-danger/20"
title={t('traderDashboard.closeAll', language)}
>
{closingAll ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<LogOut className="w-3 h-3" />
)}
{t('traderDashboard.closeAll', language)}
</button>
</div>
)}
</div>