mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 18:41:01 +08:00
feat: add one-click close position for all exchanges
- Add handleClosePosition API endpoint in server.go - Add closePosition API function in frontend - Add close position button to positions table in App.tsx and TraderDashboard.tsx - Fix GetFullConfig to include passphrase field for OKX - Fix OKX CloseLong/CloseShort to use position quantity directly (already in contracts)
This commit is contained in:
@@ -18,6 +18,7 @@ import { t, type Language } from './i18n/translations'
|
||||
import { useSystemConfig } from './hooks/useSystemConfig'
|
||||
import { DecisionCard } from './components/DecisionCard'
|
||||
import { BacktestPage } from './components/BacktestPage'
|
||||
import { LogOut, Loader2 } from 'lucide-react'
|
||||
import type {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
@@ -524,6 +525,31 @@ function TraderDetailsPage({
|
||||
lastUpdate: string
|
||||
language: Language
|
||||
}) {
|
||||
const [closingPosition, setClosingPosition] = useState<string | null>(null)
|
||||
|
||||
// 平仓操作
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
if (!selectedTraderId) return
|
||||
|
||||
const confirmMsg = language === 'zh'
|
||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
||||
|
||||
if (!confirm(confirmMsg)) return
|
||||
|
||||
setClosingPosition(symbol)
|
||||
try {
|
||||
await api.closePosition(selectedTraderId, symbol, side)
|
||||
const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
||||
alert(successMsg)
|
||||
window.location.reload()
|
||||
} catch (err: unknown) {
|
||||
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
|
||||
alert(errorMsg)
|
||||
} finally {
|
||||
setClosingPosition(null)
|
||||
}
|
||||
}
|
||||
// If API failed with error, show empty state (likely backend not running)
|
||||
if (tradersError) {
|
||||
return (
|
||||
@@ -836,6 +862,9 @@ function TraderDetailsPage({
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('entryPrice', language)}
|
||||
</th>
|
||||
@@ -889,6 +918,27 @@ function TraderDetailsPage({
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClosePosition(pos.symbol, pos.side.toUpperCase())}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
}}
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
|
||||
@@ -102,6 +102,15 @@ export const api = {
|
||||
if (!result.success) throw new Error('停止交易员失败')
|
||||
},
|
||||
|
||||
async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> {
|
||||
const result = await httpClient.post<{ message: string }>(
|
||||
`${API_BASE}/traders/${traderId}/close-position`,
|
||||
{ symbol, side }
|
||||
)
|
||||
if (!result.success) throw new Error('平仓失败')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async updateTraderPrompt(
|
||||
traderId: string,
|
||||
customPrompt: string
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
Check,
|
||||
X,
|
||||
XCircle,
|
||||
LogOut,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { stripLeadingIcons } from '../lib/text'
|
||||
import type {
|
||||
@@ -63,6 +65,9 @@ export default function TraderDashboard() {
|
||||
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>()
|
||||
const [chartUpdateKey, setChartUpdateKey] = useState(0)
|
||||
|
||||
// 平仓操作状态
|
||||
const [closingPosition, setClosingPosition] = useState<string | null>(null) // symbol being closed
|
||||
|
||||
// 点击持仓币种时调用
|
||||
const handlePositionSymbolClick = (symbol: string) => {
|
||||
setSelectedChartSymbol(symbol)
|
||||
@@ -75,6 +80,31 @@ export default function TraderDashboard() {
|
||||
localStorage.setItem('decisionLimit', newLimit.toString())
|
||||
}
|
||||
|
||||
// 平仓操作
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
if (!selectedTraderId) return
|
||||
|
||||
const confirmMsg = language === 'zh'
|
||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
||||
|
||||
if (!confirm(confirmMsg)) return
|
||||
|
||||
setClosingPosition(symbol)
|
||||
try {
|
||||
await api.closePosition(selectedTraderId, symbol, side)
|
||||
const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
||||
alert(successMsg)
|
||||
// 刷新持仓数据
|
||||
window.location.reload()
|
||||
} catch (err: unknown) {
|
||||
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
|
||||
alert(errorMsg)
|
||||
} finally {
|
||||
setClosingPosition(null)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取trader列表(仅在用户登录时)
|
||||
const { data: traders, error: tradersError } = useSWR<TraderInfo[]>(
|
||||
user && token ? 'traders' : null,
|
||||
@@ -474,6 +504,9 @@ export default function TraderDashboard() {
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">
|
||||
{t('entryPrice', language)}
|
||||
</th>
|
||||
@@ -544,6 +577,27 @@ export default function TraderDashboard() {
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClosePosition(pos.symbol, pos.side.toUpperCase())}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
}}
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="py-3 font-mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
|
||||
Reference in New Issue
Block a user