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:
tinkle-community
2025-12-09 16:46:58 +08:00
parent 48792907b3
commit 9fa2432705
20 changed files with 258 additions and 2348 deletions

View File

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

View File

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