mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-27 22:12:52 +08:00
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:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user