Files
nofx/web/src/components/strategy/CoinSourceEditor.tsx
tinkle-community 50923f6a2e feat: add DeepVoidBackground and update UI theme across pages
- Add DeepVoidBackground component with animated gradient effects
- Apply nofx theme classes to StrategyStudioPage, AITradersPage, etc.
- Update styling for consistent dark theme with gold accents
- Add PageNotFound and TraderDashboardPage components
2026-01-04 17:49:59 +08:00

402 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { Plus, X, Database, TrendingUp, List, Ban, Zap } from 'lucide-react'
import type { CoinSourceConfig } from '../../types'
interface CoinSourceEditorProps {
config: CoinSourceConfig
onChange: (config: CoinSourceConfig) => void
disabled?: boolean
language: string
}
export function CoinSourceEditor({
config,
onChange,
disabled,
language,
}: CoinSourceEditorProps) {
const [newCoin, setNewCoin] = useState('')
const [newExcludedCoin, setNewExcludedCoin] = useState('')
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
sourceType: { zh: '数据来源类型', en: 'Source Type' },
static: { zh: '静态列表', en: 'Static List' },
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
oi_top: { zh: 'OI Top 持仓增长', en: 'OI Top' },
mixed: { zh: '混合模式', en: 'Mixed Mode' },
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
addCoin: { zh: '添加币种', en: 'Add Coin' },
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
ai500Limit: { zh: '数量上限', en: 'Limit' },
useOITop: { zh: '启用 OI Top 数据', en: 'Enable OI Top' },
oiTopLimit: { zh: '数量上限', en: 'Limit' },
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
ai500Desc: {
zh: '使用 AI500 智能筛选的热门币种',
en: 'Use AI500 smart-filtered popular coins',
},
oiTopDesc: {
zh: '使用持仓量增长最快的币种',
en: 'Use coins with fastest OI growth',
},
mixedDesc: {
zh: '组合多种数据源AI500 + OI Top + 自定义',
en: 'Combine multiple sources: AI500 + OI Top + Custom',
},
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded' },
nofxosNote: { zh: '使用 NofxOS API Key在指标配置中设置', en: 'Uses NofxOS API Key (set in Indicators config)' },
}
return translations[key]?.[language] || key
}
const sourceTypes = [
{ value: 'static', icon: List, color: '#848E9C' },
{ value: 'ai500', icon: Database, color: '#F0B90B' },
{ value: 'oi_top', icon: TrendingUp, color: '#0ECB81' },
{ value: 'mixed', icon: Database, color: '#60a5fa' },
] as const
// xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix
const xyzDexAssets = new Set([
// Stocks
'TSLA', 'NVDA', 'AAPL', 'MSFT', 'META', 'AMZN', 'GOOGL', 'AMD', 'COIN', 'NFLX',
'PLTR', 'HOOD', 'INTC', 'MSTR', 'TSM', 'ORCL', 'MU', 'RIVN', 'COST', 'LLY',
'CRCL', 'SKHX', 'SNDK',
// Forex
'EUR', 'JPY',
// Commodities
'GOLD', 'SILVER',
// Index
'XYZ100',
])
const isXyzDexAsset = (symbol: string): boolean => {
const base = symbol.toUpperCase().replace(/^XYZ:/, '').replace(/USDT$|USD$|-USDC$/, '')
return xyzDexAssets.has(base)
}
const handleAddCoin = () => {
if (!newCoin.trim()) return
const symbol = newCoin.toUpperCase().trim()
// For xyz dex assets (stocks, forex, commodities), use xyz: prefix without USDT
let formattedSymbol: string
if (isXyzDexAsset(symbol)) {
// Remove xyz: prefix (case-insensitive) and any USD suffixes
const base = symbol.replace(/^xyz:/i, '').replace(/USDT$|USD$|-USDC$/i, '')
formattedSymbol = `xyz:${base}`
} else {
formattedSymbol = symbol.endsWith('USDT') ? symbol : `${symbol}USDT`
}
const currentCoins = config.static_coins || []
if (!currentCoins.includes(formattedSymbol)) {
onChange({
...config,
static_coins: [...currentCoins, formattedSymbol],
})
}
setNewCoin('')
}
const handleRemoveCoin = (coin: string) => {
onChange({
...config,
static_coins: (config.static_coins || []).filter((c) => c !== coin),
})
}
const handleAddExcludedCoin = () => {
if (!newExcludedCoin.trim()) return
const symbol = newExcludedCoin.toUpperCase().trim()
// For xyz dex assets, use xyz: prefix without USDT
let formattedSymbol: string
if (isXyzDexAsset(symbol)) {
const base = symbol.replace(/^xyz:/i, '').replace(/USDT$|USD$|-USDC$/i, '')
formattedSymbol = `xyz:${base}`
} else {
formattedSymbol = symbol.endsWith('USDT') ? symbol : `${symbol}USDT`
}
const currentExcluded = config.excluded_coins || []
if (!currentExcluded.includes(formattedSymbol)) {
onChange({
...config,
excluded_coins: [...currentExcluded, formattedSymbol],
})
}
setNewExcludedCoin('')
}
const handleRemoveExcludedCoin = (coin: string) => {
onChange({
...config,
excluded_coins: (config.excluded_coins || []).filter((c) => c !== coin),
})
}
// NofxOS badge component
const NofxOSBadge = () => (
<span
className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30"
>
NofxOS
</span>
)
return (
<div className="space-y-6">
{/* Source Type Selector */}
<div>
<label className="block text-sm font-medium mb-3 text-nofx-text">
{t('sourceType')}
</label>
<div className="grid grid-cols-4 gap-3">
{sourceTypes.map(({ value, icon: Icon, color }) => (
<button
key={value}
onClick={() =>
!disabled &&
onChange({ ...config, source_type: value as CoinSourceConfig['source_type'] })
}
disabled={disabled}
className={`p-4 rounded-lg border transition-all ${config.source_type === value
? 'ring-2 ring-nofx-gold bg-nofx-gold/10'
: 'hover:bg-white/5 bg-nofx-bg'
} border-nofx-gold/20`}
>
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
<div className="text-sm font-medium text-nofx-text">
{t(value)}
</div>
<div className="text-xs mt-1 text-nofx-text-muted">
{t(`${value}Desc`)}
</div>
</button>
))}
</div>
</div>
{/* Static Coins */}
{(config.source_type === 'static' || config.source_type === 'mixed') && (
<div>
<label className="block text-sm font-medium mb-3 text-nofx-text">
{t('staticCoins')}
</label>
<div className="flex flex-wrap gap-2 mb-3">
{(config.static_coins || []).map((coin) => (
<span
key={coin}
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-bg-lighter text-nofx-text"
>
{coin}
{!disabled && (
<button
onClick={() => handleRemoveCoin(coin)}
className="ml-1 hover:text-red-400 transition-colors"
>
<X className="w-3 h-3" />
</button>
)}
</span>
))}
</div>
{!disabled && (
<div className="flex gap-2">
<input
type="text"
value={newCoin}
onChange={(e) => setNewCoin(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddCoin()}
placeholder="BTC, ETH, SOL..."
className="flex-1 px-4 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
<button
onClick={handleAddCoin}
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
>
<Plus className="w-4 h-4" />
{t('addCoin')}
</button>
</div>
)}
</div>
)}
{/* Excluded Coins */}
<div>
<div className="flex items-center gap-2 mb-3">
<Ban className="w-4 h-4 text-nofx-danger" />
<label className="text-sm font-medium text-nofx-text">
{t('excludedCoins')}
</label>
</div>
<p className="text-xs mb-3 text-nofx-text-muted">
{t('excludedCoinsDesc')}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{(config.excluded_coins || []).map((coin) => (
<span
key={coin}
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-danger/15 text-nofx-danger"
>
{coin}
{!disabled && (
<button
onClick={() => handleRemoveExcludedCoin(coin)}
className="ml-1 hover:text-white transition-colors"
>
<X className="w-3 h-3" />
</button>
)}
</span>
))}
{(config.excluded_coins || []).length === 0 && (
<span className="text-xs italic text-nofx-text-muted">
{language === 'zh' ? '无' : 'None'}
</span>
)}
</div>
{!disabled && (
<div className="flex gap-2">
<input
type="text"
value={newExcludedCoin}
onChange={(e) => setNewExcludedCoin(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddExcludedCoin()}
placeholder="BTC, ETH, DOGE..."
className="flex-1 px-4 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
<button
onClick={handleAddExcludedCoin}
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm bg-nofx-danger text-white hover:bg-red-600"
>
<Ban className="w-4 h-4" />
{t('addExcludedCoin')}
</button>
</div>
)}
</div>
{/* AI500 Options */}
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
<div
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-nofx-gold" />
<span className="text-sm font-medium text-nofx-text">
AI500 {t('dataSourceConfig')}
</span>
<NofxOSBadge />
</div>
</div>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={config.use_ai500}
onChange={(e) =>
!disabled && onChange({ ...config, use_ai500: e.target.checked })
}
disabled={disabled}
className="w-5 h-5 rounded accent-nofx-gold"
/>
<span className="text-nofx-text">{t('useAI500')}</span>
</label>
{config.use_ai500 && (
<div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted">
{t('ai500Limit')}:
</span>
<select
value={config.ai500_limit || 10}
onChange={(e) =>
!disabled &&
onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
}
disabled={disabled}
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
{[5, 10, 15, 20, 30, 50].map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
)}
<p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')}
</p>
</div>
</div>
)}
{/* OI Top Options */}
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
<div
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-nofx-success" />
<span className="text-sm font-medium text-nofx-text">
OI Top {t('dataSourceConfig')}
</span>
<NofxOSBadge />
</div>
</div>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={config.use_oi_top}
onChange={(e) =>
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
}
disabled={disabled}
className="w-5 h-5 rounded accent-nofx-success"
/>
<span className="text-nofx-text">{t('useOITop')}</span>
</label>
{config.use_oi_top && (
<div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted">
{t('oiTopLimit')}:
</span>
<select
value={config.oi_top_limit || 20}
onChange={(e) =>
!disabled &&
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
}
disabled={disabled}
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
{[5, 10, 15, 20, 30, 50].map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
)}
<p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')}
</p>
</div>
</div>
)}
</div>
)
}