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:
tinkle-community
2025-12-06 19:16:37 +08:00
parent 5e5be347ad
commit a77c54dbef
8 changed files with 255 additions and 23 deletions

View File

@@ -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' }}

View File

@@ -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

View File

@@ -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' }}