mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
feat: improve strategy studio and fix trader deletion bug
- Add strategy export/import functionality to Strategy Studio - Fix trader deletion not removing from memory (competition page ghost data) - Simplify TraderConfigViewModal: remove unused fields, show strategy name - Improve quant data formatting: OI/Netflow multi-timeframe display - Add configurable OI/Netflow toggles in indicator settings - Clean up unused frontend components and dead code
This commit is contained in:
@@ -1,214 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import useSWR from 'swr'
|
||||
import { api } from '../lib/api'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useTradersConfigStore, useTradersModalStore } from '../stores'
|
||||
import { useTraderActions } from '../hooks/useTraderActions'
|
||||
import { TraderConfigModal } from '../components/TraderConfigModal'
|
||||
import {
|
||||
ModelConfigModal,
|
||||
ExchangeConfigModal,
|
||||
} from '../components/traders'
|
||||
import { PageHeader } from '../components/traders/sections/PageHeader'
|
||||
import { AIModelsSection } from '../components/traders/sections/AIModelsSection'
|
||||
import { ExchangesSection } from '../components/traders/sections/ExchangesSection'
|
||||
import { TradersGrid } from '../components/traders/sections/TradersGrid'
|
||||
|
||||
interface AITradersPageProps {
|
||||
onTraderSelect?: (traderId: string) => void
|
||||
}
|
||||
|
||||
export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const { language } = useLanguage()
|
||||
const { user, token } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Zustand stores
|
||||
const {
|
||||
allModels,
|
||||
allExchanges,
|
||||
supportedModels,
|
||||
supportedExchanges,
|
||||
configuredModels,
|
||||
configuredExchanges,
|
||||
loadConfigs,
|
||||
setAllModels,
|
||||
setAllExchanges,
|
||||
} = useTradersConfigStore()
|
||||
|
||||
const {
|
||||
showCreateModal,
|
||||
showEditModal,
|
||||
showModelModal,
|
||||
showExchangeModal,
|
||||
editingModel,
|
||||
editingExchange,
|
||||
editingTrader,
|
||||
setShowCreateModal,
|
||||
setShowEditModal,
|
||||
setShowModelModal,
|
||||
setShowExchangeModal,
|
||||
setEditingModel,
|
||||
setEditingExchange,
|
||||
setEditingTrader,
|
||||
} = useTradersModalStore()
|
||||
|
||||
// SWR for traders data
|
||||
const { data: traders, mutate: mutateTraders } = useSWR(
|
||||
user && token ? 'traders' : null,
|
||||
api.getTraders,
|
||||
{ refreshInterval: 5000 }
|
||||
)
|
||||
|
||||
// Load configurations
|
||||
useEffect(() => {
|
||||
loadConfigs(user, token)
|
||||
}, [user, token, loadConfigs])
|
||||
|
||||
// Business logic hook
|
||||
const {
|
||||
isModelInUse,
|
||||
isExchangeInUse,
|
||||
handleCreateTrader,
|
||||
handleEditTrader,
|
||||
handleSaveEditTrader,
|
||||
handleDeleteTrader,
|
||||
handleToggleTrader,
|
||||
handleAddModel,
|
||||
handleAddExchange,
|
||||
handleModelClick,
|
||||
handleExchangeClick,
|
||||
handleSaveModel,
|
||||
handleDeleteModel,
|
||||
handleSaveExchange,
|
||||
handleDeleteExchange,
|
||||
} = useTraderActions({
|
||||
traders,
|
||||
allModels,
|
||||
allExchanges,
|
||||
supportedModels,
|
||||
supportedExchanges,
|
||||
language,
|
||||
mutateTraders,
|
||||
setAllModels,
|
||||
setAllExchanges,
|
||||
setShowCreateModal,
|
||||
setShowEditModal,
|
||||
setShowModelModal,
|
||||
setShowExchangeModal,
|
||||
setEditingModel,
|
||||
setEditingExchange,
|
||||
editingTrader,
|
||||
setEditingTrader,
|
||||
})
|
||||
|
||||
// 计算派生状态
|
||||
const enabledModels = allModels?.filter((m) => m.enabled) || []
|
||||
const enabledExchanges =
|
||||
allExchanges?.filter((e) => {
|
||||
if (!e.enabled) return false
|
||||
if (e.id === 'aster') {
|
||||
return e.asterUser?.trim() && e.asterSigner?.trim()
|
||||
}
|
||||
if (e.id === 'hyperliquid') {
|
||||
return e.hyperliquidWalletAddr?.trim()
|
||||
}
|
||||
return true
|
||||
}) || []
|
||||
|
||||
// 处理交易员查看
|
||||
const handleTraderSelect = (traderId: string) => {
|
||||
if (onTraderSelect) {
|
||||
onTraderSelect(traderId)
|
||||
} else {
|
||||
navigate(`/dashboard?trader=${traderId}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 md:space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
language={language}
|
||||
tradersCount={traders?.length || 0}
|
||||
configuredModelsCount={configuredModels.length}
|
||||
configuredExchangesCount={configuredExchanges.length}
|
||||
onAddModel={handleAddModel}
|
||||
onAddExchange={handleAddExchange}
|
||||
onCreateTrader={() => setShowCreateModal(true)}
|
||||
/>
|
||||
|
||||
{/* Configuration Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
<AIModelsSection
|
||||
language={language}
|
||||
configuredModels={configuredModels}
|
||||
isModelInUse={isModelInUse}
|
||||
onModelClick={handleModelClick}
|
||||
/>
|
||||
|
||||
<ExchangesSection
|
||||
language={language}
|
||||
configuredExchanges={configuredExchanges}
|
||||
isExchangeInUse={isExchangeInUse}
|
||||
onExchangeClick={handleExchangeClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Traders Grid */}
|
||||
<TradersGrid
|
||||
language={language}
|
||||
traders={traders}
|
||||
onTraderSelect={handleTraderSelect}
|
||||
onEditTrader={handleEditTrader}
|
||||
onDeleteTrader={handleDeleteTrader}
|
||||
onToggleTrader={handleToggleTrader}
|
||||
/>
|
||||
|
||||
{/* Modals */}
|
||||
<TraderConfigModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
isEditMode={false}
|
||||
availableModels={enabledModels}
|
||||
availableExchanges={enabledExchanges}
|
||||
onSave={handleCreateTrader}
|
||||
/>
|
||||
|
||||
<TraderConfigModal
|
||||
isOpen={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
isEditMode={true}
|
||||
traderData={editingTrader}
|
||||
availableModels={enabledModels}
|
||||
availableExchanges={enabledExchanges}
|
||||
onSave={handleSaveEditTrader}
|
||||
/>
|
||||
|
||||
{showModelModal && (
|
||||
<ModelConfigModal
|
||||
allModels={supportedModels}
|
||||
configuredModels={allModels}
|
||||
editingModelId={editingModel}
|
||||
onSave={handleSaveModel}
|
||||
onDelete={handleDeleteModel}
|
||||
onClose={() => setShowModelModal(false)}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showExchangeModal && (
|
||||
<ExchangeConfigModal
|
||||
allExchanges={supportedExchanges}
|
||||
editingExchangeId={editingExchange}
|
||||
onSave={handleSaveExchange}
|
||||
onDelete={handleDeleteExchange}
|
||||
onClose={() => setShowExchangeModal(false)}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
Terminal,
|
||||
Code,
|
||||
Send,
|
||||
Download,
|
||||
Upload,
|
||||
} from 'lucide-react'
|
||||
import type { Strategy, StrategyConfig, AIModel } from '../types'
|
||||
import { confirmToast, notify } from '../lib/notify'
|
||||
@@ -263,6 +265,67 @@ export function StrategyStudioPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Export strategy as JSON file
|
||||
const handleExportStrategy = (strategy: Strategy) => {
|
||||
const exportData = {
|
||||
name: strategy.name,
|
||||
description: strategy.description,
|
||||
config: strategy.config,
|
||||
exported_at: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `strategy_${strategy.name.replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported')
|
||||
}
|
||||
|
||||
// Import strategy from JSON file
|
||||
const handleImportStrategy = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file || !token) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const importData = JSON.parse(text)
|
||||
|
||||
// Validate imported data
|
||||
if (!importData.config || !importData.name) {
|
||||
throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file')
|
||||
}
|
||||
|
||||
// Create new strategy with imported config
|
||||
const response = await fetch(`${API_BASE}/api/strategies`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`,
|
||||
description: importData.description || '',
|
||||
config: importData.config,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to import strategy')
|
||||
|
||||
notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported')
|
||||
await fetchStrategies()
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
|
||||
notify.error(errorMsg)
|
||||
} finally {
|
||||
// Reset file input
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Save strategy
|
||||
const handleSaveStrategy = async () => {
|
||||
if (!token || !selectedStrategy || !editingConfig) return
|
||||
@@ -526,13 +589,26 @@ export function StrategyStudioPage() {
|
||||
<div className="p-2">
|
||||
<div className="flex items-center justify-between mb-2 px-2">
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('strategies')}</span>
|
||||
<button
|
||||
onClick={handleCreateStrategy}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Import button with hidden file input */}
|
||||
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer" style={{ color: '#848E9C' }} title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
|
||||
<Upload className="w-4 h-4" />
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImportStrategy}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
onClick={handleCreateStrategy}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{strategies.map((strategy) => (
|
||||
@@ -554,22 +630,33 @@ export function StrategyStudioPage() {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm truncate" style={{ color: '#EAECEF' }}>{strategy.name}</span>
|
||||
{!strategy.is_default && (
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
>
|
||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-red-500/20"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
title={language === 'zh' ? '导出' : 'Export'}
|
||||
>
|
||||
<Download className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
</button>
|
||||
{!strategy.is_default && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
title={language === 'zh' ? '复制' : 'Duplicate'}
|
||||
>
|
||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-red-500/20"
|
||||
title={language === 'zh' ? '删除' : 'Delete'}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{strategy.is_active && (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user