fix: OKX trading issues and improve position tracking

- Add maxMktSz check for OKX market orders to prevent exceeding limits
- Increase margin safety buffer (0.1% fee + 1% buffer) for all exchanges
- Fix Binance position closure detection with direct trade queries
- Move Recent Completed Trades before Current Positions in AI prompt
- Update README screenshots with table layout for better alignment
This commit is contained in:
tinkle-community
2025-12-10 22:01:57 +08:00
parent 870faa0843
commit ecbedc6525
29 changed files with 2141 additions and 1647 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -276,16 +276,16 @@ export function TraderConfigModal({
>
{availableExchanges.map((exchange) => (
<option key={exchange.id} value={exchange.id}>
{getShortName(
exchange.name || exchange.id
).toUpperCase()}
{getShortName(exchange.name || exchange.exchange_type || exchange.id).toUpperCase()}
{exchange.account_name ? ` - ${exchange.account_name}` : ''}
</option>
))}
</select>
{/* Exchange Registration Link */}
{formData.exchange_id && (() => {
// Exchange ID is the exchange type (e.g., "binance", "okx", "aster")
const exchangeType = formData.exchange_id.toLowerCase()
// Find the selected exchange to get its type
const selectedExchange = availableExchanges.find(e => e.id === formData.exchange_id)
const exchangeType = selectedExchange?.exchange_type?.toLowerCase() || ''
const regLink = EXCHANGE_REGISTRATION_LINKS[exchangeType]
if (!regLink) return null
return (

View File

@@ -16,11 +16,23 @@ import { toast } from 'sonner'
import { Tooltip } from './Tooltip'
import { getShortName } from './utils'
// Supported exchange templates for creating new accounts
const SUPPORTED_EXCHANGE_TEMPLATES = [
{ exchange_type: 'binance', name: 'Binance Futures', type: 'cex' as const },
{ exchange_type: 'bybit', name: 'Bybit Futures', type: 'cex' as const },
{ exchange_type: 'okx', name: 'OKX Futures', type: 'cex' as const },
{ exchange_type: 'hyperliquid', name: 'Hyperliquid', type: 'dex' as const },
{ exchange_type: 'aster', name: 'Aster DEX', type: 'dex' as const },
{ exchange_type: 'lighter', name: 'Lighter', type: 'dex' as const },
]
interface ExchangeConfigModalProps {
allExchanges: Exchange[]
editingExchangeId: string | null
onSave: (
exchangeId: string,
exchangeId: string | null, // null for creating new account
exchangeType: string,
accountName: string,
apiKey: string,
secretKey?: string,
passphrase?: string, // OKX专用
@@ -46,9 +58,8 @@ export function ExchangeConfigModal({
onClose,
language,
}: ExchangeConfigModalProps) {
const [selectedExchangeId, setSelectedExchangeId] = useState(
editingExchangeId || ''
)
// Selected exchange type for creating new accounts
const [selectedExchangeType, setSelectedExchangeType] = useState('')
const [apiKey, setApiKey] = useState('')
const [secretKey, setSecretKey] = useState('')
const [passphrase, setPassphrase] = useState('')
@@ -87,10 +98,25 @@ export function ExchangeConfigModal({
// 保存中状态
const [isSaving, setIsSaving] = useState(false)
// 获取当前编辑的交易所信息
const selectedExchange = allExchanges?.find(
(e) => e.id === selectedExchangeId
)
// 账户名称
const [accountName, setAccountName] = useState('')
// 获取当前编辑的交易所信息或模板
// For editing: find the existing account by id (UUID)
// For creating: use the selected exchange template
const selectedExchange = editingExchangeId
? allExchanges?.find((e) => e.id === editingExchangeId)
: null
// Get the exchange template for displaying UI fields
const selectedTemplate = editingExchangeId
? SUPPORTED_EXCHANGE_TEMPLATES.find((t) => t.exchange_type === selectedExchange?.exchange_type)
: SUPPORTED_EXCHANGE_TEMPLATES.find((t) => t.exchange_type === selectedExchangeType)
// Get the current exchange type (from existing account or selected template)
const currentExchangeType = editingExchangeId
? selectedExchange?.exchange_type
: selectedExchangeType
// 交易所注册链接配置
const exchangeRegistrationLinks: Record<string, { url: string; hasReferral?: boolean }> = {
@@ -105,6 +131,7 @@ export function ExchangeConfigModal({
// 如果是编辑现有交易所,初始化表单数据
useEffect(() => {
if (editingExchangeId && selectedExchange) {
setAccountName(selectedExchange.account_name || '')
setApiKey(selectedExchange.apiKey || '')
setSecretKey(selectedExchange.secretKey || '')
setPassphrase('') // Don't load existing passphrase for security
@@ -127,7 +154,7 @@ export function ExchangeConfigModal({
// 加载服务器IP当选择binance时
useEffect(() => {
if (selectedExchangeId === 'binance' && !serverIP) {
if (currentExchangeType === 'binance' && !serverIP) {
setLoadingIP(true)
api
.getServerIP()
@@ -141,7 +168,7 @@ export function ExchangeConfigModal({
setLoadingIP(false)
})
}
}, [selectedExchangeId])
}, [currentExchangeType])
const handleCopyIP = async (ip: string) => {
try {
@@ -231,32 +258,49 @@ export function ExchangeConfigModal({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!selectedExchangeId || isSaving) return
if (isSaving) return
// For creating, we need the exchange type
if (!editingExchangeId && !selectedExchangeType) return
// Validate account name
const trimmedAccountName = accountName.trim()
if (!trimmedAccountName) {
toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name')
return
}
const exchangeId = editingExchangeId || null
const exchangeType = currentExchangeType || ''
setIsSaving(true)
try {
// 根据交易所类型验证不同字段
if (selectedExchange?.id === 'binance') {
if (currentExchangeType === 'binance') {
if (!apiKey.trim() || !secretKey.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
} else if (selectedExchange?.id === 'okx') {
await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), '', testnet)
} else if (currentExchangeType === 'okx') {
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
} else if (selectedExchange?.id === 'hyperliquid') {
await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
} else if (currentExchangeType === 'hyperliquid') {
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
await onSave(
selectedExchangeId,
exchangeId,
exchangeType,
trimmedAccountName,
apiKey.trim(),
'',
'',
testnet,
hyperliquidWalletAddr.trim()
)
} else if (selectedExchange?.id === 'aster') {
} else if (currentExchangeType === 'aster') {
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())
return
await onSave(
selectedExchangeId,
exchangeId,
exchangeType,
trimmedAccountName,
'',
'',
'',
@@ -266,10 +310,12 @@ export function ExchangeConfigModal({
asterSigner.trim(),
asterPrivateKey.trim()
)
} else if (selectedExchange?.id === 'lighter') {
} else if (currentExchangeType === 'lighter') {
if (!lighterWalletAddr.trim() || !lighterPrivateKey.trim()) return
await onSave(
selectedExchangeId,
exchangeId,
exchangeType,
trimmedAccountName,
lighterPrivateKey.trim(),
'',
'',
@@ -285,16 +331,13 @@ export function ExchangeConfigModal({
} else {
// 默认情况其他CEX交易所
if (!apiKey.trim() || !secretKey.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), '', testnet)
}
} finally {
setIsSaving(false)
}
}
// 可选择的交易所列表(所有支持的交易所)
const availableExchanges = allExchanges || []
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div
@@ -314,7 +357,7 @@ export function ExchangeConfigModal({
: t('addExchange', language)}
</h3>
<div className="flex items-center gap-2">
{selectedExchange?.id === 'binance' && (
{currentExchangeType === 'binance' && (
<button
type="button"
onClick={() => setShowGuide(true)}
@@ -373,8 +416,8 @@ export function ExchangeConfigModal({
{t('environmentSteps.selectTitle', language)}
</div>
<select
value={selectedExchangeId}
onChange={(e) => setSelectedExchangeId(e.target.value)}
value={selectedExchangeType}
onChange={(e) => setSelectedExchangeType(e.target.value)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
@@ -391,10 +434,10 @@ export function ExchangeConfigModal({
<option value="">
{t('pleaseSelectExchange', language)}
</option>
{availableExchanges.map((exchange) => (
<option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name)} (
{exchange.type.toUpperCase()})
{SUPPORTED_EXCHANGE_TEMPLATES.map((template) => (
<option key={template.exchange_type} value={template.exchange_type}>
{getShortName(template.name)} (
{template.type.toUpperCase()})
</option>
))}
</select>
@@ -402,32 +445,65 @@ export function ExchangeConfigModal({
</div>
)}
{selectedExchange && (
{selectedTemplate && (
<div
className="p-4 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 flex items-center justify-center">
{getExchangeIcon(selectedExchange.id, {
{getExchangeIcon(selectedTemplate.exchange_type, {
width: 32,
height: 32,
})}
</div>
<div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>
{getShortName(selectedExchange.name)}
{getShortName(selectedTemplate.name)}
{editingExchangeId && selectedExchange?.account_name && (
<span className="text-sm font-normal ml-2" style={{ color: '#848E9C' }}>
- {selectedExchange.account_name}
</span>
)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{selectedExchange.type.toUpperCase()} {' '}
{selectedExchange.id}
{selectedTemplate.type.toUpperCase()} {' '}
{selectedTemplate.exchange_type}
</div>
</div>
</div>
{/* 账户名称输入 */}
<div className="mt-3">
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{language === 'zh' ? '账户名称' : 'Account Name'} *
</label>
<input
type="text"
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
placeholder={language === 'zh' ? '例如:主账户、套利账户' : 'e.g., Main Account, Arbitrage Account'}
className="w-full px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
required
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{language === 'zh'
? '为此账户设置一个易于识别的名称,以便区分同一交易所的多个账户'
: 'Set an easily recognizable name for this account to distinguish multiple accounts on the same exchange'}
</div>
</div>
{/* 注册链接 */}
<a
href={exchangeRegistrationLinks[selectedExchange.id]?.url || '#'}
href={exchangeRegistrationLinks[currentExchangeType || '']?.url || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between p-3 rounded-lg mt-3 transition-all hover:scale-[1.02]"
@@ -441,7 +517,7 @@ export function ExchangeConfigModal({
<span className="text-sm" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '还没有交易所账号?点击注册' : "No exchange account? Register here"}
</span>
{exchangeRegistrationLinks[selectedExchange.id]?.hasReferral && (
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}
@@ -455,15 +531,15 @@ export function ExchangeConfigModal({
</div>
)}
{selectedExchange && (
{selectedTemplate && (
<>
{/* Binance/Bybit/OKX 的输入字段 */}
{(selectedExchange.id === 'binance' ||
selectedExchange.id === 'bybit' ||
selectedExchange.id === 'okx') && (
{(currentExchangeType === 'binance' ||
currentExchangeType === 'bybit' ||
currentExchangeType === 'okx') && (
<>
{/* 币安用户配置提示 (D1 方案) */}
{selectedExchange.id === 'binance' && (
{currentExchangeType === 'binance' && (
<div
className="mb-4 p-3 rounded cursor-pointer transition-colors"
style={{
@@ -605,7 +681,7 @@ export function ExchangeConfigModal({
/>
</div>
{selectedExchange.id === 'okx' && (
{currentExchangeType === 'okx' && (
<div>
<label
className="block text-sm font-semibold mb-2"
@@ -630,7 +706,7 @@ export function ExchangeConfigModal({
)}
{/* Binance 白名单IP提示 */}
{selectedExchange.id === 'binance' && (
{currentExchangeType === 'binance' && (
<div
className="p-4 rounded"
style={{
@@ -690,7 +766,7 @@ export function ExchangeConfigModal({
)}
{/* Aster 交易所的字段 */}
{selectedExchange.id === 'aster' && (
{currentExchangeType === 'aster' && (
<>
{/* API Pro 代理钱包说明 banner */}
<div
@@ -829,7 +905,7 @@ export function ExchangeConfigModal({
)}
{/* Hyperliquid 交易所的字段 */}
{selectedExchange.id === 'hyperliquid' && (
{currentExchangeType === 'hyperliquid' && (
<>
{/* 安全提示 banner */}
<div
@@ -965,7 +1041,7 @@ export function ExchangeConfigModal({
)}
{/* LIGHTER 特定配置 */}
{selectedExchange?.id === 'lighter' && (
{currentExchangeType === 'lighter' && (
<>
{/* L1 Wallet Address */}
<div className="mb-4">
@@ -1100,30 +1176,31 @@ export function ExchangeConfigModal({
type="submit"
disabled={
isSaving ||
!selectedExchange ||
(selectedExchange.id === 'binance' &&
!selectedTemplate ||
!accountName.trim() ||
(currentExchangeType === 'binance' &&
(!apiKey.trim() || !secretKey.trim())) ||
(selectedExchange.id === 'okx' &&
(currentExchangeType === 'okx' &&
(!apiKey.trim() ||
!secretKey.trim() ||
!passphrase.trim())) ||
(selectedExchange.id === 'hyperliquid' &&
(currentExchangeType === 'hyperliquid' &&
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
(selectedExchange.id === 'aster' &&
(currentExchangeType === 'aster' &&
(!asterUser.trim() ||
!asterSigner.trim() ||
!asterPrivateKey.trim())) ||
(selectedExchange.id === 'lighter' &&
(currentExchangeType === 'lighter' &&
(!lighterWalletAddr.trim() || !lighterPrivateKey.trim())) ||
(selectedExchange.id === 'bybit' &&
(currentExchangeType === 'bybit' &&
(!apiKey.trim() || !secretKey.trim())) ||
(selectedExchange.type === 'cex' &&
selectedExchange.id !== 'hyperliquid' &&
selectedExchange.id !== 'aster' &&
selectedExchange.id !== 'lighter' &&
selectedExchange.id !== 'binance' &&
selectedExchange.id !== 'bybit' &&
selectedExchange.id !== 'okx' &&
(selectedTemplate?.type === 'cex' &&
currentExchangeType !== 'hyperliquid' &&
currentExchangeType !== 'aster' &&
currentExchangeType !== 'lighter' &&
currentExchangeType !== 'binance' &&
currentExchangeType !== 'bybit' &&
currentExchangeType !== 'okx' &&
(!apiKey.trim() || !secretKey.trim()))
}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"

View File

@@ -45,17 +45,20 @@ export function ExchangesSection({
>
<div className="flex items-center gap-2 md:gap-3">
<div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
{getExchangeIcon(exchange.id, { width: 28, height: 28 })}
{getExchangeIcon(exchange.exchange_type, { width: 28, height: 28 })}
</div>
<div className="min-w-0">
<div
className="font-semibold text-sm md:text-base truncate"
style={{ color: '#EAECEF' }}
>
{getShortName(exchange.name)}
{exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)}
<span className="text-xs font-normal ml-1.5" style={{ color: '#F0B90B' }}>
- {exchange.account_name || 'Default'}
</span>
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{exchange.type.toUpperCase()} {' '}
{exchange.type?.toUpperCase() || 'CEX'} {' '}
{inUse
? t('inUse', language)
: exchange.enabled

View File

@@ -1,12 +1,13 @@
import { Bot, BarChart3, Trash2, Pencil } from 'lucide-react'
import { t, type Language } from '../../../i18n/translations'
import { getModelDisplayName } from '../index'
import type { TraderInfo } from '../../../types'
import type { TraderInfo, Exchange } from '../../../types'
import { PunkAvatar, getTraderAvatar } from '../../PunkAvatar'
interface TradersGridProps {
language: Language
traders: TraderInfo[] | undefined
exchanges?: Exchange[]
onTraderSelect: (traderId: string) => void
onEditTrader: (traderId: string) => void
onDeleteTrader: (traderId: string) => void
@@ -16,11 +17,20 @@ interface TradersGridProps {
export function TradersGrid({
language,
traders,
exchanges = [],
onTraderSelect,
onEditTrader,
onDeleteTrader,
onToggleTrader,
}: TradersGridProps) {
// Helper function to get exchange display name
const getExchangeDisplayName = (exchangeId: string | undefined) => {
if (!exchangeId) return 'Unknown'
const exchange = exchanges.find(e => e.id === exchangeId)
if (!exchange) return exchangeId.toUpperCase()
const typeName = exchange.exchange_type?.toUpperCase() || exchange.name
return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName
}
if (!traders || traders.length === 0) {
return (
<div className="text-center py-12 md:py-16" style={{ color: '#848E9C' }}>
@@ -74,7 +84,7 @@ export function TradersGrid({
{getModelDisplayName(
trader.ai_model.split('_').pop() || trader.ai_model
)}{' '}
Model {trader.exchange_id?.toUpperCase()}
Model {getExchangeDisplayName(trader.exchange_id)}
<span style={{ color: '#F0B90B' }}> {trader.strategy_name || 'No Strategy'}</span>
</div>
</div>