mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 14:56:57 +08:00
Refine strategy creation flow and diagnostics
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}%
|
||||
|
||||
@@ -12,7 +12,6 @@ export const coinSource = {
|
||||
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider', es: 'Proveedor AI500' },
|
||||
oi_top: { zh: 'OI 持仓增加', en: 'OI Increase', es: 'Aumento OI' },
|
||||
oi_low: { zh: 'OI 持仓减少', en: 'OI Decrease', es: 'Disminución OI' },
|
||||
mixed: { zh: '混合模式', en: 'Mixed Mode', es: 'Modo Mixto' },
|
||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins', es: 'Monedas Personalizadas' },
|
||||
addCoin: { zh: '添加币种', en: 'Add Coin', es: 'Agregar Moneda' },
|
||||
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider', es: 'Habilitar AI500' },
|
||||
@@ -22,8 +21,6 @@ export const coinSource = {
|
||||
useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease', es: 'Habilitar Disminución OI' },
|
||||
oiLowLimit: { zh: '数量上限', en: 'Limit', es: 'Límite' },
|
||||
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins', es: 'Especificar monedas manualmente' },
|
||||
mixedConfig: { zh: '组合数据源配置', en: 'Combined Sources Configuration', es: 'Configuración Combinada' },
|
||||
mixedSummary: { zh: '已选组合', en: 'Selected Sources', es: 'Fuentes Seleccionadas' },
|
||||
maxCoins: { zh: '最多', en: 'Up to', es: 'Hasta' },
|
||||
coins: { zh: '个币种', en: 'coins', es: 'monedas' },
|
||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration', es: 'Configuración de Fuente' },
|
||||
@@ -34,7 +31,6 @@ export const coinSource = {
|
||||
ai500Desc: { zh: '使用 AI500 智能筛选的热门币种', en: 'Use AI500 smart-filtered popular coins', es: 'Monedas filtradas por AI500' },
|
||||
oi_topDesc: { zh: '持仓增加榜,适合做多', en: 'OI increase ranking, for long', es: 'Ranking OI creciente, para largo' },
|
||||
oi_lowDesc: { zh: '持仓减少榜,适合做空', en: 'OI decrease ranking, for short', es: 'Ranking OI decreciente, para corto' },
|
||||
mixedDesc: { zh: '组合多种数据源', en: 'Combine multiple sources', es: 'Combinar fuentes múltiples' },
|
||||
oiIncreaseShort: { zh: 'OI增', en: 'OI↑', es: 'OI↑' },
|
||||
oiDecreaseShort: { zh: 'OI减', en: 'OI↓', es: 'OI↓' },
|
||||
custom: { zh: '自定义', en: 'Custom', es: 'Personalizado' },
|
||||
|
||||
@@ -105,7 +105,7 @@ export interface GridStrategyConfig {
|
||||
}
|
||||
|
||||
export interface CoinSourceConfig {
|
||||
source_type: 'static' | 'ai500' | 'oi_top' | 'oi_low' | 'mixed';
|
||||
source_type: 'static' | 'ai500' | 'oi_top' | 'oi_low';
|
||||
static_coins?: string[];
|
||||
excluded_coins?: string[]; // 排除的币种列表
|
||||
use_ai500: boolean;
|
||||
|
||||
Reference in New Issue
Block a user