Refine strategy creation flow and diagnostics

This commit is contained in:
lky-spec
2026-05-09 14:48:24 +08:00
parent 0f11be77f8
commit e67a927a4f
29 changed files with 3410 additions and 1305 deletions

View File

@@ -1,4 +1,5 @@
import useSWR from 'swr'
import { useEffect } from 'react'
import { useAuth } from '../../contexts/AuthContext'
import { api } from '../../lib/api'
import { ArrowUpRight, ArrowDownRight, Wallet } from 'lucide-react'
@@ -7,7 +8,7 @@ import type { Position, TraderInfo } from '../../types'
export function PositionsPanel() {
const { user, token } = useAuth()
const { data: traders } = useSWR<TraderInfo[]>(
const { data: traders, mutate: mutateTraders } = useSWR<TraderInfo[]>(
user && token ? 'agent-traders' : null,
api.getTraders,
{ refreshInterval: 30000, shouldRetryOnError: false }
@@ -17,12 +18,21 @@ export function PositionsPanel() {
const runningTrader = traders?.find((t) => t.is_running)
const traderId = runningTrader?.trader_id
const { data: positions } = useSWR<Position[]>(
const { data: positions, mutate: mutatePositions } = useSWR<Position[]>(
traderId ? `agent-positions-${traderId}` : null,
() => api.getPositions(traderId),
{ refreshInterval: 15000, shouldRetryOnError: false }
)
useEffect(() => {
const handleRefresh = () => {
void mutateTraders()
void mutatePositions()
}
window.addEventListener('agent-config-refresh', handleRefresh)
return () => window.removeEventListener('agent-config-refresh', handleRefresh)
}, [mutatePositions, mutateTraders])
if (!user || !token) {
return (
<div

View File

@@ -1,4 +1,5 @@
import useSWR from 'swr'
import { useEffect } from 'react'
import { useAuth } from '../../contexts/AuthContext'
import { api } from '../../lib/api'
import { Activity, CircleOff, Bot } from 'lucide-react'
@@ -7,12 +8,20 @@ import type { TraderInfo } from '../../types'
export function TraderStatusPanel() {
const { user, token } = useAuth()
const { data: traders } = useSWR<TraderInfo[]>(
const { data: traders, mutate } = useSWR<TraderInfo[]>(
user && token ? 'agent-sidebar-traders' : null,
api.getTraders,
{ refreshInterval: 30000, shouldRetryOnError: false }
)
useEffect(() => {
const handleRefresh = () => {
void mutate()
}
window.addEventListener('agent-config-refresh', handleRefresh)
return () => window.removeEventListener('agent-config-refresh', handleRefresh)
}, [mutate])
if (!user || !token) {
return (
<div

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap } from 'lucide-react'
import type { CoinSourceConfig } from '../../types'
import { coinSource, ts } from '../../i18n/strategy-translations'
import { NofxSelect } from '../ui/select'
@@ -27,31 +27,6 @@ export function CoinSourceEditor({
{ value: 'oi_low', icon: TrendingDown, color: '#F6465D' },
] as const
// Calculate mixed mode summary
const getMixedSummary = () => {
const sources: string[] = []
let totalLimit = 0
if (config.use_ai500) {
sources.push(`AI500(${config.ai500_limit || 3})`)
totalLimit += config.ai500_limit || 3
}
if (config.use_oi_top) {
sources.push(`${ts(coinSource.oiIncreaseShort, language)}(${config.oi_top_limit || 3})`)
totalLimit += config.oi_top_limit || 3
}
if (config.use_oi_low) {
sources.push(`${ts(coinSource.oiDecreaseShort, language)}(${config.oi_low_limit || 3})`)
totalLimit += config.oi_low_limit || 3
}
if ((config.static_coins || []).length > 0) {
sources.push(`${ts(coinSource.custom, language)}(${config.static_coins?.length || 0})`)
totalLimit += config.static_coins?.length || 0
}
return { sources, totalLimit }
}
// xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix
const xyzDexAssets = new Set([
// Stocks
@@ -453,228 +428,6 @@ export function CoinSourceEditor({
</div>
</div>
)}
{/* Mixed Mode - Unified Card Selector */}
{config.source_type === 'mixed' && (
<div className="p-4 rounded-lg bg-blue-500/5 border border-blue-500/20">
<div className="flex items-center gap-2 mb-4">
<Shuffle className="w-4 h-4 text-blue-400" />
<span className="text-sm font-medium text-nofx-text">
{ts(coinSource.mixedConfig, language)}
</span>
</div>
{/* 4 Source Cards in 2x2 Grid */}
<div className="grid grid-cols-2 gap-3 mb-4">
{/* AI500 Card */}
<div
className={`p-3 rounded-lg border transition-all cursor-pointer ${
config.use_ai500
? 'bg-nofx-gold/10 border-nofx-gold/50'
: 'bg-nofx-bg border-nofx-border hover:border-nofx-gold/30'
}`}
onClick={() => !disabled && onChange({ ...config, use_ai500: !config.use_ai500 })}
>
<div className="flex items-center gap-2 mb-2">
<input
type="checkbox"
checked={config.use_ai500}
onChange={(e) => !disabled && onChange({ ...config, use_ai500: e.target.checked })}
disabled={disabled}
className="w-4 h-4 rounded accent-nofx-gold"
onClick={(e) => e.stopPropagation()}
/>
<Database className="w-4 h-4 text-nofx-gold" />
<span className="text-sm font-medium text-nofx-text">AI500</span>
<NofxOSBadge />
</div>
{config.use_ai500 && (
<div className="flex items-center gap-2 mt-2 pl-6">
<span className="text-xs text-nofx-text-muted">Limit:</span>
<NofxSelect
value={config.ai500_limit || 3}
onChange={(val) => !disabled && onChange({ ...config, ai500_limit: parseInt(val) || 3 })}
disabled={disabled}
options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => ({ value: n, label: String(n) }))}
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
</div>
)}
</div>
{/* OI Top Card */}
<div
className={`p-3 rounded-lg border transition-all cursor-pointer ${
config.use_oi_top
? 'bg-nofx-success/10 border-nofx-success/50'
: 'bg-nofx-bg border-nofx-border hover:border-nofx-success/30'
}`}
onClick={() => !disabled && onChange({ ...config, use_oi_top: !config.use_oi_top })}
>
<div className="flex items-center gap-2 mb-2">
<input
type="checkbox"
checked={config.use_oi_top}
onChange={(e) => !disabled && onChange({ ...config, use_oi_top: e.target.checked })}
disabled={disabled}
className="w-4 h-4 rounded accent-nofx-success"
onClick={(e) => e.stopPropagation()}
/>
<TrendingUp className="w-4 h-4 text-nofx-success" />
<span className="text-sm font-medium text-nofx-text">
{ts(coinSource.oiIncreaseLabel, language)}
</span>
</div>
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
{ts(coinSource.forLong, language)}
</p>
{config.use_oi_top && (
<div className="flex items-center gap-2 mt-2 pl-6">
<span className="text-xs text-nofx-text-muted">Limit:</span>
<NofxSelect
value={config.oi_top_limit || 3}
onChange={(val) => !disabled && onChange({ ...config, oi_top_limit: parseInt(val) || 3 })}
disabled={disabled}
options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => ({ value: n, label: String(n) }))}
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
</div>
)}
</div>
{/* OI Low Card */}
<div
className={`p-3 rounded-lg border transition-all cursor-pointer ${
config.use_oi_low
? 'bg-nofx-danger/10 border-nofx-danger/50'
: 'bg-nofx-bg border-nofx-border hover:border-nofx-danger/30'
}`}
onClick={() => !disabled && onChange({ ...config, use_oi_low: !config.use_oi_low })}
>
<div className="flex items-center gap-2 mb-2">
<input
type="checkbox"
checked={config.use_oi_low}
onChange={(e) => !disabled && onChange({ ...config, use_oi_low: e.target.checked })}
disabled={disabled}
className="w-4 h-4 rounded accent-red-500"
onClick={(e) => e.stopPropagation()}
/>
<TrendingDown className="w-4 h-4 text-nofx-danger" />
<span className="text-sm font-medium text-nofx-text">
{ts(coinSource.oiDecreaseLabel, language)}
</span>
</div>
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
{ts(coinSource.forShort, language)}
</p>
{config.use_oi_low && (
<div className="flex items-center gap-2 mt-2 pl-6">
<span className="text-xs text-nofx-text-muted">Limit:</span>
<NofxSelect
value={config.oi_low_limit || 3}
onChange={(val) => !disabled && onChange({ ...config, oi_low_limit: parseInt(val) || 3 })}
disabled={disabled}
options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => ({ value: n, label: String(n) }))}
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
</div>
)}
</div>
{/* Static/Custom Card */}
<div
className={`p-3 rounded-lg border transition-all cursor-pointer ${
(config.static_coins || []).length > 0
? 'bg-gray-500/10 border-gray-500/50'
: 'bg-nofx-bg border-nofx-border hover:border-gray-500/30'
}`}
>
<div className="flex items-center gap-2 mb-2">
<List className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-nofx-text">
{ts(coinSource.custom, language)}
</span>
{(config.static_coins || []).length > 0 && (
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
{config.static_coins?.length}
</span>
)}
</div>
<div className="flex flex-wrap gap-1 mt-2">
{(config.static_coins || []).slice(0, 3).map((coin) => (
<span
key={coin}
className="flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-nofx-bg-lighter text-nofx-text"
>
{coin}
{!disabled && (
<button
onClick={(e) => {
e.stopPropagation()
handleRemoveCoin(coin)
}}
className="hover:text-red-400 transition-colors"
>
<X className="w-2.5 h-2.5" />
</button>
)}
</span>
))}
{(config.static_coins || []).length > 3 && (
<span className="text-xs text-nofx-text-muted">
+{(config.static_coins?.length || 0) - 3}
</span>
)}
</div>
{!disabled && (
<div className="flex gap-1 mt-2">
<input
type="text"
value={newCoin}
onChange={(e) => setNewCoin(e.target.value)}
onKeyDown={(e) => {
e.stopPropagation()
if (e.key === 'Enter') handleAddCoin()
}}
onClick={(e) => e.stopPropagation()}
placeholder="BTC, ETH..."
className="flex-1 px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
<button
onClick={(e) => {
e.stopPropagation()
handleAddCoin()
}}
className="px-2 py-1 rounded text-xs bg-nofx-gold text-black hover:bg-yellow-500"
>
<Plus className="w-3 h-3" />
</button>
</div>
)}
</div>
</div>
{/* Summary */}
{(() => {
const { sources, totalLimit } = getMixedSummary()
if (sources.length === 0) return null
return (
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
<div className="flex items-center justify-between text-xs">
<span className="text-nofx-text-muted">{ts(coinSource.mixedSummary, language)}:</span>
<span className="text-nofx-text font-medium">
{sources.join(' + ')}
</span>
</div>
<div className="text-xs text-nofx-text-muted mt-1">
{ts(coinSource.maxCoins, language)} {totalLimit} {ts(coinSource.coins, language)}
</div>
</div>
)
})()}
</div>
)}
</div>
)
}

View File

@@ -338,7 +338,8 @@ export function TraderConfigModal({
<div>
{t('coinSource', language)}: {aiConfig.coin_source.source_type === 'static' ? '固定币种' :
aiConfig.coin_source.source_type === 'ai500' ? 'AI500' :
aiConfig.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
aiConfig.coin_source.source_type === 'oi_top' ? 'OI Top' :
aiConfig.coin_source.source_type === 'oi_low' ? 'OI Low' : '-'}
</div>
<div>
{t('marginLimit', language)}: {((aiConfig.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%