diff --git a/web/src/components/ChartTabs.tsx b/web/src/components/ChartTabs.tsx index bdefb69771..f4c46455c8 100644 --- a/web/src/components/ChartTabs.tsx +++ b/web/src/components/ChartTabs.tsx @@ -3,27 +3,29 @@ import { EquityChart } from './EquityChart' import { AdvancedChart } from './AdvancedChart' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' +import { chartTabs } from '../i18n/strategy-translations' import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' +const tTabs = (key: keyof typeof chartTabs, language: string): string => { + const entry = chartTabs[key] + if (!entry) return key + return entry[language as keyof typeof entry] || entry.en || key +} + interface ChartTabsProps { traderId: string - selectedSymbol?: string // 从外部选择的币种 - updateKey?: number // 强制更新的 key - exchangeId?: string // 交易所ID + selectedSymbol?: string + updateKey?: number + exchangeId?: string } type ChartTab = 'equity' | 'kline' type Interval = '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d' type MarketType = 'hyperliquid' | 'crypto' | 'stocks' | 'forex' | 'metals' -interface SymbolInfo { - symbol: string - name: string - category: string -} +interface SymbolInfo { symbol: string; name: string; category: string } -// 市场类型配置 const MARKET_CONFIG = { hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', label: { zh: 'HL', en: 'HL' }, color: 'cyan', hasDropdown: true }, crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', label: { zh: '加密', en: 'Crypto' }, color: 'yellow', hasDropdown: false }, @@ -33,21 +35,14 @@ const MARKET_CONFIG = { } const INTERVALS: { value: Interval; label: string }[] = [ - { value: '1m', label: '1m' }, - { value: '5m', label: '5m' }, - { value: '15m', label: '15m' }, - { value: '30m', label: '30m' }, - { value: '1h', label: '1h' }, - { value: '4h', label: '4h' }, - { value: '1d', label: '1d' }, + { value: '1m', label: '1m' }, { value: '5m', label: '5m' }, { value: '15m', label: '15m' }, + { value: '30m', label: '30m' }, { value: '1h', label: '1h' }, { value: '4h', label: '4h' }, { value: '1d', label: '1d' }, ] -// 根据交易所ID推断市场类型 function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType { if (!exchangeId) return 'hyperliquid' const lower = exchangeId.toLowerCase() if (lower.includes('hyperliquid')) return 'hyperliquid' - // 其他交易所默认使用 crypto 类型 return 'crypto' } @@ -63,25 +58,17 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C const [searchFilter, setSearchFilter] = useState('') const dropdownRef = useRef(null) - // 当交易所ID变化时,自动切换市场类型 - useEffect(() => { - const newMarketType = getMarketTypeFromExchange(exchangeId) - setMarketType(newMarketType) - }, [exchangeId]) + useEffect(() => { setMarketType(getMarketTypeFromExchange(exchangeId)) }, [exchangeId]) - // 根据市场类型确定交易所 const marketConfig = MARKET_CONFIG[marketType] - // 优先使用传入的 exchangeId(非 hyperliquid 时) const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange) - // 获取可用币种列表 useEffect(() => { if (marketConfig.hasDropdown) { fetch(`/api/symbols?exchange=${marketConfig.exchange}`) .then(res => res.json()) .then(data => { if (data.symbols) { - // 按类别排序: crypto > stock > forex > commodity > index const categoryOrder: Record = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 } const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => { const orderA = categoryOrder[a.category] ?? 5 @@ -96,7 +83,6 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C } }, [marketType, marketConfig.exchange, marketConfig.hasDropdown]) - // 点击外部关闭下拉 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -107,96 +93,50 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C return () => document.removeEventListener('mousedown', handleClickOutside) }, []) - // 切换市场类型时更新默认符号 const handleMarketTypeChange = (type: MarketType) => { setMarketType(type) setChartSymbol(MARKET_CONFIG[type].defaultSymbol) setShowDropdown(false) } - // 过滤后的币种列表 - const filteredSymbols = availableSymbols.filter(s => - s.symbol.toLowerCase().includes(searchFilter.toLowerCase()) - ) + const filteredSymbols = availableSymbols.filter(s => s.symbol.toLowerCase().includes(searchFilter.toLowerCase())) - // 当从外部选择币种时,自动切换到K线图 useEffect(() => { if (selectedSymbol) { - console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey) setChartSymbol(selectedSymbol) setActiveTab('kline') } }, [selectedSymbol, updateKey]) - // 处理手动输入符号 const handleSymbolSubmit = (e: React.FormEvent) => { e.preventDefault() if (symbolInput.trim()) { let symbol = symbolInput.trim().toUpperCase() - // 加密货币自动加 USDT 后缀 - if (marketType === 'crypto' && !symbol.endsWith('USDT')) { - symbol = symbol + 'USDT' - } + if (marketType === 'crypto' && !symbol.endsWith('USDT')) { symbol = symbol + 'USDT' } setChartSymbol(symbol) setSymbolInput('') } } - console.log('[ChartTabs] rendering, activeTab:', activeTab) - return ( -
- {/* - Premium Professional Toolbar - Mobile: Single row, horizontal scroll with gradient mask - Desktop: Standard flex-wrap/nowrap - */} -
- {/* Left: Tab Switcher */} +
+
- - - - - {/* Market Type Pills - Only when kline active, HIDDEN on mobile to save space */} {activeTab === 'kline' && (
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => { const config = MARKET_CONFIG[type] const isActive = marketType === type return ( - @@ -205,18 +145,12 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
)}
- - {/* Right: Symbol + Interval */} {activeTab === 'kline' && (
- {/* Symbol Dropdown */}
{marketConfig.hasDropdown ? ( <> - @@ -225,14 +159,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
- setSearchFilter(e.target.value)} - placeholder="Search symbol..." - className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none font-mono" - autoFocus - /> + setSearchFilter(e.target.value)} placeholder="Search symbol..." className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none font-mono" autoFocus />
@@ -244,11 +171,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
{labels[category]}
{categorySymbols.map(s => ( - @@ -264,71 +187,27 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C {chartSymbol} )}
- - {/* Interval Selector - Allow scrolling if needed */}
{INTERVALS.map((int) => ( - + ))}
- - {/* Quick Input - Hidden on mobile, dropdown search is enough */}
- setSymbolInput(e.target.value)} - placeholder="Sym" - className="w-16 px-2 py-1 bg-black/40 border border-white/10 rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors" - /> - + setSymbolInput(e.target.value)} placeholder="Sym" className="w-16 px-2 py-1 bg-black/40 border border-white/10 rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors" /> +
)}
- - {/* Tab Content - Chart autosizes to this container */}
{activeTab === 'equity' ? ( - + ) : ( - - + + )} diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 328bf3ca1f..e2bdf8d685 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -125,7 +125,7 @@ export function TraderConfigModal({ const handleFetchCurrentBalance = async () => { if (!isEditMode || !traderData?.trader_id) { - setBalanceFetchError('只有在编辑模式下才能获取当前余额') + setBalanceFetchError(t('fetchBalanceEditModeOnly', language)) return } @@ -142,13 +142,13 @@ export function TraderConfigModal({ const currentBalance = result.data.total_equity || result.data.balance || 0 setFormData((prev) => ({ ...prev, initial_balance: currentBalance })) - toast.success('已获取当前余额') + toast.success(t('balanceFetched', language)) } else { - throw new Error(result.message || '获取余额失败') + throw new Error(result.message || t('balanceFetchFailed', language)) } } catch (error) { - console.error('获取余额失败:', error) - setBalanceFetchError('获取余额失败,请检查网络连接') + console.error(t('balanceFetchFailed', language) + ':', error) + setBalanceFetchError(t('balanceFetchNetworkError', language)) } finally { setIsFetchingBalance(false) } @@ -175,13 +175,13 @@ export function TraderConfigModal({ } await toast.promise(onSave(saveData), { - loading: '正在保存…', - success: '保存成功', - error: '保存失败', + loading: t('saving', language), + success: t('saveSuccess', language), + error: t('saveFailed', language), }) onClose() } catch (error) { - console.error('保存失败:', error) + console.error(t('saveFailed', language) + ':', error) } finally { setIsSaving(false) } @@ -208,10 +208,10 @@ export function TraderConfigModal({

- {isEditMode ? '修改交易员' : '创建交易员'} + {isEditMode ? t('editTrader', language) : t('createTrader', language)}

- {isEditMode ? '修改交易员配置' : '选择策略并配置基础参数'} + {isEditMode ? t('editTraderConfig', language) : t('selectStrategyAndConfigParams', language)}

@@ -231,12 +231,12 @@ export function TraderConfigModal({ {/* Basic Info */}

- 1 基础配置 + 1 {t('basicConfig', language)}

- 还没有交易所账号?点击注册 + {t('noExchangeAccount', language)} {regLink.hasReferral && ( - 折扣优惠 + {t('discount', language)} )} @@ -318,13 +318,13 @@ export function TraderConfigModal({ {/* Strategy Selection */}

- 2 选择交易策略 + 2 {t('selectTradingStrategy', language)}

{strategies.length === 0 && ( -

- 暂无策略,请先在策略工作室创建策略 +

+ {t('noStrategyHint', language)}

)}
@@ -354,25 +354,25 @@ export function TraderConfigModal({
- 策略详情 + {t('strategyDetails', language)} {selectedStrategy.is_active && ( - 激活中 + {t('activating', language)} )}

- {selectedStrategy.description || '无描述'} + {selectedStrategy.description || (language === 'zh' ? '无描述' : 'No description')}

- 币种来源: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' : + {t('coinSource', language)}: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' : selectedStrategy.config.coin_source.source_type === 'ai500' ? 'AI500' : selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
- 保证金上限: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}% + {t('marginLimit', language)}: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
@@ -383,13 +383,13 @@ export function TraderConfigModal({ {/* Trading Parameters */}

- 3 交易参数 + 3 {t('tradingParams', language)}

@@ -446,7 +446,7 @@ export function TraderConfigModal({ {/* Competition visibility */}
-

- 隐藏后将不在竞技场页面显示此交易员 +

+ {t('hiddenInCompetition', language)}

@@ -482,7 +482,7 @@ export function TraderConfigModal({
-

- 用于手动更新初始余额基准(例如充值/提现后) +

+ {t('balanceUpdateHint', language)}

{balanceFetchError && (

@@ -535,7 +535,7 @@ export function TraderConfigModal({ - 系统将自动获取您的账户净值作为初始余额 + {t('autoFetchBalanceInfo', language)}

)} @@ -550,7 +550,7 @@ export function TraderConfigModal({ onClick={onClose} className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]" > - 取消 + {t('cancel', language)} {onSave && ( )}
diff --git a/web/src/components/strategy/CoinSourceEditor.tsx b/web/src/components/strategy/CoinSourceEditor.tsx index 68d48d2b71..2a6fcb8a10 100644 --- a/web/src/components/strategy/CoinSourceEditor.tsx +++ b/web/src/components/strategy/CoinSourceEditor.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react' import type { CoinSourceConfig } from '../../types' +import { coinSource } from '../../i18n/strategy-translations' interface CoinSourceEditorProps { config: CoinSourceConfig @@ -9,6 +10,13 @@ interface CoinSourceEditorProps { language: string } +// Helper function to get translation from centralized file +const t = (key: keyof typeof coinSource, language: string): string => { + const entry = coinSource[key] + if (!entry) return key + return entry[language as keyof typeof entry] || entry.en || key +} + export function CoinSourceEditor({ config, onChange, @@ -18,52 +26,6 @@ export function CoinSourceEditor({ const [newCoin, setNewCoin] = useState('') const [newExcludedCoin, setNewExcludedCoin] = useState('') - const t = (key: string) => { - const translations: Record> = { - sourceType: { zh: '数据来源类型', en: 'Source Type' }, - static: { zh: '静态列表', en: 'Static List' }, - ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' }, - oi_top: { zh: 'OI 持仓增加', en: 'OI Increase' }, - oi_low: { zh: 'OI 持仓减少', en: 'OI Decrease' }, - 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 持仓增加榜', en: 'Enable OI Increase' }, - oiTopLimit: { zh: '数量上限', en: 'Limit' }, - useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease' }, - oiLowLimit: { zh: '数量上限', en: 'Limit' }, - staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' }, - ai500Desc: { - zh: '使用 AI500 智能筛选的热门币种', - en: 'Use AI500 smart-filtered popular coins', - }, - oiTopDesc: { - zh: '持仓增加榜,适合做多', - en: 'OI increase ranking, for long', - }, - oi_lowDesc: { - zh: '持仓减少榜,适合做空', - en: 'OI decrease ranking, for short', - }, - mixedDesc: { - zh: '组合多种数据源', - en: 'Combine multiple sources', - }, - mixedConfig: { zh: '组合数据源配置', en: 'Combined Sources Configuration' }, - mixedSummary: { zh: '已选组合', en: 'Selected Sources' }, - maxCoins: { zh: '最多', en: 'Up to' }, - coins: { zh: '个币种', en: 'coins' }, - 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' }, @@ -97,18 +59,11 @@ export function CoinSourceEditor({ return { sources, totalLimit } } - // xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix + // xyz dex assets 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', + 'CRCL', 'SKHX', 'SNDK', 'EUR', 'JPY', 'GOLD', 'SILVER', 'XYZ100', ]) const isXyzDexAsset = (symbol: string): boolean => { @@ -119,11 +74,8 @@ export function CoinSourceEditor({ 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 { @@ -132,26 +84,18 @@ export function CoinSourceEditor({ const currentCoins = config.static_coins || [] if (!currentCoins.includes(formattedSymbol)) { - onChange({ - ...config, - static_coins: [...currentCoins, formattedSymbol], - }) + onChange({ ...config, static_coins: [...currentCoins, formattedSymbol] }) } setNewCoin('') } const handleRemoveCoin = (coin: string) => { - onChange({ - ...config, - static_coins: (config.static_coins || []).filter((c) => c !== coin), - }) + 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, '') @@ -162,45 +106,32 @@ export function CoinSourceEditor({ const currentExcluded = config.excluded_coins || [] if (!currentExcluded.includes(formattedSymbol)) { - onChange({ - ...config, - excluded_coins: [...currentExcluded, formattedSymbol], - }) + onChange({ ...config, excluded_coins: [...currentExcluded, formattedSymbol] }) } setNewExcludedCoin('') } const handleRemoveExcludedCoin = (coin: string) => { - onChange({ - ...config, - excluded_coins: (config.excluded_coins || []).filter((c) => c !== coin), - }) + onChange({ ...config, excluded_coins: (config.excluded_coins || []).filter((c) => c !== coin) }) } - // NofxOS badge component const NofxOSBadge = () => ( - + NofxOS ) return (
- {/* Source Type Selector */}
{sourceTypes.map(({ value, icon: Icon, color }) => ( ))}
- {/* Static Coins - only for static mode */} {config.source_type === 'static' && (
{(config.static_coins || []).map((coin) => ( - + {coin} {!disabled && ( - )} @@ -258,45 +179,32 @@ export function CoinSourceEditor({ className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500" > - {t('addCoin')} + {t('addCoin', language)}
)}
)} - {/* Excluded Coins */}
- +
-

- {t('excludedCoinsDesc')} -

+

{t('excludedCoinsDesc', language)}

{(config.excluded_coins || []).map((coin) => ( - + {coin} {!disabled && ( - )} ))} {(config.excluded_coins || []).length === 0 && ( - - {language === 'zh' ? '无' : 'None'} - + {language === 'zh' ? '无' : 'None'} )}
{!disabled && ( @@ -314,420 +222,177 @@ export function CoinSourceEditor({ 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" > - {t('addExcludedCoin')} + {t('addExcludedCoin', language)}
)}
- {/* AI500 Options - only for ai500 mode */} {config.source_type === 'ai500' && ( -
+
- - AI500 {t('dataSourceConfig')} - + AI500 {t('dataSourceConfig', language)}
-
- {config.use_ai500 && (
- - {t('ai500Limit')}: - + {t('ai500Limit', language)}:
)} - -

- {t('nofxosNote')} -

+

{t('nofxosNote', language)}

)} - {/* OI Top Options - only for oi_top mode */} {config.source_type === 'oi_top' && ( -
+
- OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')} + OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig', language)}
-
- {config.use_oi_top && (
- - {t('oiTopLimit')}: - + {t('oiTopLimit', language)}:
)} - -

- {t('nofxosNote')} -

+

{t('nofxosNote', language)}

)} - {/* OI Low Options - only for oi_low mode */} {config.source_type === 'oi_low' && ( -
+
- OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')} + OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig', language)}
-
- {config.use_oi_low && (
- - {t('oiLowLimit')}: - + {t('oiLowLimit', language)}:
)} - -

- {t('nofxosNote')} -

+

{t('nofxosNote', language)}

)} - {/* Mixed Mode - Unified Card Selector */} {config.source_type === 'mixed' && (
- - {t('mixedConfig')} - + {t('mixedConfig', language)}
- - {/* 4 Source Cards in 2x2 Grid */}
- {/* AI500 Card */} -
!disabled && onChange({ ...config, use_ai500: !config.use_ai500 })} - > +
!disabled && onChange({ ...config, use_ai500: !config.use_ai500 })}>
- !disabled && onChange({ ...config, use_ai500: e.target.checked })} - disabled={disabled} - className="w-4 h-4 rounded accent-nofx-gold" - onClick={(e) => e.stopPropagation()} - /> + { e.stopPropagation(); !disabled && onChange({ ...config, use_ai500: e.target.checked }) }} disabled={disabled} className="w-4 h-4 rounded accent-nofx-gold" onClick={(e) => e.stopPropagation()} /> AI500
- {config.use_ai500 && ( -
- Limit: - -
- )}
- - {/* OI Top Card */} -
!disabled && onChange({ ...config, use_oi_top: !config.use_oi_top })} - > +
!disabled && onChange({ ...config, use_oi_top: !config.use_oi_top })}>
- !disabled && onChange({ ...config, use_oi_top: e.target.checked })} - disabled={disabled} - className="w-4 h-4 rounded accent-nofx-success" - onClick={(e) => e.stopPropagation()} - /> + { e.stopPropagation(); !disabled && onChange({ ...config, use_oi_top: e.target.checked }) }} disabled={disabled} className="w-4 h-4 rounded accent-nofx-success" onClick={(e) => e.stopPropagation()} /> - - {language === 'zh' ? 'OI 增加' : 'OI Increase'} - + {language === 'zh' ? 'OI 增加' : 'OI Increase'}
-

- {language === 'zh' ? '适合做多' : 'For long'} -

- {config.use_oi_top && ( -
- Limit: - -
- )}
- - {/* OI Low Card */} -
!disabled && onChange({ ...config, use_oi_low: !config.use_oi_low })} - > +
!disabled && onChange({ ...config, use_oi_low: !config.use_oi_low })}>
- !disabled && onChange({ ...config, use_oi_low: e.target.checked })} - disabled={disabled} - className="w-4 h-4 rounded accent-red-500" - onClick={(e) => e.stopPropagation()} - /> + { e.stopPropagation(); !disabled && onChange({ ...config, use_oi_low: e.target.checked }) }} disabled={disabled} className="w-4 h-4 rounded accent-red-500" onClick={(e) => e.stopPropagation()} /> - - {language === 'zh' ? 'OI 减少' : 'OI Decrease'} - + {language === 'zh' ? 'OI 减少' : 'OI Decrease'}
-

- {language === 'zh' ? '适合做空' : 'For short'} -

- {config.use_oi_low && ( -
- Limit: - -
- )}
- - {/* Static/Custom Card */} -
0 - ? 'bg-gray-500/10 border-gray-500/50' - : 'bg-nofx-bg border-nofx-border hover:border-gray-500/30' - }`} - > +
0 ? 'bg-gray-500/10 border-gray-500/50' : 'bg-nofx-bg border-nofx-border'}`}>
- - {language === 'zh' ? '自定义' : 'Custom'} - - {(config.static_coins || []).length > 0 && ( - - {config.static_coins?.length} - - )} -
-
- {(config.static_coins || []).slice(0, 3).map((coin) => ( - - {coin} - {!disabled && ( - - )} - - ))} - {(config.static_coins || []).length > 3 && ( - - +{(config.static_coins?.length || 0) - 3} - - )} + {language === 'zh' ? '自定义' : 'Custom'} + {(config.static_coins || []).length > 0 && {config.static_coins?.length}}
- {!disabled && ( -
- 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" - /> - -
- )}
- - {/* Summary */} {(() => { const { sources, totalLimit } = getMixedSummary() if (sources.length === 0) return null return (
- {t('mixedSummary')}: - - {sources.join(' + ')} - -
-
- {t('maxCoins')} {totalLimit} {t('coins')} + {t('mixedSummary', language)}: + {sources.join(' + ')}
+
{t('maxCoins', language)} {totalLimit} {t('coins', language)}
) })()} diff --git a/web/src/components/strategy/GridConfigEditor.tsx b/web/src/components/strategy/GridConfigEditor.tsx index 5c7dd73693..a791ade044 100644 --- a/web/src/components/strategy/GridConfigEditor.tsx +++ b/web/src/components/strategy/GridConfigEditor.tsx @@ -1,530 +1,191 @@ import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react' import type { GridStrategyConfig } from '../../types' +import { gridConfig } from '../../i18n/strategy-translations' -interface GridConfigEditorProps { - config: GridStrategyConfig - onChange: (config: GridStrategyConfig) => void - disabled?: boolean - language: string +const defaultGridConfig: GridStrategyConfig = { + symbol: 'BTCUSDT', grid_count: 10, total_investment: 1000, leverage: 5, + upper_price: 0, lower_price: 0, use_atr_bounds: true, atr_multiplier: 2.0, + distribution: 'gaussian', max_drawdown_pct: 15, stop_loss_pct: 5, + daily_loss_limit_pct: 10, use_maker_only: true, enable_direction_adjust: false, direction_bias_ratio: 0.7, } -// Default grid config -export const defaultGridConfig: GridStrategyConfig = { - symbol: 'BTCUSDT', - grid_count: 10, - total_investment: 1000, - leverage: 5, - upper_price: 0, - lower_price: 0, - use_atr_bounds: true, - atr_multiplier: 2.0, - distribution: 'gaussian', - max_drawdown_pct: 15, - stop_loss_pct: 5, - daily_loss_limit_pct: 10, - use_maker_only: true, - enable_direction_adjust: false, - direction_bias_ratio: 0.7, +const t = (key: keyof typeof gridConfig, language: string): string => { + const entry = gridConfig[key] + if (!entry) return key + return entry[language as keyof typeof entry] || entry.en || key } -export function GridConfigEditor({ - config, - onChange, - disabled, - language, -}: GridConfigEditorProps) { - const t = (key: string) => { - const translations: Record> = { - // Section titles - tradingPair: { zh: '交易设置', en: 'Trading Setup' }, - gridParameters: { zh: '网格参数', en: 'Grid Parameters' }, - priceBounds: { zh: '价格边界', en: 'Price Bounds' }, - riskControl: { zh: '风险控制', en: 'Risk Control' }, - - // Trading pair - symbol: { zh: '交易对', en: 'Trading Pair' }, - symbolDesc: { zh: '选择要进行网格交易的交易对', en: 'Select trading pair for grid trading' }, - - // Investment - totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' }, - totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' }, - leverage: { zh: '杠杆倍数', en: 'Leverage' }, - leverageDesc: { zh: '交易使用的杠杆倍数 (1-5)', en: 'Leverage for trading (1-5)' }, - - // Grid parameters - gridCount: { zh: '网格数量', en: 'Grid Count' }, - gridCountDesc: { zh: '网格层级数量 (5-50)', en: 'Number of grid levels (5-50)' }, - distribution: { zh: '资金分配方式', en: 'Distribution' }, - distributionDesc: { zh: '网格层级的资金分配方式', en: 'Fund allocation across grid levels' }, - uniform: { zh: '均匀分配', en: 'Uniform' }, - gaussian: { zh: '高斯分配 (推荐)', en: 'Gaussian (Recommended)' }, - pyramid: { zh: '金字塔分配', en: 'Pyramid' }, - - // Price bounds - useAtrBounds: { zh: '自动计算边界 (ATR)', en: 'Auto-calculate Bounds (ATR)' }, - useAtrBoundsDesc: { zh: '基于 ATR 自动计算网格上下边界', en: 'Auto-calculate bounds based on ATR' }, - atrMultiplier: { zh: 'ATR 倍数', en: 'ATR Multiplier' }, - atrMultiplierDesc: { zh: '边界距离当前价格的 ATR 倍数', en: 'ATR multiplier for bounds distance' }, - upperPrice: { zh: '上边界价格', en: 'Upper Price' }, - upperPriceDesc: { zh: '网格上边界价格 (0=自动计算)', en: 'Grid upper bound (0=auto)' }, - lowerPrice: { zh: '下边界价格', en: 'Lower Price' }, - lowerPriceDesc: { zh: '网格下边界价格 (0=自动计算)', en: 'Grid lower bound (0=auto)' }, - - // Risk control - maxDrawdown: { zh: '最大回撤 (%)', en: 'Max Drawdown (%)' }, - maxDrawdownDesc: { zh: '触发紧急退出的最大回撤百分比', en: 'Max drawdown before emergency exit' }, - stopLoss: { zh: '止损 (%)', en: 'Stop Loss (%)' }, - stopLossDesc: { zh: '单仓位止损百分比', en: 'Stop loss per position' }, - dailyLossLimit: { zh: '日损失限制 (%)', en: 'Daily Loss Limit (%)' }, - dailyLossLimitDesc: { zh: '每日最大亏损百分比', en: 'Maximum daily loss percentage' }, - useMakerOnly: { zh: '仅使用 Maker 订单', en: 'Maker Only Orders' }, - useMakerOnlyDesc: { zh: '使用限价单以降低手续费', en: 'Use limit orders for lower fees' }, - - // Direction adjustment - directionAdjust: { zh: '方向自动调整', en: 'Direction Auto-Adjust' }, - enableDirectionAdjust: { zh: '启用方向调整', en: 'Enable Direction Adjust' }, - enableDirectionAdjustDesc: { zh: '根据箱体突破自动调整网格方向', en: 'Auto-adjust grid direction based on box breakouts' }, - directionBiasRatio: { zh: '偏向强度', en: 'Bias Strength' }, - directionBiasRatioDesc: { zh: '偏多/偏空模式的强度', en: 'Strength for long_bias/short_bias modes' }, - directionBiasExplain: { zh: '偏多模式:X%买 + (100-X)%卖 | 偏空模式:(100-X)%买 + X%卖', en: 'Long bias: X% buy + (100-X)% sell | Short bias: (100-X)% buy + X% sell' }, - directionExplain: { zh: '短期箱体突破 → 偏向,中期箱体突破 → 全仓,价格回归 → 逐步恢复中性', en: 'Short box breakout → bias, Mid box breakout → full, Price return → gradually recover to neutral' }, - directionModes: { zh: '方向模式说明', en: 'Direction Modes' }, - modeNeutral: { zh: '中性:50%买 + 50%卖(默认)', en: 'Neutral: 50% buy + 50% sell (default)' }, - modeLongBias: { zh: '偏多:X%买 + (100-X)%卖', en: 'Long Bias: X% buy + (100-X)% sell' }, - modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell' }, - modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell' }, - modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell' }, - } - return translations[key]?.[language] || key - } - - const updateField = ( - key: K, - value: GridStrategyConfig[K] - ) => { - if (!disabled) { - onChange({ ...config, [key]: value }) - } - } - - const inputStyle = { - background: '#1E2329', - border: '1px solid #2B3139', - color: '#EAECEF', +export function GridConfigEditor({ config, onChange, disabled, language }: { + config: GridStrategyConfig + onChange: (config: GridStrategyConfig) => void + disabled?: boolean + language: string +}) { + const updateField = (key: K, value: GridStrategyConfig[K]) => { + if (!disabled) onChange({ ...config, [key]: value }) } - const sectionStyle = { - background: '#0B0E11', - border: '1px solid #2B3139', - } + const inputStyle = { background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' } + const sectionStyle = { background: '#0B0E11', border: '1px solid #2B3139' } return (
- {/* Trading Setup */}
-

- {t('tradingPair')} -

+

{t('tradingPair', language)}

-
- {/* Symbol */}
- -

- {t('symbolDesc')} -

- updateField('symbol', e.target.value)} disabled={disabled} className="w-full px-3 py-2 rounded" style={inputStyle}> + +
- - {/* Investment */}
- -

- {t('totalInvestmentDesc')} -

- updateField('total_investment', parseFloat(e.target.value) || 1000)} - disabled={disabled} - min={100} - step={100} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('totalInvestmentDesc', language)}

+ updateField('total_investment', parseFloat(e.target.value) || 1000)} disabled={disabled} min={100} step={100} className="w-full px-3 py-2 rounded" style={inputStyle} />
- - {/* Leverage */}
- -

- {t('leverageDesc')} -

- updateField('leverage', parseInt(e.target.value) || 5)} - disabled={disabled} - min={1} - max={5} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('leverageDesc', language)}

+ updateField('leverage', parseInt(e.target.value) || 5)} disabled={disabled} min={1} max={5} className="w-full px-3 py-2 rounded" style={inputStyle} />
- - {/* Grid Parameters */}
-

- {t('gridParameters')} -

+

{t('gridParameters', language)}

-
- {/* Grid Count */}
- -

- {t('gridCountDesc')} -

- updateField('grid_count', parseInt(e.target.value) || 10)} - disabled={disabled} - min={5} - max={50} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('gridCountDesc', language)}

+ updateField('grid_count', parseInt(e.target.value) || 10)} disabled={disabled} min={5} max={50} className="w-full px-3 py-2 rounded" style={inputStyle} />
- - {/* Distribution */}
- -

- {t('distributionDesc')} -

- updateField('distribution', e.target.value as 'uniform' | 'gaussian' | 'pyramid')} disabled={disabled} className="w-full px-3 py-2 rounded" style={inputStyle}> +
- - {/* Price Bounds */}
-

- {t('priceBounds')} -

+

{t('priceBounds', language)}

- - {/* ATR Toggle */}
- -

- {t('useAtrBoundsDesc')} -

+ +

{t('useAtrBoundsDesc', language)}

- {config.use_atr_bounds ? (
- -

- {t('atrMultiplierDesc')} -

- updateField('atr_multiplier', parseFloat(e.target.value) || 2.0)} - disabled={disabled} - min={1} - max={5} - step={0.5} - className="w-32 px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('atrMultiplierDesc', language)}

+ updateField('atr_multiplier', parseFloat(e.target.value) || 2.0)} disabled={disabled} min={1} max={5} step={0.5} className="w-32 px-3 py-2 rounded" style={inputStyle} />
) : (
- -

- {t('upperPriceDesc')} -

- updateField('upper_price', parseFloat(e.target.value) || 0)} - disabled={disabled} - min={0} - step={0.01} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('upperPriceDesc', language)}

+ updateField('upper_price', parseFloat(e.target.value) || 0)} disabled={disabled} min={0} step={0.01} className="w-full px-3 py-2 rounded" style={inputStyle} />
- -

- {t('lowerPriceDesc')} -

- updateField('lower_price', parseFloat(e.target.value) || 0)} - disabled={disabled} - min={0} - step={0.01} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('lowerPriceDesc', language)}

+ updateField('lower_price', parseFloat(e.target.value) || 0)} disabled={disabled} min={0} step={0.01} className="w-full px-3 py-2 rounded" style={inputStyle} />
)}
- - {/* Risk Control */}
-

- {t('riskControl')} -

+

{t('riskControl', language)}

-
- -

- {t('maxDrawdownDesc')} -

- updateField('max_drawdown_pct', parseFloat(e.target.value) || 15)} - disabled={disabled} - min={5} - max={50} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('maxDrawdownDesc', language)}

+ updateField('max_drawdown_pct', parseFloat(e.target.value) || 15)} disabled={disabled} min={5} max={50} className="w-full px-3 py-2 rounded" style={inputStyle} />
-
- -

- {t('stopLossDesc')} -

- updateField('stop_loss_pct', parseFloat(e.target.value) || 5)} - disabled={disabled} - min={1} - max={20} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('stopLossDesc', language)}

+ updateField('stop_loss_pct', parseFloat(e.target.value) || 5)} disabled={disabled} min={1} max={20} className="w-full px-3 py-2 rounded" style={inputStyle} />
-
- -

- {t('dailyLossLimitDesc')} -

- updateField('daily_loss_limit_pct', parseFloat(e.target.value) || 10)} - disabled={disabled} - min={1} - max={30} - className="w-full px-3 py-2 rounded" - style={inputStyle} - /> + +

{t('dailyLossLimitDesc', language)}

+ updateField('daily_loss_limit_pct', parseFloat(e.target.value) || 10)} disabled={disabled} min={1} max={30} className="w-full px-3 py-2 rounded" style={inputStyle} />
- - {/* Maker Only Toggle */}
- -

- {t('useMakerOnlyDesc')} -

+ +

{t('useMakerOnlyDesc', language)}

- - {/* Direction Auto-Adjust */}
-

- {t('directionAdjust')} -

+

{t('directionAdjust', language)}

- - {/* Enable Toggle */}
- -

- {t('enableDirectionAdjustDesc')} -

+ +

{t('enableDirectionAdjustDesc', language)}

- {config.enable_direction_adjust && ( <> - {/* Direction Modes Explanation */}
-

- 📊 {t('directionModes')} -

+

📊 {t('directionModes', language)}

-
• {t('modeNeutral')}
-
{t('modeLongBias')}
-
{t('modeLong')}
-
{t('modeShortBias')}
-
{t('modeShort')}
+
• {t('modeNeutral', language)}
+
{t('modeLongBias', language)}
+
{t('modeLong', language)}
+
{t('modeShortBias', language)}
+
{t('modeShort', language)}
-

- 💡 {t('directionExplain')} -

+

💡 {t('directionExplain', language)}

- - {/* Bias Strength */}
- -

- {t('directionBiasRatioDesc')} -

-

- {t('directionBiasExplain')} -

+ +

{t('directionBiasRatioDesc', language)}

+

{t('directionBiasExplain', language)}

- updateField('direction_bias_ratio', parseInt(e.target.value) / 100)} - disabled={disabled} - min={55} - max={90} - step={5} - className="flex-1 h-2 rounded-lg appearance-none cursor-pointer" - style={{ background: '#2B3139' }} - /> - - X = {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% - -
-
-
- 偏多/Long Bias: - {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 买 + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 卖 -
-
- 偏空/Short Bias: - {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 买 + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 卖 -
+ updateField('direction_bias_ratio', parseInt(e.target.value) / 100)} disabled={disabled} min={55} max={90} step={5} className="flex-1 h-2 rounded-lg appearance-none cursor-pointer" style={{ background: '#2B3139' }} /> + X = {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}%
diff --git a/web/src/components/strategy/GridRiskPanel.tsx b/web/src/components/strategy/GridRiskPanel.tsx index 75c5b73bb4..7c1f83c347 100644 --- a/web/src/components/strategy/GridRiskPanel.tsx +++ b/web/src/components/strategy/GridRiskPanel.tsx @@ -1,11 +1,18 @@ import { useState, useEffect, useCallback } from 'react' import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react' import type { GridRiskInfo } from '../../types' +import { gridRisk } from '../../i18n/strategy-translations' interface GridRiskPanelProps { traderId: string language?: string - refreshInterval?: number // ms, default 5000 + refreshInterval?: number +} + +const t = (key: keyof typeof gridRisk, language: string): string => { + const entry = gridRisk[key] + if (!entry) return key + return entry[language as keyof typeof entry] || entry.en || key } export function GridRiskPanel({ @@ -18,79 +25,13 @@ export function GridRiskPanel({ const [error, setError] = useState(null) const [expanded, setExpanded] = useState(false) - const t = (key: string) => { - const translations: Record> = { - // Section titles - gridRisk: { zh: '网格风控', en: 'Grid Risk' }, - leverageInfo: { zh: '杠杆', en: 'Leverage' }, - positionInfo: { zh: '仓位', en: 'Position' }, - liquidationInfo: { zh: '清算', en: 'Liquidation' }, - marketState: { zh: '市场', en: 'Market' }, - boxState: { zh: '箱体', en: 'Box' }, - - // Leverage - currentLeverage: { zh: '当前', en: 'Current' }, - effectiveLeverage: { zh: '有效', en: 'Effective' }, - recommendedLeverage: { zh: '建议', en: 'Recommend' }, - - // Position - currentPosition: { zh: '当前', en: 'Current' }, - maxPosition: { zh: '最大', en: 'Max' }, - positionPercent: { zh: '占比', en: 'Usage' }, - - // Liquidation - liquidationPrice: { zh: '清算价', en: 'Liq Price' }, - liquidationDistance: { zh: '距离', en: 'Distance' }, - - // Market - regimeLevel: { zh: '波动', en: 'Regime' }, - currentPrice: { zh: '价格', en: 'Price' }, - breakoutLevel: { zh: '突破', en: 'Breakout' }, - breakoutDirection: { zh: '方向', en: 'Direction' }, - - // Box - shortBox: { zh: '短期', en: 'Short' }, - midBox: { zh: '中期', en: 'Mid' }, - longBox: { zh: '长期', en: 'Long' }, - - // Regime levels - narrow: { zh: '窄幅', en: 'Narrow' }, - standard: { zh: '标准', en: 'Standard' }, - wide: { zh: '宽幅', en: 'Wide' }, - volatile: { zh: '剧烈', en: 'Volatile' }, - trending: { zh: '趋势', en: 'Trending' }, - - // Breakout levels - none: { zh: '无', en: 'None' }, - short: { zh: '短期', en: 'Short' }, - mid: { zh: '中期', en: 'Mid' }, - long: { zh: '长期', en: 'Long' }, - - // Directions - up: { zh: '↑', en: '↑' }, - down: { zh: '↓', en: '↓' }, - - // Status - loading: { zh: '加载中...', en: 'Loading...' }, - error: { zh: '加载失败', en: 'Load Failed' }, - noData: { zh: '暂无数据', en: 'No Data' }, - } - return translations[key]?.[language] || key - } - const fetchRiskInfo = useCallback(async () => { try { const token = localStorage.getItem('auth_token') const response = await fetch(`/api/traders/${traderId}/grid-risk`, { - headers: { - Authorization: `Bearer ${token}`, - }, + headers: { Authorization: `Bearer ${token}` }, }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`) - } - + if (!response.ok) throw new Error(`HTTP ${response.status}`) const data = await response.json() setRiskInfo(data) setError(null) @@ -141,227 +82,147 @@ export function GridRiskPanel({ return price.toFixed(6) } - const formatUSD = (value: number) => { - return `$${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` - } + const formatUSD = (value: number) => `$${value.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` - const cardStyle = { - background: '#0B0E11', - border: '1px solid #2B3139', - } + const cardStyle = { background: '#0B0E11', border: '1px solid #2B3139' } if (loading) { - return ( -
- {t('loading')} -
- ) + return
{t('loading', language)}
} if (error) { - return ( -
- {t('error')}: {error} -
- ) + return
{t('error', language)}: {error}
} if (!riskInfo) { - return ( -
- {t('noData')} -
- ) + return
{t('noData', language)}
} return (
- {/* Collapsible Header */} -
setExpanded(!expanded)} - > +
setExpanded(!expanded)}>
- - {t('gridRisk')} - + {t('gridRisk', language)}
-
- {/* Summary badges when collapsed */} -
- - {t(riskInfo.regime_level || 'standard')} - - - {riskInfo.effective_leverage.toFixed(1)}x - - - {riskInfo.position_percent.toFixed(0)}% - -
- {expanded ? ( - - ) : ( - - )} +
+ + {t(riskInfo.regime_level || 'standard', language)} + + {riskInfo.effective_leverage.toFixed(1)}x + {riskInfo.position_percent.toFixed(0)}% + {expanded ? : }
- {/* Expanded Content */} {expanded && (
- {/* Row 1: Leverage & Position */}
- {/* Leverage */}
- {t('leverageInfo')} + {t('leverageInfo', language)}
-
{t('currentLeverage')}
+
{t('currentLeverage', language)}
{riskInfo.current_leverage}x
-
{t('effectiveLeverage')}
+
{t('effectiveLeverage', language)}
{riskInfo.effective_leverage.toFixed(2)}x
-
{t('recommendedLeverage')}
-
riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }} - > - {riskInfo.recommended_leverage}x -
+
{t('recommendedLeverage', language)}
+
riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}>{riskInfo.recommended_leverage}x
- - {/* Position */}
- {t('positionInfo')} + {t('positionInfo', language)}
-
{t('currentPosition')}
+
{t('currentPosition', language)}
{formatUSD(riskInfo.current_position)}
-
{t('maxPosition')}
+
{t('maxPosition', language)}
{formatUSD(riskInfo.max_position)}
-
{t('positionPercent')}
-
- {riskInfo.position_percent.toFixed(1)}% -
+
{t('positionPercent', language)}
+
{riskInfo.position_percent.toFixed(1)}%
- {/* Mini progress bar */}
-
+
- - {/* Row 2: Market State & Liquidation */}
- {/* Market State */}
- {t('marketState')} + {t('marketState', language)}
-
{t('regimeLevel')}
-
- {t(riskInfo.regime_level || 'standard')} -
+
{t('regimeLevel', language)}
+
{t(riskInfo.regime_level || 'standard', language)}
-
{t('currentPrice')}
+
{t('currentPrice', language)}
{formatPrice(riskInfo.current_price)}
-
{t('breakoutLevel')}
-
- {t(riskInfo.breakout_level || 'none')} -
+
{t('breakoutLevel', language)}
+
{t(riskInfo.breakout_level || 'none', language)}
-
{t('breakoutDirection')}
-
- {riskInfo.breakout_direction ? t(riskInfo.breakout_direction) : '-'} +
{t('breakoutDirection', language)}
+
+ {riskInfo.breakout_direction ? t(riskInfo.breakout_direction, language) : '-'}
- - {/* Liquidation */}
- {t('liquidationInfo')} + {t('liquidationInfo', language)}
-
{t('liquidationPrice')}
-
- {riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'} -
+
{t('liquidationPrice', language)}
+
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
-
{t('liquidationDistance')}
-
- {riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'} -
+
{t('liquidationDistance', language)}
+
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
- - {/* Row 3: Box State */}
- {t('boxState')} + {t('boxState', language)}
- {t('shortBox')} - - {formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)} - + {t('shortBox', language)} + {formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
- {t('midBox')} - - {formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)} - + {t('midBox', language)} + {formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
- {t('longBox')} - - {formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)} - + {t('longBox', language)} + {formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
diff --git a/web/src/components/strategy/IndicatorEditor.tsx b/web/src/components/strategy/IndicatorEditor.tsx index 4b2b05ca10..4e2f666056 100644 --- a/web/src/components/strategy/IndicatorEditor.tsx +++ b/web/src/components/strategy/IndicatorEditor.tsx @@ -1,642 +1,136 @@ import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react' import type { IndicatorConfig } from '../../types' +import { indicator } from '../../i18n/strategy-translations' -// Default NofxOS API Key const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c' -interface IndicatorEditorProps { - config: IndicatorConfig - onChange: (config: IndicatorConfig) => void - disabled?: boolean - language: string -} - -// 所有可用时间周期 const allTimeframes = [ - { value: '1m', label: '1m', category: 'scalp' }, - { value: '3m', label: '3m', category: 'scalp' }, - { value: '5m', label: '5m', category: 'scalp' }, - { value: '15m', label: '15m', category: 'intraday' }, - { value: '30m', label: '30m', category: 'intraday' }, - { value: '1h', label: '1h', category: 'intraday' }, - { value: '2h', label: '2h', category: 'swing' }, - { value: '4h', label: '4h', category: 'swing' }, - { value: '6h', label: '6h', category: 'swing' }, - { value: '8h', label: '8h', category: 'swing' }, - { value: '12h', label: '12h', category: 'swing' }, - { value: '1d', label: '1D', category: 'position' }, - { value: '3d', label: '3D', category: 'position' }, - { value: '1w', label: '1W', category: 'position' }, + { value: '1m', label: '1m', category: 'scalp' }, { value: '3m', label: '3m', category: 'scalp' }, { value: '5m', label: '5m', category: 'scalp' }, + { value: '15m', label: '15m', category: 'intraday' }, { value: '30m', label: '30m', category: 'intraday' }, { value: '1h', label: '1h', category: 'intraday' }, + { value: '2h', label: '2h', category: 'swing' }, { value: '4h', label: '4h', category: 'swing' }, { value: '6h', label: '6h', category: 'swing' }, + { value: '8h', label: '8h', category: 'swing' }, { value: '12h', label: '12h', category: 'swing' }, { value: '1d', label: '1D', category: 'position' }, + { value: '3d', label: '3D', category: 'position' }, { value: '1w', label: '1W', category: 'position' }, ] -export function IndicatorEditor({ - config, - onChange, - disabled, - language, -}: IndicatorEditorProps) { - const t = (key: string) => { - const translations: Record> = { - // Section titles - marketData: { zh: '市场数据', en: 'Market Data' }, - marketDataDesc: { zh: 'AI 分析所需的核心价格数据', en: 'Core price data for AI analysis' }, - technicalIndicators: { zh: '技术指标', en: 'Technical Indicators' }, - technicalIndicatorsDesc: { zh: '可选的技术分析指标,AI 可自行计算', en: 'Optional indicators, AI can calculate them' }, - marketSentiment: { zh: '市场情绪', en: 'Market Sentiment' }, - marketSentimentDesc: { zh: '持仓量、资金费率等市场情绪数据', en: 'OI, funding rate and market sentiment data' }, - quantData: { zh: '量化数据', en: 'Quant Data' }, - quantDataDesc: { zh: '资金流向、大户动向', en: 'Netflow, whale movements' }, - - // Timeframes - timeframes: { zh: '时间周期', en: 'Timeframes' }, - timeframesDesc: { zh: '选择 K 线分析周期,★ 为主周期(双击设置)', en: 'Select K-line timeframes, ★ = primary (double-click)' }, - klineCount: { zh: 'K 线数量', en: 'K-line Count' }, - scalp: { zh: '超短', en: 'Scalp' }, - intraday: { zh: '日内', en: 'Intraday' }, - swing: { zh: '波段', en: 'Swing' }, - position: { zh: '趋势', en: 'Position' }, - - // Data types - rawKlines: { zh: 'OHLCV 原始 K 线', en: 'Raw OHLCV K-lines' }, - rawKlinesDesc: { zh: '必须 - 开高低收量原始数据,AI 核心分析依据', en: 'Required - Open/High/Low/Close/Volume data for AI' }, - required: { zh: '必须', en: 'Required' }, - - // Indicators - ema: { zh: 'EMA 均线', en: 'EMA' }, - emaDesc: { zh: '指数移动平均线', en: 'Exponential Moving Average' }, - macd: { zh: 'MACD', en: 'MACD' }, - macdDesc: { zh: '异同移动平均线', en: 'Moving Average Convergence Divergence' }, - rsi: { zh: 'RSI', en: 'RSI' }, - rsiDesc: { zh: '相对强弱指标', en: 'Relative Strength Index' }, - atr: { zh: 'ATR', en: 'ATR' }, - atrDesc: { zh: '真实波幅均值', en: 'Average True Range' }, - boll: { zh: 'BOLL 布林带', en: 'Bollinger Bands' }, - bollDesc: { zh: '布林带指标(上中下轨)', en: 'Upper/Middle/Lower Bands' }, - volume: { zh: '成交量', en: 'Volume' }, - volumeDesc: { zh: '交易量分析', en: 'Trading volume analysis' }, - oi: { zh: '持仓量', en: 'Open Interest' }, - oiDesc: { zh: '合约未平仓量', en: 'Futures open interest' }, - fundingRate: { zh: '资金费率', en: 'Funding Rate' }, - fundingRateDesc: { zh: '永续合约资金费率', en: 'Perpetual funding rate' }, - - // OI Ranking - oiRanking: { zh: 'OI 排行', en: 'OI Ranking' }, - oiRankingDesc: { zh: '持仓量增减排行', en: 'OI change ranking' }, - oiRankingNote: { zh: '显示持仓量增加/减少的币种排行,帮助发现资金流向', en: 'Shows coins with OI increase/decrease, helps identify capital flow' }, - - // NetFlow Ranking - netflowRanking: { zh: '资金流向', en: 'NetFlow' }, - netflowRankingDesc: { zh: '机构/散户资金流向', en: 'Institution/retail fund flow' }, - netflowRankingNote: { zh: '显示机构资金流入/流出排行,散户动向对比,发现聪明钱信号', en: 'Shows institution inflow/outflow ranking, retail flow comparison, Smart Money signals' }, - - // Price Ranking - priceRanking: { zh: '涨跌幅排行', en: 'Price Ranking' }, - priceRankingDesc: { zh: '涨跌幅排行榜', en: 'Gainers/losers ranking' }, - priceRankingNote: { zh: '显示涨幅/跌幅排行,结合资金流和持仓变化分析趋势强度', en: 'Shows top gainers/losers, combined with fund flow and OI for trend analysis' }, - priceRankingMulti: { zh: '多周期', en: 'Multi-period' }, - - // Common settings - duration: { zh: '周期', en: 'Duration' }, - limit: { zh: '数量', en: 'Limit' }, - - // Tips - aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' }, - - // NofxOS Data Provider - nofxosTitle: { zh: 'NofxOS 量化数据源', en: 'NofxOS Data Provider' }, - nofxosDesc: { zh: '专业加密货币量化数据服务', en: 'Professional crypto quant data service' }, - nofxosFeatures: { zh: 'AI500 · OI排行 · 资金流向 · 涨跌榜', en: 'AI500 · OI Ranking · Fund Flow · Price Ranking' }, - viewApiDocs: { zh: 'API 文档', en: 'API Docs' }, - apiKey: { zh: 'API Key', en: 'API Key' }, - apiKeyPlaceholder: { zh: '输入 NofxOS API Key', en: 'Enter NofxOS API Key' }, - fillDefault: { zh: '填入默认', en: 'Fill Default' }, - connected: { zh: '已配置', en: 'Configured' }, - notConfigured: { zh: '未配置', en: 'Not Configured' }, - nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources' }, - } - return translations[key]?.[language] || key - } +const t = (key: keyof typeof indicator, language: string): string => { + const entry = indicator[key] + if (!entry) return key + return entry[language as keyof typeof entry] || entry.en || key +} - // 获取当前选中的时间周期 +export function IndicatorEditor({ config, onChange, disabled, language }: { + config: IndicatorConfig + onChange: (config: IndicatorConfig) => void + disabled?: boolean + language: string +}) { const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe] - - // 切换时间周期选择 const toggleTimeframe = (tf: string) => { if (disabled) return const current = [...selectedTimeframes] const index = current.indexOf(tf) - - if (index >= 0) { - if (current.length > 1) { - current.splice(index, 1) - const newPrimary = tf === config.klines.primary_timeframe ? current[0] : config.klines.primary_timeframe - onChange({ - ...config, - klines: { - ...config.klines, - selected_timeframes: current, - primary_timeframe: newPrimary, - enable_multi_timeframe: current.length > 1, - }, - }) - } - } else { - current.push(tf) - onChange({ - ...config, - klines: { - ...config.klines, - selected_timeframes: current, - enable_multi_timeframe: current.length > 1, - }, - }) - } + if (index >= 0) { if (current.length > 1) { current.splice(index, 1); const newPrimary = tf === config.klines.primary_timeframe ? current[0] : config.klines.primary_timeframe; onChange({ ...config, klines: { ...config.klines, selected_timeframes: current, primary_timeframe: newPrimary, enable_multi_timeframe: current.length > 1 } }) } } + else { current.push(tf); onChange({ ...config, klines: { ...config.klines, selected_timeframes: current, enable_multi_timeframe: current.length > 1 } }) } } - - // 设置主时间周期 - const setPrimaryTimeframe = (tf: string) => { - if (disabled) return - onChange({ - ...config, - klines: { - ...config.klines, - primary_timeframe: tf, - }, - }) - } - - const categoryColors: Record = { - scalp: '#F6465D', - intraday: '#F0B90B', - swing: '#0ECB81', - position: '#60a5fa', - } - - // Ensure enable_raw_klines is always true - const ensureRawKlines = () => { - if (!config.enable_raw_klines) { - onChange({ ...config, enable_raw_klines: true }) - } - } - - // Call on mount if needed - if (config.enable_raw_klines === undefined || config.enable_raw_klines === false) { - ensureRawKlines() - } - - // Check if any NofxOS feature is enabled + const setPrimaryTimeframe = (tf: string) => { if (disabled) return; onChange({ ...config, klines: { ...config.klines, primary_timeframe: tf } }) } + const categoryColors: Record = { scalp: '#F6465D', intraday: '#F0B90B', swing: '#0ECB81', position: '#60a5fa' } + if (config.enable_raw_klines === undefined || config.enable_raw_klines === false) { onChange({ ...config, enable_raw_klines: true }) } const hasNofxosEnabled = config.enable_quant_data || config.enable_oi_ranking || config.enable_netflow_ranking || config.enable_price_ranking const hasApiKey = !!config.nofxos_api_key return (
- {/* ============================================ */} - {/* NofxOS Data Provider - Top Configuration */} - {/* ============================================ */} -
- {/* Decorative gradient line at top */} -
- +
+
- {/* Header Row */}
-
- -
-
-

- {t('nofxosTitle')} -

- - {t('nofxosFeatures')} - -
+
+

{t('nofxosTitle', language)}

{t('nofxosFeatures', language)}
- - {/* Status & API Docs */}
- {hasApiKey ? ( - - - {t('connected')} - - ) : ( - - - {t('notConfigured')} - - )} - - - {t('viewApiDocs')} - + {hasApiKey ? {t('connected', language)} : {t('notConfigured', language)}} + {t('viewApiDocs', language)}
- - {/* API Key Input */}
- !disabled && onChange({ ...config, nofxos_api_key: e.target.value })} - disabled={disabled} - placeholder={t('apiKeyPlaceholder')} - className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono" - style={{ - background: 'rgba(30, 35, 41, 0.8)', - border: hasApiKey ? '1px solid rgba(14, 203, 129, 0.3)' : '1px solid rgba(139, 92, 246, 0.3)', - color: '#EAECEF', - }} - /> + !disabled && onChange({ ...config, nofxos_api_key: e.target.value })} disabled={disabled} placeholder={t('apiKeyPlaceholder', language)} className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono" style={{ background: 'rgba(30, 35, 41, 0.8)', border: hasApiKey ? '1px solid rgba(14, 203, 129, 0.3)' : '1px solid rgba(139, 92, 246, 0.3)', color: '#EAECEF' }} />
- {!disabled && !config.nofxos_api_key && ( - - )} + {!disabled && !config.nofxos_api_key && }
- - {/* NofxOS Data Sources Grid */}
-
- {t('nofxosDataSources')} -
+
{t('nofxosDataSources', language)}
- {/* Quant Data */} -
!disabled && onChange({ ...config, enable_quant_data: !config.enable_quant_data })} - > +
!disabled && onChange({ ...config, enable_quant_data: !config.enable_quant_data })}>
-
-
- {t('quantData')} -
- { e.stopPropagation(); !disabled && onChange({ ...config, enable_quant_data: e.target.checked }) }} - disabled={disabled} - className="w-3.5 h-3.5 rounded accent-blue-500" - /> +
{t('quantData', language)}
+ { e.stopPropagation(); !disabled && onChange({ ...config, enable_quant_data: e.target.checked }) }} disabled={disabled} className="w-3.5 h-3.5 rounded accent-blue-500" />
-

{t('quantDataDesc')}

- {config.enable_quant_data && ( -
- - -
- )} +

{t('quantDataDesc', language)}

- - {/* OI Ranking */} -
!disabled && onChange({ - ...config, - enable_oi_ranking: !config.enable_oi_ranking, - ...(!config.enable_oi_ranking && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}), - ...(!config.enable_oi_ranking && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}), - })} - > +
!disabled && onChange({ ...config, enable_oi_ranking: !config.enable_oi_ranking, ...(!config.enable_oi_ranking && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}), ...(!config.enable_oi_ranking && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}) })}>
-
-
- {t('oiRanking')} -
- { e.stopPropagation(); !disabled && onChange({ - ...config, - enable_oi_ranking: e.target.checked, - ...(e.target.checked && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}), - ...(e.target.checked && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}), - }) }} - disabled={disabled} - className="w-3.5 h-3.5 rounded accent-green-500" - /> +
{t('oiRanking', language)}
+ { e.stopPropagation(); !disabled && onChange({ ...config, enable_oi_ranking: e.target.checked }) }} disabled={disabled} className="w-3.5 h-3.5 rounded accent-green-500" />
-

{t('oiRankingDesc')}

- {config.enable_oi_ranking && ( -
e.stopPropagation()}> - - -
- )} +

{t('oiRankingDesc', language)}

- - {/* NetFlow Ranking */} -
!disabled && onChange({ - ...config, - enable_netflow_ranking: !config.enable_netflow_ranking, - ...(!config.enable_netflow_ranking && !config.netflow_ranking_duration ? { netflow_ranking_duration: '1h' } : {}), - ...(!config.enable_netflow_ranking && !config.netflow_ranking_limit ? { netflow_ranking_limit: 10 } : {}), - })} - > +
!disabled && onChange({ ...config, enable_netflow_ranking: !config.enable_netflow_ranking })}>
-
-
- {t('netflowRanking')} -
- { e.stopPropagation(); !disabled && onChange({ - ...config, - enable_netflow_ranking: e.target.checked, - ...(e.target.checked && !config.netflow_ranking_duration ? { netflow_ranking_duration: '1h' } : {}), - ...(e.target.checked && !config.netflow_ranking_limit ? { netflow_ranking_limit: 10 } : {}), - }) }} - disabled={disabled} - className="w-3.5 h-3.5 rounded accent-amber-500" - /> +
{t('netflowRanking', language)}
+ { e.stopPropagation(); !disabled && onChange({ ...config, enable_netflow_ranking: e.target.checked }) }} disabled={disabled} className="w-3.5 h-3.5 rounded accent-amber-500" />
-

{t('netflowRankingDesc')}

- {config.enable_netflow_ranking && ( -
e.stopPropagation()}> - - -
- )} +

{t('netflowRankingDesc', language)}

- - {/* Price Ranking */} -
!disabled && onChange({ - ...config, - enable_price_ranking: !config.enable_price_ranking, - ...(!config.enable_price_ranking && !config.price_ranking_duration ? { price_ranking_duration: '1h,4h,24h' } : {}), - ...(!config.enable_price_ranking && !config.price_ranking_limit ? { price_ranking_limit: 10 } : {}), - })} - > +
!disabled && onChange({ ...config, enable_price_ranking: !config.enable_price_ranking })}>
-
-
- {t('priceRanking')} -
- { e.stopPropagation(); !disabled && onChange({ - ...config, - enable_price_ranking: e.target.checked, - ...(e.target.checked && !config.price_ranking_duration ? { price_ranking_duration: '1h,4h,24h' } : {}), - ...(e.target.checked && !config.price_ranking_limit ? { price_ranking_limit: 10 } : {}), - }) }} - disabled={disabled} - className="w-3.5 h-3.5 rounded accent-pink-500" - /> +
{t('priceRanking', language)}
+ { e.stopPropagation(); !disabled && onChange({ ...config, enable_price_ranking: e.target.checked }) }} disabled={disabled} className="w-3.5 h-3.5 rounded accent-pink-500" />
-

{t('priceRankingDesc')}

- {config.enable_price_ranking && ( -
e.stopPropagation()}> - - -
- )} +

{t('priceRankingDesc', language)}

- - {/* Warning if features enabled but no API key */} - {hasNofxosEnabled && !hasApiKey && ( -
- - - {language === 'zh' ? '请配置 API Key 以启用 NofxOS 数据源' : 'Please configure API Key to enable NofxOS data sources'} - -
- )} + {hasNofxosEnabled && !hasApiKey &&
{language === 'zh' ? '请配置 API Key 以启用 NofxOS 数据源' : 'Please configure API Key to enable NofxOS data sources'}
}
- - {/* ============================================ */} - {/* Section 1: Market Data (Required) */} - {/* ============================================ */}
- {t('marketData')} - - {t('marketDataDesc')} + {t('marketData', language)} + - {t('marketDataDesc', language)}
-
- {/* Raw Klines - Required, Always On */}
-
- -
+
-
- {t('rawKlines')} - - - {t('required')} - -
-

{t('rawKlinesDesc')}

+
{t('rawKlines', language)}{t('required', language)}
+

{t('rawKlinesDesc', language)}

- +
- - {/* Timeframe Selection */}
-
- - {t('timeframes')} -
-
- {t('klineCount')}: - - !disabled && - onChange({ - ...config, - klines: { ...config.klines, primary_count: parseInt(e.target.value) || 30 }, - }) - } - disabled={disabled} - min={10} - max={200} - className="w-16 px-2 py-1 rounded text-xs text-center" - style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} - /> -
+
{t('timeframes', language)}
+
{t('klineCount', language)}: !disabled && onChange({ ...config, klines: { ...config.klines, primary_count: parseInt(e.target.value) || 30 } })} disabled={disabled} min={10} max={200} className="w-16 px-2 py-1 rounded text-xs text-center" style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} />
-

{t('timeframesDesc')}

- - {/* Timeframe Grid */} +

{t('timeframesDesc', language)}

{(['scalp', 'intraday', 'swing', 'position'] as const).map((category) => { const categoryTfs = allTimeframes.filter((tf) => tf.category === category) return (
- - {t(category)} - + {t(category, language)}
{categoryTfs.map((tf) => { const isSelected = selectedTimeframes.includes(tf.value) const isPrimary = config.klines.primary_timeframe === tf.value return ( - + ) })}
@@ -647,25 +141,14 @@ export function IndicatorEditor({
- - {/* ============================================ */} - {/* Section 2: Technical Indicators (Optional) */} - {/* ============================================ */}
- {t('technicalIndicators')} - - {t('technicalIndicatorsDesc')} + {t('technicalIndicators', language)} + - {t('technicalIndicatorsDesc', language)}
-
- {/* Tip */} -
- -

{t('aiCanCalculate')}

-
- - {/* Indicator Grid */} +

{t('aiCanCalculate', language)}

{[ { key: 'enable_ema', label: 'ema', desc: 'emaDesc', color: '#F0B90B', periodKey: 'ema_periods', defaultPeriods: '20,50' }, @@ -674,62 +157,23 @@ export function IndicatorEditor({ { key: 'enable_atr', label: 'atr', desc: 'atrDesc', color: '#60a5fa', periodKey: 'atr_periods', defaultPeriods: '14' }, { key: 'enable_boll', label: 'boll', desc: 'bollDesc', color: '#ec4899', periodKey: 'boll_periods', defaultPeriods: '20' }, ].map(({ key, label, desc, color, periodKey, defaultPeriods }) => ( -
+
-
-
- {t(label)} -
- !disabled && onChange({ ...config, [key]: e.target.checked })} - disabled={disabled} - className="w-4 h-4 rounded accent-yellow-500" - /> +
{t(label, language)}
+ !disabled && onChange({ ...config, [key]: e.target.checked })} disabled={disabled} className="w-4 h-4 rounded accent-yellow-500" />
-

{t(desc)}

- {periodKey && config[key as keyof IndicatorConfig] && ( - { - if (disabled) return - const periods = e.target.value - .split(',') - .map((s) => parseInt(s.trim())) - .filter((n) => !isNaN(n) && n > 0) - onChange({ ...config, [periodKey]: periods }) - }} - disabled={disabled} - placeholder={defaultPeriods} - className="w-full px-2 py-1 rounded text-[10px] text-center" - style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} - /> - )} +

{t(desc, language)}

))}
- - {/* ============================================ */} - {/* Section 3: Market Sentiment */} - {/* ============================================ */}
- {t('marketSentiment')} - - {t('marketSentimentDesc')} + {t('marketSentiment', language)} + - {t('marketSentimentDesc', language)}
-
{[ @@ -737,28 +181,12 @@ export function IndicatorEditor({ { key: 'enable_oi', label: 'oi', desc: 'oiDesc', color: '#34d399' }, { key: 'enable_funding_rate', label: 'fundingRate', desc: 'fundingRateDesc', color: '#fbbf24' }, ].map(({ key, label, desc, color }) => ( -
+
-
-
- {t(label)} -
- !disabled && onChange({ ...config, [key]: e.target.checked })} - disabled={disabled} - className="w-4 h-4 rounded accent-yellow-500" - /> +
{t(label, language)}
+ !disabled && onChange({ ...config, [key]: e.target.checked })} disabled={disabled} className="w-4 h-4 rounded accent-yellow-500" />
-

{t(desc)}

+

{t(desc, language)}

))}
diff --git a/web/src/components/strategy/PromptSectionsEditor.tsx b/web/src/components/strategy/PromptSectionsEditor.tsx index f6959b41fb..2f4fec92be 100644 --- a/web/src/components/strategy/PromptSectionsEditor.tsx +++ b/web/src/components/strategy/PromptSectionsEditor.tsx @@ -1,187 +1,79 @@ import { useState } from 'react' import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react' import type { PromptSectionsConfig } from '../../types' +import { promptSections } from '../../i18n/strategy-translations' -interface PromptSectionsEditorProps { - config: PromptSectionsConfig | undefined - onChange: (config: PromptSectionsConfig) => void - disabled?: boolean - language: string -} - -// Default prompt sections (same as backend defaults) const defaultSections: PromptSectionsConfig = { - role_definition: `# 你是专业的加密货币交易AI - -你专注于技术分析和风险管理,基于市场数据做出理性的交易决策。 -你的目标是在控制风险的前提下,捕捉高概率的交易机会。`, - - trading_frequency: `# ⏱️ 交易频率认知 - -- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔 -- 每小时>2笔 = 过度交易 -- 单笔持仓时间≥30-60分钟 -如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`, - - entry_standards: `# 🎯 开仓标准(严格) - -只在多重信号共振时开仓: -- 趋势方向明确(EMA排列、价格位置) -- 动量确认(MACD、RSI协同) -- 波动率适中(ATR合理范围) -- 量价配合(成交量支持方向) - -避免:单一指标、信号矛盾、横盘震荡、刚平仓即重启。`, - - decision_process: `# 📋 决策流程 - -1. 检查持仓 → 是否该止盈/止损 -2. 扫描候选币 + 多时间框 → 是否存在强信号 -3. 评估风险回报比 → 是否满足最小要求 -4. 先写思维链,再输出结构化JSON`, + role_definition: `# 你是专业的加密货币交易AI\n\n你专注于技术分析和风险管理,基于市场数据做出理性的交易决策。\n你的目标是在控制风险的前提下,捕捉高概率的交易机会。`, + trading_frequency: `# ⏱️ 交易频率认知\n\n- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔\n- 每小时>2笔 = 过度交易\n- 单笔持仓时间≥30-60分钟\n如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`, + entry_standards: `# 🎯 开仓标准(严格)\n\n只在多重信号共振时开仓:\n- 趋势方向明确(EMA排列、价格位置)\n- 动量确认(MACD、RSI协同)\n- 波动率适中(ATR合理范围)\n- 量价配合(成交量支持方向)\n\n避免:单一指标、信号矛盾、横盘震荡、刚平仓即重启。`, + decision_process: `# 📋 决策流程\n\n1. 检查持仓 → 是否该止盈/止损\n2. 扫描候选币 + 多时间框 → 是否存在强信号\n3. 评估风险回报比 → 是否满足最小要求\n4. 先写思维链,再输出结构化JSON`, } -export function PromptSectionsEditor({ - config, - onChange, - disabled, - language, -}: PromptSectionsEditorProps) { - const [expandedSections, setExpandedSections] = useState>({ - role_definition: false, - trading_frequency: false, - entry_standards: false, - decision_process: false, - }) - - const t = (key: string) => { - const translations: Record> = { - promptSections: { zh: 'System Prompt 自定义', en: 'System Prompt Customization' }, - promptSectionsDesc: { zh: '自定义 AI 行为和决策逻辑(输出格式和风控规则不可修改)', en: 'Customize AI behavior and decision logic (output format and risk rules are fixed)' }, - roleDefinition: { zh: '角色定义', en: 'Role Definition' }, - roleDefinitionDesc: { zh: '定义 AI 的身份和核心目标', en: 'Define AI identity and core objectives' }, - tradingFrequency: { zh: '交易频率', en: 'Trading Frequency' }, - tradingFrequencyDesc: { zh: '设定交易频率预期和过度交易警告', en: 'Set trading frequency expectations and overtrading warnings' }, - entryStandards: { zh: '开仓标准', en: 'Entry Standards' }, - entryStandardsDesc: { zh: '定义开仓信号条件和避免事项', en: 'Define entry signal conditions and avoidances' }, - decisionProcess: { zh: '决策流程', en: 'Decision Process' }, - decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process' }, - resetToDefault: { zh: '重置为默认', en: 'Reset to Default' }, - chars: { zh: '字符', en: 'chars' }, - } - return translations[key]?.[language] || key - } +const t = (key: keyof typeof promptSections, language: string): string => { + const entry = promptSections[key] + if (!entry) return key + return entry[language as keyof typeof entry] || entry.en || key +} +export function PromptSectionsEditor({ config, onChange, disabled, language }: { + config: PromptSectionsConfig | undefined + onChange: (config: PromptSectionsConfig) => void + disabled?: boolean + language: string +}) { + const [expandedSections, setExpandedSections] = useState>({ role_definition: false, trading_frequency: false, entry_standards: false, decision_process: false }) const sections = [ - { key: 'role_definition', label: t('roleDefinition'), desc: t('roleDefinitionDesc') }, - { key: 'trading_frequency', label: t('tradingFrequency'), desc: t('tradingFrequencyDesc') }, - { key: 'entry_standards', label: t('entryStandards'), desc: t('entryStandardsDesc') }, - { key: 'decision_process', label: t('decisionProcess'), desc: t('decisionProcessDesc') }, + { key: 'role_definition', label: t('roleDefinition', language), desc: t('roleDefinitionDesc', language) }, + { key: 'trading_frequency', label: t('tradingFrequency', language), desc: t('tradingFrequencyDesc', language) }, + { key: 'entry_standards', label: t('entryStandards', language), desc: t('entryStandardsDesc', language) }, + { key: 'decision_process', label: t('decisionProcess', language), desc: t('decisionProcessDesc', language) }, ] - const currentConfig = config || {} - - const updateSection = (key: keyof PromptSectionsConfig, value: string) => { - if (!disabled) { - onChange({ ...currentConfig, [key]: value }) - } - } - - const resetSection = (key: keyof PromptSectionsConfig) => { - if (!disabled) { - onChange({ ...currentConfig, [key]: defaultSections[key] }) - } - } - - const toggleSection = (key: string) => { - setExpandedSections((prev) => ({ ...prev, [key]: !prev[key] })) - } - - const getValue = (key: keyof PromptSectionsConfig): string => { - return currentConfig[key] || defaultSections[key] || '' - } + const updateSection = (key: keyof PromptSectionsConfig, value: string) => { if (!disabled) onChange({ ...currentConfig, [key]: value }) } + const resetSection = (key: keyof PromptSectionsConfig) => { if (!disabled) onChange({ ...currentConfig, [key]: defaultSections[key] }) } + const toggleSection = (key: string) => { setExpandedSections((prev) => ({ ...prev, [key]: !prev[key] })) } + const getValue = (key: keyof PromptSectionsConfig): string => currentConfig[key] || defaultSections[key] || '' return (
-

- {t('promptSections')} -

-

- {t('promptSectionsDesc')} -

+

{t('promptSections', language)}

+

{t('promptSectionsDesc', language)}

-
{sections.map(({ key, label, desc }) => { const sectionKey = key as keyof PromptSectionsConfig const isExpanded = expandedSections[key] const value = getValue(sectionKey) const isModified = currentConfig[sectionKey] !== undefined && currentConfig[sectionKey] !== defaultSections[sectionKey] - return ( -
- - {isExpanded && (
-

- {desc} -

+

{desc}