mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
refactor: optimize codebase encoding
This commit is contained in:
@@ -2,6 +2,7 @@ import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
interface LoginRequiredOverlayProps {
|
||||
isOpen: boolean
|
||||
@@ -12,52 +13,19 @@ interface LoginRequiredOverlayProps {
|
||||
export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) {
|
||||
const { language } = useLanguage()
|
||||
|
||||
const texts = {
|
||||
zh: {
|
||||
title: '系统访问受限',
|
||||
subtitle: featureName ? `访问「${featureName}」需要更高权限` : '此模块需要授权访问',
|
||||
description: '初始化身份验证协议以解锁完整系统功能:AI 交易员配置、策略市场数据流、回测模拟核心。',
|
||||
benefits: [
|
||||
'AI 交易员控制权',
|
||||
'高频策略核心市场',
|
||||
'历史数据回测引擎',
|
||||
'全系统数据可视化'
|
||||
],
|
||||
login: '执行登录指令',
|
||||
register: '注册新用户 ID',
|
||||
later: '中止操作'
|
||||
},
|
||||
en: {
|
||||
title: 'SYSTEM ACCESS DENIED',
|
||||
subtitle: featureName ? `Module "${featureName}" requires elevated privileges` : 'Authorization required for this module',
|
||||
description: 'Initialize authentication protocol to unlock full system capabilities: AI Trader configuration, Strategy Market data streams, and Backtest Simulation core.',
|
||||
benefits: [
|
||||
'AI Trader Control',
|
||||
'HFT Strategy Market',
|
||||
'Historical Backtest Engine',
|
||||
'Full System Visualization'
|
||||
],
|
||||
login: 'EXECUTE LOGIN',
|
||||
register: 'REGISTER NEW ID',
|
||||
later: 'ABORT'
|
||||
},
|
||||
id: {
|
||||
title: 'AKSES SISTEM DITOLAK',
|
||||
subtitle: featureName ? `Modul "${featureName}" memerlukan hak akses lebih tinggi` : 'Otorisasi diperlukan untuk modul ini',
|
||||
description: 'Inisialisasi protokol autentikasi untuk membuka kemampuan sistem penuh: konfigurasi Trader AI, aliran data Pasar Strategi, dan inti Simulasi Backtest.',
|
||||
benefits: [
|
||||
'Kontrol Trader AI',
|
||||
'Pasar Strategi HFT',
|
||||
'Mesin Backtest Historis',
|
||||
'Visualisasi Sistem Penuh'
|
||||
],
|
||||
login: 'JALANKAN LOGIN',
|
||||
register: 'DAFTAR ID BARU',
|
||||
later: 'BATALKAN'
|
||||
}
|
||||
}
|
||||
const tr = (key: string, params?: Record<string, string | number>) =>
|
||||
t(`loginRequired.${key}`, language, params)
|
||||
|
||||
const t = texts[language]
|
||||
const subtitle = featureName
|
||||
? tr('subtitleWithFeature', { featureName })
|
||||
: tr('subtitleDefault')
|
||||
|
||||
const benefits = [
|
||||
tr('benefit1'),
|
||||
tr('benefit2'),
|
||||
tr('benefit3'),
|
||||
tr('benefit4'),
|
||||
]
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -108,7 +76,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
||||
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
||||
<AlertTriangle size={18} className="animate-pulse" />
|
||||
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
||||
<span className="font-bold tracking-widest text-sm uppercase">{tr('accessDenied')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,19 +84,19 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
{/* Terminal Text */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
|
||||
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p>
|
||||
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{tr('title')}</h2>
|
||||
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
|
||||
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
|
||||
<span className="text-green-500 mr-2">$</span>
|
||||
{t.description}
|
||||
{tr('description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{t.benefits.map((benefit, i) => (
|
||||
{benefits.map((benefit, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
|
||||
<span className="text-nofx-gold">✓</span> {benefit}
|
||||
</div>
|
||||
@@ -143,7 +111,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
||||
>
|
||||
<LogIn size={14} />
|
||||
<span>{t.login}</span>
|
||||
<span>{tr('loginButton')}</span>
|
||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-></span>
|
||||
</a>
|
||||
|
||||
@@ -152,7 +120,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
|
||||
>
|
||||
<UserPlus size={14} />
|
||||
<span>{t.register}</span>
|
||||
<span>{tr('registerButton')}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -161,7 +129,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
onClick={onClose}
|
||||
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||
>
|
||||
[ {t.later} ]
|
||||
[ {tr('abort')} ]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
CandlestickChart as CandlestickIcon,
|
||||
} from 'lucide-react'
|
||||
import { api } from '../../lib/api'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import type {
|
||||
BacktestEquityPoint,
|
||||
BacktestTradeEvent,
|
||||
@@ -136,7 +137,7 @@ export function EquityChart({ equity, trades }: EquityChartProps) {
|
||||
interface CandlestickChartProps {
|
||||
runId: string
|
||||
trades: BacktestTradeEvent[]
|
||||
language: string
|
||||
language: Language
|
||||
}
|
||||
|
||||
export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) {
|
||||
@@ -289,7 +290,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
||||
if (symbols.length === 0) {
|
||||
return (
|
||||
<div className="py-12 text-center" style={{ color: '#5E6673' }}>
|
||||
{language === 'zh' ? '没有交易记录' : 'No trades to display'}
|
||||
{t('backtestChart.noTrades', language)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -300,7 +301,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
||||
<div className="flex items-center gap-2">
|
||||
<CandlestickIcon size={16} style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '币种' : 'Symbol'}
|
||||
{t('backtestChart.symbol', language)}
|
||||
</span>
|
||||
<select
|
||||
value={selectedSymbol}
|
||||
@@ -319,7 +320,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} style={{ color: '#848E9C' }} />
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '周期' : 'Interval'}
|
||||
{t('backtestChart.interval', language)}
|
||||
</span>
|
||||
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
|
||||
{CHART_TIMEFRAMES.map((tf) => (
|
||||
@@ -339,7 +340,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
||||
</div>
|
||||
|
||||
<span className="text-xs" style={{ color: '#5E6673' }}>
|
||||
({symbolTrades.length} {language === 'zh' ? '笔交易' : 'trades'})
|
||||
({symbolTrades.length} {t('backtestChart.trades', language)})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -351,7 +352,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center h-[400px]" style={{ color: '#848E9C' }}>
|
||||
<RefreshCw className="animate-spin mr-2" size={16} />
|
||||
{language === 'zh' ? '加载K线数据...' : 'Loading kline data...'}
|
||||
{t('backtestChart.loadingKline', language)}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
@@ -365,14 +366,14 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
||||
<div className="flex items-center gap-4 text-xs" style={{ color: '#848E9C' }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#0ECB81' }} />
|
||||
<span>{language === 'zh' ? '开仓/盈利' : 'Open/Profit'}</span>
|
||||
<span>{t('backtestChart.openProfit', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#F6465D' }} />
|
||||
<span>{language === 'zh' ? '亏损平仓' : 'Loss Close'}</span>
|
||||
<span>{t('backtestChart.lossClose', language)}</span>
|
||||
</div>
|
||||
<span style={{ color: '#5E6673' }}>|</span>
|
||||
<span>▲ Long · ▼ Short · ✕ {language === 'zh' ? '平仓' : 'Close'}</span>
|
||||
<span>▲ Long · ▼ Short · ✕ {t('backtestChart.close', language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -384,7 +385,7 @@ interface BacktestChartTabProps {
|
||||
equity: BacktestEquityPoint[] | undefined
|
||||
trades: BacktestTradeEvent[] | undefined
|
||||
selectedRunId: string
|
||||
language: string
|
||||
language: Language
|
||||
tr: (key: string) => string
|
||||
}
|
||||
|
||||
@@ -405,7 +406,7 @@ export function BacktestChartTab({
|
||||
>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '资金曲线' : 'Equity Curve'}
|
||||
{t('backtestChart.equityCurve', language)}
|
||||
</h4>
|
||||
{equity && equity.length > 0 ? (
|
||||
<EquityChart equity={equity} trades={trades ?? []} />
|
||||
@@ -419,7 +420,7 @@ export function BacktestChartTab({
|
||||
{selectedRunId && trades && trades.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? 'K线图 & 交易标记' : 'Candlestick & Trade Markers'}
|
||||
{t('backtestChart.candlestickTradeMarkers', language)}
|
||||
</h4>
|
||||
<CandlestickChartComponent
|
||||
runId={selectedRunId}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import type { AIModel, Strategy } from '../../types'
|
||||
import { t as globalT } from '../../i18n/translations'
|
||||
import type { Language } from '../../i18n/translations'
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
@@ -104,12 +106,12 @@ export function BacktestConfigForm({
|
||||
}
|
||||
}, [selectedStrategy])
|
||||
|
||||
const zh = language === 'zh'
|
||||
const lang = language as Language
|
||||
const quickRanges = [
|
||||
{ label: zh ? '24小时' : '24h', hours: 24 },
|
||||
{ label: zh ? '3天' : '3d', hours: 72 },
|
||||
{ label: zh ? '7天' : '7d', hours: 168 },
|
||||
{ label: zh ? '30天' : '30d', hours: 720 },
|
||||
{ label: globalT('backtestConfigForm.quickRange24h', lang), hours: 24 },
|
||||
{ label: globalT('backtestConfigForm.quickRange3d', lang), hours: 72 },
|
||||
{ label: globalT('backtestConfigForm.quickRange7d', lang), hours: 168 },
|
||||
{ label: globalT('backtestConfigForm.quickRange30d', lang), hours: 720 },
|
||||
]
|
||||
|
||||
const applyQuickRange = (hours: number) => {
|
||||
@@ -144,9 +146,9 @@ export function BacktestConfigForm({
|
||||
</div>
|
||||
))}
|
||||
<span className="ml-2 text-xs" style={{ color: '#848E9C' }}>
|
||||
{wizardStep === 1 ? (zh ? '选择模型' : 'Select Model')
|
||||
: wizardStep === 2 ? (zh ? '配置参数' : 'Configure')
|
||||
: (zh ? '确认启动' : 'Confirm')}
|
||||
{wizardStep === 1 ? globalT('backtestConfigForm.selectModel', lang)
|
||||
: wizardStep === 2 ? globalT('backtestConfigForm.configure', lang)
|
||||
: globalT('backtestConfigForm.confirmStart', lang)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -196,7 +198,7 @@ export function BacktestConfigForm({
|
||||
{/* Strategy Selection (Optional) */}
|
||||
<div>
|
||||
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{zh ? '策略配置(可选)' : 'Strategy (Optional)'}
|
||||
{globalT('backtestConfigForm.strategyOptional', lang)}
|
||||
</label>
|
||||
<select
|
||||
className="w-full p-3 rounded-lg text-sm"
|
||||
@@ -204,7 +206,7 @@ export function BacktestConfigForm({
|
||||
value={formState.strategyId}
|
||||
onChange={(e) => onFormChange('strategyId', e.target.value)}
|
||||
>
|
||||
<option value="">{zh ? '不使用保存的策略' : 'No saved strategy'}</option>
|
||||
<option value="">{globalT('backtestConfigForm.noSavedStrategy', lang)}</option>
|
||||
{strategies?.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name} {s.is_active && '✓'} {s.is_default && '⭐'}
|
||||
@@ -215,7 +217,7 @@ export function BacktestConfigForm({
|
||||
<div className="mt-2 p-2 rounded" style={{ background: 'rgba(240,185,11,0.1)', border: '1px solid rgba(240,185,11,0.2)' }}>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span style={{ color: '#F0B90B' }}>
|
||||
{zh ? '币种来源:' : 'Coin Source:'}
|
||||
{globalT('backtestConfigForm.coinSource', lang)}
|
||||
</span>
|
||||
<span className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{coinSourceDescription.type}
|
||||
@@ -225,9 +227,7 @@ export function BacktestConfigForm({
|
||||
</div>
|
||||
{strategyHasDynamicCoins && (
|
||||
<div className="text-xs mt-1" style={{ color: '#F0B90B' }}>
|
||||
{zh
|
||||
? '⚡ 清空下方币种输入框即可使用策略的动态币种'
|
||||
: '⚡ Clear the symbols field below to use strategy\'s dynamic coins'}
|
||||
{globalT('backtestConfigForm.clearDynamicCoins', lang)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,7 +239,7 @@ export function BacktestConfigForm({
|
||||
{tr('form.symbolsLabel')}
|
||||
{strategyHasDynamicCoins && (
|
||||
<span className="ml-2" style={{ color: '#5E6673' }}>
|
||||
({zh ? '可选 - 策略已配置币种来源' : 'Optional - strategy has coin source'})
|
||||
({globalT('backtestConfigForm.optionalCoinSource', lang)})
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
@@ -283,7 +283,7 @@ export function BacktestConfigForm({
|
||||
onChange={(e) => onFormChange('symbols', e.target.value)}
|
||||
rows={2}
|
||||
placeholder={strategyHasDynamicCoins
|
||||
? (zh ? '留空将使用策略配置的币种来源' : 'Leave empty to use strategy coin source')
|
||||
? globalT('backtestConfigForm.leavEmptyForStrategy', lang)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
@@ -294,7 +294,7 @@ export function BacktestConfigForm({
|
||||
className="absolute top-2 right-2 px-2 py-1 rounded text-xs"
|
||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||
>
|
||||
{zh ? '清空使用策略币种' : 'Clear to use strategy'}
|
||||
{globalT('backtestConfigForm.clearToUseStrategy', lang)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -307,7 +307,7 @@ export function BacktestConfigForm({
|
||||
className="w-full py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 transition-all disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||
>
|
||||
{zh ? '下一步' : 'Next'}
|
||||
{globalT('backtestConfigForm.next', lang)}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</motion.div>
|
||||
@@ -359,7 +359,7 @@ export function BacktestConfigForm({
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{zh ? '时间周期' : 'Timeframes'}
|
||||
{globalT('backtestConfigForm.timeframes', lang)}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{TIMEFRAME_OPTIONS.map((tf) => {
|
||||
@@ -428,7 +428,7 @@ export function BacktestConfigForm({
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
{zh ? '上一步' : 'Back'}
|
||||
{globalT('backtestConfigForm.back', lang)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -436,7 +436,7 @@ export function BacktestConfigForm({
|
||||
className="flex-1 py-2 rounded-lg font-medium flex items-center justify-center gap-2"
|
||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||
>
|
||||
{zh ? '下一步' : 'Next'}
|
||||
{globalT('backtestConfigForm.next', lang)}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -520,7 +520,7 @@ export function BacktestConfigForm({
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
{zh ? '策略风格' : 'Strategy Style'}
|
||||
{globalT('backtestConfigForm.strategyStyle', lang)}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => (
|
||||
@@ -570,7 +570,7 @@ export function BacktestConfigForm({
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
{zh ? '上一步' : 'Back'}
|
||||
{globalT('backtestConfigForm.back', lang)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ArrowDownRight,
|
||||
} from 'lucide-react'
|
||||
import { MetricTooltip } from '../common/MetricTooltip'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import { EquityChart } from './BacktestChartTab'
|
||||
import type {
|
||||
BacktestEquityPoint,
|
||||
@@ -131,7 +132,7 @@ export function ProgressRing({ progress, size = 120 }: ProgressRingProps) {
|
||||
|
||||
interface PositionsDisplayProps {
|
||||
positions: BacktestPositionStatus[]
|
||||
language: string
|
||||
language: Language
|
||||
}
|
||||
|
||||
export function PositionsDisplay({ positions, language }: PositionsDisplayProps) {
|
||||
@@ -151,7 +152,7 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '当前持仓' : 'Active Positions'}
|
||||
{t('backtestOverview.activePositions', language)}
|
||||
</span>
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-xs"
|
||||
@@ -162,13 +163,13 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<span style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '保证金' : 'Margin'}: ${totalMargin.toFixed(2)}
|
||||
{t('backtestOverview.margin', language)}: ${totalMargin.toFixed(2)}
|
||||
</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: totalUnrealizedPnL >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||
>
|
||||
{language === 'zh' ? '浮盈' : 'Unrealized'}: {totalUnrealizedPnL >= 0 ? '+' : ''}
|
||||
{t('backtestOverview.unrealized', language)}: {totalUnrealizedPnL >= 0 ? '+' : ''}
|
||||
${totalUnrealizedPnL.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -214,8 +215,8 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px]" style={{ color: '#5E6673' }}>
|
||||
{language === 'zh' ? '数量' : 'Qty'}: {pos.quantity.toFixed(4)} ·{' '}
|
||||
{language === 'zh' ? '保证金' : 'Margin'}: ${pos.margin_used.toFixed(2)}
|
||||
{t('backtestOverview.qty', language)}: {pos.quantity.toFixed(4)} ·{' '}
|
||||
{t('backtestOverview.margin', language)}: ${pos.margin_used.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,10 +224,10 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
||||
<div className="text-right">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '开仓' : 'Entry'}: ${pos.entry_price.toFixed(2)}
|
||||
{t('backtestOverview.entry', language)}: ${pos.entry_price.toFixed(2)}
|
||||
</span>
|
||||
<span style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '现价' : 'Mark'}: ${pos.mark_price.toFixed(2)}
|
||||
{t('backtestOverview.mark', language)}: ${pos.mark_price.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1.5 mt-0.5">
|
||||
@@ -255,7 +256,7 @@ interface BacktestOverviewTabProps {
|
||||
equity: BacktestEquityPoint[] | undefined
|
||||
trades: BacktestTradeEvent[] | undefined
|
||||
metrics: BacktestMetrics | undefined
|
||||
language: string
|
||||
language: Language
|
||||
tr: (key: string) => string
|
||||
}
|
||||
|
||||
@@ -285,7 +286,7 @@ export function BacktestOverviewTab({
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '胜率' : 'Win Rate'}
|
||||
{t('backtestOverview.winRate', language)}
|
||||
<MetricTooltip metricKey="win_rate" language={language} size={11} />
|
||||
</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||
@@ -294,7 +295,7 @@ export function BacktestOverviewTab({
|
||||
</div>
|
||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '盈亏因子' : 'Profit Factor'}
|
||||
{t('backtestOverview.profitFactor', language)}
|
||||
<MetricTooltip metricKey="profit_factor" language={language} size={11} />
|
||||
</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||
@@ -303,7 +304,7 @@ export function BacktestOverviewTab({
|
||||
</div>
|
||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '总交易数' : 'Total Trades'}
|
||||
{t('backtestOverview.totalTrades', language)}
|
||||
</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||
{metrics.trades ?? 0}
|
||||
@@ -311,7 +312,7 @@ export function BacktestOverviewTab({
|
||||
</div>
|
||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '最佳币种' : 'Best Symbol'}
|
||||
{t('backtestOverview.bestSymbol', language)}
|
||||
</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#0ECB81' }}>
|
||||
{metrics.best_symbol?.replace('USDT', '') || '-'}
|
||||
|
||||
@@ -244,9 +244,9 @@ export function BacktestPage() {
|
||||
const handleDelete = async () => {
|
||||
if (!selectedRunId) return
|
||||
const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), {
|
||||
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
|
||||
okText: language === 'zh' ? '删除' : 'Delete',
|
||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
||||
title: t('backtestPageExtra.confirmDelete', language),
|
||||
okText: t('backtestPageExtra.delete', language),
|
||||
cancelText: t('backtestPageExtra.cancel', language),
|
||||
})
|
||||
if (!confirmed) return
|
||||
try {
|
||||
@@ -328,7 +328,7 @@ export function BacktestPage() {
|
||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
{language === 'zh' ? '新建回测' : 'New Backtest'}
|
||||
{t('backtestPageExtra.newBacktest', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -474,14 +474,14 @@ export function BacktestPage() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<StatCard
|
||||
icon={Target}
|
||||
label={language === 'zh' ? '当前净值' : 'Equity'}
|
||||
label={t('backtestPageExtra.equity', language)}
|
||||
value={(status?.equity ?? 0).toFixed(2)}
|
||||
suffix="USDT"
|
||||
language={language}
|
||||
/>
|
||||
<StatCard
|
||||
icon={TrendingUp}
|
||||
label={language === 'zh' ? '总收益率' : 'Return'}
|
||||
label={t('backtestPageExtra.totalReturn', language)}
|
||||
value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`}
|
||||
trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'}
|
||||
color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'}
|
||||
@@ -490,7 +490,7 @@ export function BacktestPage() {
|
||||
/>
|
||||
<StatCard
|
||||
icon={AlertTriangle}
|
||||
label={language === 'zh' ? '最大回撤' : 'Max DD'}
|
||||
label={t('backtestPageExtra.maxDD', language)}
|
||||
value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`}
|
||||
color="#F6465D"
|
||||
metricKey="max_drawdown"
|
||||
@@ -498,7 +498,7 @@ export function BacktestPage() {
|
||||
/>
|
||||
<StatCard
|
||||
icon={BarChart3}
|
||||
label={language === 'zh' ? '夏普比率' : 'Sharpe'}
|
||||
label={t('backtestPageExtra.sharpe', language)}
|
||||
value={(metrics?.sharpe_ratio ?? 0).toFixed(2)}
|
||||
metricKey="sharpe_ratio"
|
||||
language={language}
|
||||
@@ -516,20 +516,12 @@ export function BacktestPage() {
|
||||
style={{ color: viewTab === tab ? '#F0B90B' : '#848E9C' }}
|
||||
>
|
||||
{tab === 'overview'
|
||||
? language === 'zh'
|
||||
? '概览'
|
||||
: 'Overview'
|
||||
? t('backtestPageExtra.tabOverview', language)
|
||||
: tab === 'chart'
|
||||
? language === 'zh'
|
||||
? '图表'
|
||||
: 'Chart'
|
||||
? t('backtestPageExtra.tabChart', language)
|
||||
: tab === 'trades'
|
||||
? language === 'zh'
|
||||
? '交易'
|
||||
: 'Trades'
|
||||
: language === 'zh'
|
||||
? 'AI决策'
|
||||
: 'Decisions'}
|
||||
? t('backtestPageExtra.tabTrades', language)
|
||||
: t('backtestPageExtra.tabDecisions', language)}
|
||||
{viewTab === tab && (
|
||||
<motion.div
|
||||
layoutId="tab-indicator"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Layers,
|
||||
Eye,
|
||||
} from 'lucide-react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
@@ -61,7 +62,7 @@ interface BacktestRunListProps {
|
||||
runs: BacktestRunItem[]
|
||||
selectedRunId: string | undefined
|
||||
compareRunIds: string[]
|
||||
language: string
|
||||
language: Language
|
||||
tr: (key: string, params?: Record<string, string | number>) => string
|
||||
onSelectRun: (runId: string) => void
|
||||
onToggleCompare: (runId: string) => void
|
||||
@@ -84,7 +85,7 @@ export function BacktestRunList({
|
||||
{tr('runList.title')}
|
||||
</h3>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{runs.length} {language === 'zh' ? '条' : 'runs'}
|
||||
{runs.length} {t('backtestPageExtra.runs', language)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +132,7 @@ export function BacktestRunList({
|
||||
? 'rgba(240,185,11,0.2)'
|
||||
: 'transparent',
|
||||
}}
|
||||
title={language === 'zh' ? '添加到对比' : 'Add to compare'}
|
||||
title={t('backtestPageExtra.addToCompare', language)}
|
||||
>
|
||||
<Eye
|
||||
className="w-3 h-3"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'lightweight-charts'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { httpClient } from '../../lib/httpClient'
|
||||
import { t } from '../../i18n/translations'
|
||||
import {
|
||||
calculateSMA,
|
||||
calculateEMA,
|
||||
@@ -20,26 +21,26 @@ import {
|
||||
} from '../../utils/indicators'
|
||||
import { Settings, BarChart2 } from 'lucide-react'
|
||||
|
||||
// 订单接口定义
|
||||
// Order marker interface
|
||||
interface OrderMarker {
|
||||
time: number
|
||||
price: number
|
||||
side: 'long' | 'short'
|
||||
rawSide: string // 原始 side 字段 (buy/sell from database)
|
||||
rawSide: string // Original side field (buy/sell from database)
|
||||
action: 'open' | 'close'
|
||||
pnl?: number
|
||||
symbol: string
|
||||
}
|
||||
|
||||
// 挂单接口定义 (交易所的止盈止损订单)
|
||||
// Open orders interface (exchange TP/SL orders)
|
||||
interface OpenOrder {
|
||||
order_id: string
|
||||
symbol: string
|
||||
side: string // BUY/SELL
|
||||
position_side: string // LONG/SHORT
|
||||
type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET
|
||||
price: number // 限价单价格
|
||||
stop_price: number // 触发价格 (止损/止盈)
|
||||
price: number // Limit order price
|
||||
stop_price: number // Trigger price (SL/TP)
|
||||
quantity: number
|
||||
status: string
|
||||
}
|
||||
@@ -49,11 +50,11 @@ interface AdvancedChartProps {
|
||||
interval?: string
|
||||
traderID?: string
|
||||
height?: number
|
||||
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
||||
onSymbolChange?: (symbol: string) => void // 币种切换回调
|
||||
exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
||||
onSymbolChange?: (symbol: string) => void // Symbol change callback
|
||||
}
|
||||
|
||||
// 指标配置
|
||||
// Indicator configuration
|
||||
interface IndicatorConfig {
|
||||
id: string
|
||||
name: string
|
||||
@@ -62,31 +63,31 @@ interface IndicatorConfig {
|
||||
params?: any
|
||||
}
|
||||
|
||||
// 获取成交额货币单位
|
||||
// Get quote currency unit
|
||||
const getQuoteUnit = (exchange: string): string => {
|
||||
if (['alpaca'].includes(exchange)) {
|
||||
return 'USD'
|
||||
}
|
||||
if (['forex', 'metals'].includes(exchange)) {
|
||||
return '' // 外汇/贵金属没有真实成交量
|
||||
return '' // Forex/metals have no real volume
|
||||
}
|
||||
return 'USDT' // 加密货币默认 USDT
|
||||
return 'USDT' // Crypto defaults to USDT
|
||||
}
|
||||
|
||||
// 获取成交量数量单位
|
||||
const getBaseUnit = (exchange: string, symbol: string): string => {
|
||||
// Get base volume unit
|
||||
const getBaseUnit = (exchange: string, symbol: string, language: string): string => {
|
||||
if (['alpaca'].includes(exchange)) {
|
||||
return '股'
|
||||
return t('advancedChart.shares', language as 'en' | 'zh' | 'id')
|
||||
}
|
||||
if (['forex', 'metals'].includes(exchange)) {
|
||||
return ''
|
||||
}
|
||||
// 加密货币:从 symbol 提取基础资产
|
||||
// Crypto: extract base asset from symbol
|
||||
const base = symbol.replace(/USDT$|USD$|BUSD$/, '')
|
||||
return base || '个'
|
||||
return base || t('advancedChart.units', language as 'en' | 'zh' | 'id')
|
||||
}
|
||||
|
||||
// 格式化大数字
|
||||
// Format large numbers
|
||||
const formatVolume = (value: number): string => {
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'
|
||||
@@ -99,43 +100,43 @@ export function AdvancedChart({
|
||||
interval = '5m',
|
||||
traderID,
|
||||
height = 550,
|
||||
exchange = 'binance', // 默认使用 binance
|
||||
exchange = 'binance', // Default to binance
|
||||
onSymbolChange: _onSymbolChange, // Available for future use
|
||||
}: AdvancedChartProps) {
|
||||
void _onSymbolChange // Prevent unused warning
|
||||
const { language } = useLanguage()
|
||||
const quoteUnit = getQuoteUnit(exchange)
|
||||
const baseUnit = getBaseUnit(exchange, symbol)
|
||||
const baseUnit = getBaseUnit(exchange, symbol, language)
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chartRef = useRef<IChartApi | null>(null)
|
||||
const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
|
||||
const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null)
|
||||
const indicatorSeriesRef = useRef<Map<string, ISeriesApi<any>>>(new Map())
|
||||
const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5
|
||||
const currentMarkersDataRef = useRef<any[]>([]) // 存储当前的标记数据
|
||||
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // 存储 kline 额外数据
|
||||
const priceLinesRef = useRef<any[]>([]) // 存储挂单价格线
|
||||
const currentMarkersDataRef = useRef<any[]>([]) // Store current marker data
|
||||
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // Store kline extra data
|
||||
const priceLinesRef = useRef<any[]>([]) // Store open order price lines
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showIndicatorPanel, setShowIndicatorPanel] = useState(false)
|
||||
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // 订单标记显示开关,默认显示
|
||||
const isInitialLoadRef = useRef(true) // 跟踪是否为初始加载
|
||||
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // Order marker toggle, default on
|
||||
const isInitialLoadRef = useRef(true) // Track if this is initial load
|
||||
const [tooltipData, setTooltipData] = useState<any>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 行情统计数据(当前K线)
|
||||
// Market stats (current candle)
|
||||
const [marketStats, setMarketStats] = useState<{
|
||||
price: number
|
||||
priceChange: number
|
||||
priceChangePercent: number
|
||||
high: number
|
||||
low: number
|
||||
volume: number // 数量(BTC/股数)
|
||||
quoteVolume: number // 成交额(USDT/USD)
|
||||
volume: number // Quantity (BTC/shares)
|
||||
quoteVolume: number // Turnover (USDT/USD)
|
||||
} | null>(null)
|
||||
|
||||
// 指标配置
|
||||
// Indicator configuration
|
||||
const [indicators, setIndicators] = useState<IndicatorConfig[]>([
|
||||
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
|
||||
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } },
|
||||
@@ -147,7 +148,7 @@ export function AdvancedChart({
|
||||
{ id: 'bb', name: 'Bollinger Bands', enabled: false, color: '#9B59B6' },
|
||||
])
|
||||
|
||||
// 从服务获取K线数据
|
||||
// Fetch kline data from service
|
||||
const fetchKlineData = async (symbol: string, interval: string) => {
|
||||
try {
|
||||
const limit = 1500
|
||||
@@ -158,18 +159,18 @@ export function AdvancedChart({
|
||||
throw new Error('Failed to fetch kline data')
|
||||
}
|
||||
|
||||
// 转换数据格式
|
||||
// Convert data format
|
||||
const rawData = result.data.map((candle: any) => ({
|
||||
time: Math.floor(candle.openTime / 1000) as UTCTimestamp,
|
||||
open: candle.open,
|
||||
high: candle.high,
|
||||
low: candle.low,
|
||||
close: candle.close,
|
||||
volume: candle.volume, // 数量(BTC/股数)
|
||||
quoteVolume: candle.quoteVolume, // 成交额(USDT/USD)
|
||||
volume: candle.volume, // Quantity (BTC/shares)
|
||||
quoteVolume: candle.quoteVolume, // Turnover (USDT/USD)
|
||||
}))
|
||||
|
||||
// 按时间排序并去重(lightweight-charts 要求数据按时间升序且无重复)
|
||||
// Sort by time and deduplicate (lightweight-charts requires ascending, unique times)
|
||||
const sortedData = rawData.sort((a: any, b: any) => a.time - b.time)
|
||||
const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) =>
|
||||
index === 0 || item.time !== arr[index - 1].time
|
||||
@@ -186,16 +187,16 @@ export function AdvancedChart({
|
||||
}
|
||||
}
|
||||
|
||||
// 解析时间:支持 Unix 时间戳(数字)或字符串格式
|
||||
// Parse time: supports Unix timestamp (number) or string format
|
||||
const parseCustomTime = (time: any): number => {
|
||||
if (!time) {
|
||||
console.warn('[AdvancedChart] Empty time value')
|
||||
return 0
|
||||
}
|
||||
|
||||
// 如果已经是数字(Unix 时间戳)
|
||||
// If already a number (Unix timestamp)
|
||||
if (typeof time === 'number') {
|
||||
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳)
|
||||
// Determine ms vs seconds: if > 10^12, treat as milliseconds
|
||||
if (time > 1000000000000) {
|
||||
const seconds = Math.floor(time / 1000)
|
||||
console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
|
||||
@@ -208,7 +209,7 @@ export function AdvancedChart({
|
||||
const timeStr = String(time)
|
||||
console.log('[AdvancedChart] Parsing time string:', timeStr)
|
||||
|
||||
// 尝试标准ISO格式
|
||||
// Try standard ISO format
|
||||
const isoTime = new Date(timeStr).getTime()
|
||||
if (!isNaN(isoTime) && isoTime > 0) {
|
||||
const timestamp = Math.floor(isoTime / 1000)
|
||||
@@ -216,7 +217,7 @@ export function AdvancedChart({
|
||||
return timestamp
|
||||
}
|
||||
|
||||
// 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据)
|
||||
// Parse custom format "MM-DD HH:mm UTC" (for legacy data)
|
||||
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
|
||||
if (match) {
|
||||
const currentYear = new Date().getFullYear()
|
||||
@@ -237,11 +238,11 @@ export function AdvancedChart({
|
||||
return 0
|
||||
}
|
||||
|
||||
// 获取订单数据
|
||||
// Fetch order data
|
||||
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
|
||||
try {
|
||||
console.log('[AdvancedChart] Fetching orders for trader:', traderID, 'symbol:', symbol)
|
||||
// 获取已成交的订单,增加到200条以显示更多历史订单
|
||||
// Fetch filled orders, up to 200 for more history
|
||||
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=200`)
|
||||
|
||||
console.log('[AdvancedChart] Orders API response:', result)
|
||||
@@ -258,14 +259,14 @@ export function AdvancedChart({
|
||||
orders.forEach((order: any) => {
|
||||
console.log('[AdvancedChart] Processing order:', order)
|
||||
|
||||
// 处理字段名:支持PascalCase和snake_case
|
||||
// Handle field names: support PascalCase and snake_case
|
||||
const filledAt = order.filled_at || order.FilledAt || order.created_at || order.CreatedAt
|
||||
const avgPrice = order.avg_fill_price || order.AvgFillPrice || order.price || order.Price
|
||||
const orderAction = order.order_action || order.OrderAction
|
||||
const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL
|
||||
const symbol = order.symbol || order.Symbol
|
||||
|
||||
// 跳过没有成交时间或价格的订单
|
||||
// Skip orders without fill time or price
|
||||
if (!filledAt || !avgPrice || avgPrice === 0) {
|
||||
console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice })
|
||||
return
|
||||
@@ -277,7 +278,7 @@ export function AdvancedChart({
|
||||
return
|
||||
}
|
||||
|
||||
// 根据 order_action 判断是开仓还是平仓
|
||||
// Determine open/close from order_action
|
||||
let action: 'open' | 'close' = 'open'
|
||||
let positionSide: 'long' | 'short' = 'long'
|
||||
|
||||
@@ -290,7 +291,7 @@ export function AdvancedChart({
|
||||
positionSide = orderAction.includes('LONG') ? 'long' : 'short'
|
||||
}
|
||||
} else {
|
||||
// 如果没有 order_action,根据 side 判断
|
||||
// If no order_action, infer from side
|
||||
positionSide = side === 'buy' ? 'long' : 'short'
|
||||
}
|
||||
|
||||
@@ -307,7 +308,7 @@ export function AdvancedChart({
|
||||
time: timeSeconds,
|
||||
price: avgPrice,
|
||||
side: positionSide,
|
||||
rawSide: side, // 原始 side 字段 (buy/sell)
|
||||
rawSide: side, // Original side field (buy/sell)
|
||||
action: action,
|
||||
symbol,
|
||||
})
|
||||
@@ -321,7 +322,7 @@ export function AdvancedChart({
|
||||
}
|
||||
}
|
||||
|
||||
// 获取交易所挂单 (止盈止损订单)
|
||||
// Fetch exchange open orders (TP/SL)
|
||||
const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => {
|
||||
try {
|
||||
console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol)
|
||||
@@ -341,7 +342,7 @@ export function AdvancedChart({
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
// Initialize chart
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current) return
|
||||
|
||||
@@ -424,7 +425,7 @@ export function AdvancedChart({
|
||||
|
||||
chartRef.current = chart
|
||||
|
||||
// 创建K线系列
|
||||
// Create candlestick series
|
||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||
upColor: '#0ECB81',
|
||||
downColor: '#F6465D',
|
||||
@@ -435,7 +436,7 @@ export function AdvancedChart({
|
||||
})
|
||||
candlestickSeriesRef.current = candlestickSeries as any
|
||||
|
||||
// 创建成交量系列
|
||||
// Create volume series
|
||||
const volumeSeries = chart.addSeries(HistogramSeries, {
|
||||
color: '#26a69a',
|
||||
priceFormat: {
|
||||
@@ -447,7 +448,7 @@ export function AdvancedChart({
|
||||
})
|
||||
volumeSeriesRef.current = volumeSeries as any
|
||||
|
||||
// 响应式调整 (ResizeObserver)
|
||||
// Responsive resize (ResizeObserver)
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (entries.length === 0 || !entries[0].contentRect) return
|
||||
const { width, height } = entries[0].contentRect
|
||||
@@ -458,7 +459,7 @@ export function AdvancedChart({
|
||||
resizeObserver.observe(chartContainerRef.current)
|
||||
}
|
||||
|
||||
// 监听鼠标移动,显示 OHLC 信息
|
||||
// Listen for crosshair movement to show OHLC info
|
||||
chart.subscribeCrosshairMove((param) => {
|
||||
if (!param.time || !param.point || !candlestickSeriesRef.current) {
|
||||
setTooltipData(null)
|
||||
@@ -473,7 +474,7 @@ export function AdvancedChart({
|
||||
|
||||
const candleData = data as any
|
||||
|
||||
// 从存储的数据中获取 volume 和 quoteVolume
|
||||
// Get volume and quoteVolume from stored data
|
||||
const klineExtra = klineDataRef.current.get(param.time as number) || { volume: 0, quoteVolume: 0 }
|
||||
|
||||
setTooltipData({
|
||||
@@ -496,18 +497,18 @@ export function AdvancedChart({
|
||||
}, []) // Chart is created once, ResizeObserver handles dimension changes
|
||||
|
||||
|
||||
// 加载数据和指标
|
||||
// Load data and indicators
|
||||
useEffect(() => {
|
||||
// 当 symbol 或 interval 改变时,重置初始加载标志(以便自动适配新数据)
|
||||
// Reset initial load flag when symbol/interval changes (for auto-fit)
|
||||
isInitialLoadRef.current = true
|
||||
|
||||
// 清除旧的标记数据,避免旧数据影响新图表
|
||||
// Clear old marker data to prevent stale data in new chart
|
||||
currentMarkersDataRef.current = []
|
||||
if (seriesMarkersRef.current) {
|
||||
try {
|
||||
seriesMarkersRef.current.setMarkers([])
|
||||
} catch (e) {
|
||||
// 忽略错误,稍后会重新创建
|
||||
// Ignore errors, will be recreated later
|
||||
}
|
||||
seriesMarkersRef.current = null
|
||||
}
|
||||
@@ -516,30 +517,30 @@ export function AdvancedChart({
|
||||
if (!candlestickSeriesRef.current) return
|
||||
|
||||
console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '')
|
||||
// 只在首次加载时显示 loading,刷新时不显示避免闪烁
|
||||
// Only show loading on first load, avoid flicker on refresh
|
||||
if (!isRefresh) {
|
||||
setLoading(true)
|
||||
}
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// 1. 获取K线数据
|
||||
// 1. Fetch kline data
|
||||
const klineData = await fetchKlineData(symbol, interval)
|
||||
console.log('[AdvancedChart] Loaded', klineData.length, 'klines')
|
||||
candlestickSeriesRef.current.setData(klineData)
|
||||
|
||||
// 存储 volume/quoteVolume 数据供 tooltip 使用
|
||||
// Store volume/quoteVolume data for tooltip
|
||||
klineDataRef.current.clear()
|
||||
klineData.forEach((k: any) => {
|
||||
klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 })
|
||||
})
|
||||
|
||||
// 1.5 计算行情统计数据
|
||||
// 1.5 Calculate market stats
|
||||
if (klineData.length > 1) {
|
||||
const latestKline = klineData[klineData.length - 1]
|
||||
const prevKline = klineData[klineData.length - 2]
|
||||
|
||||
// 涨跌幅:当前K线收盘价 vs 前一根K线收盘价
|
||||
// Price change: current candle close vs previous candle close
|
||||
const priceChange = latestKline.close - prevKline.close
|
||||
const priceChangePercent = (priceChange / prevKline.close) * 100
|
||||
|
||||
@@ -565,7 +566,7 @@ export function AdvancedChart({
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 显示成交量
|
||||
// 2. Display volume
|
||||
if (volumeSeriesRef.current) {
|
||||
const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled
|
||||
if (volumeEnabled) {
|
||||
@@ -576,15 +577,15 @@ export function AdvancedChart({
|
||||
}))
|
||||
volumeSeriesRef.current.setData(volumeData)
|
||||
} else {
|
||||
// 关闭成交量时清空数据
|
||||
// Clear data when volume is disabled
|
||||
volumeSeriesRef.current.setData([])
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 添加指标
|
||||
// 3. Add indicators
|
||||
updateIndicators(klineData)
|
||||
|
||||
// 4. 获取并显示订单标记
|
||||
// 4. Fetch and display order markers
|
||||
if (traderID && candlestickSeriesRef.current) {
|
||||
console.log('[AdvancedChart] Starting to fetch orders...')
|
||||
const orders = await fetchOrders(traderID, symbol)
|
||||
@@ -593,17 +594,17 @@ export function AdvancedChart({
|
||||
if (orders.length > 0) {
|
||||
console.log('[AdvancedChart] Creating markers from', orders.length, 'orders')
|
||||
|
||||
// 提取 K 线时间数组(已排序)
|
||||
// Extract sorted kline time array
|
||||
const klineTimes = klineData.map((k: any) => k.time as number)
|
||||
const klineMinTime = klineTimes[0] || 0
|
||||
const klineMaxTime = klineTimes[klineTimes.length - 1] || 0
|
||||
console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)')
|
||||
|
||||
// 二分查找:找到订单时间所属的 K 线蜡烛
|
||||
// 返回 time <= orderTime 的最大 K 线时间
|
||||
// Binary search: find the kline candle for the order time
|
||||
// Return the largest kline time <= orderTime
|
||||
const findCandleTime = (orderTime: number): number | null => {
|
||||
if (orderTime < klineMinTime || orderTime > klineMaxTime) {
|
||||
return null // 超出范围
|
||||
return null // Out of range
|
||||
}
|
||||
|
||||
let left = 0
|
||||
@@ -621,11 +622,11 @@ export function AdvancedChart({
|
||||
return klineTimes[left]
|
||||
}
|
||||
|
||||
// 按 K 线时间分组统计订单
|
||||
// Group orders by kline time
|
||||
const ordersByCandle = new Map<number, { buys: number; sells: number }>()
|
||||
|
||||
orders.forEach(order => {
|
||||
// 使用二分查找找到对应的 K 线蜡烛时间
|
||||
// Use binary search to find matching kline candle time
|
||||
const candleTime = findCandleTime(order.time)
|
||||
|
||||
if (candleTime === null) {
|
||||
@@ -643,7 +644,7 @@ export function AdvancedChart({
|
||||
ordersByCandle.set(candleTime, existing)
|
||||
})
|
||||
|
||||
// 为每个有订单的 K 线创建标记
|
||||
// Create markers for each kline with orders
|
||||
const markers: Array<{
|
||||
time: Time
|
||||
position: 'belowBar' | 'aboveBar'
|
||||
@@ -654,7 +655,7 @@ export function AdvancedChart({
|
||||
}> = []
|
||||
|
||||
ordersByCandle.forEach((counts, candleTime) => {
|
||||
// 显示买入标记(绿色,在K线下方)
|
||||
// Show buy markers (green, below bar)
|
||||
if (counts.buys > 0) {
|
||||
markers.push({
|
||||
time: candleTime as Time,
|
||||
@@ -665,7 +666,7 @@ export function AdvancedChart({
|
||||
size: 1,
|
||||
})
|
||||
}
|
||||
// 显示卖出标记(红色,在K线上方)
|
||||
// Show sell markers (red, above bar)
|
||||
if (counts.sells > 0) {
|
||||
markers.push({
|
||||
time: candleTime as Time,
|
||||
@@ -678,7 +679,7 @@ export function AdvancedChart({
|
||||
}
|
||||
})
|
||||
|
||||
// 按时间排序(lightweight-charts 要求标记按时间顺序)
|
||||
// Sort by time (lightweight-charts requires chronological order)
|
||||
markers.sort((a, b) => (a.time as number) - (b.time as number))
|
||||
|
||||
console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length)
|
||||
@@ -687,17 +688,17 @@ export function AdvancedChart({
|
||||
console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2))
|
||||
|
||||
try {
|
||||
// 存储标记数据供后续切换使用
|
||||
// Store marker data for later toggle use
|
||||
currentMarkersDataRef.current = markers
|
||||
|
||||
// 使用 v5 API: createSeriesMarkers
|
||||
// Using v5 API: createSeriesMarkers
|
||||
const markersToShow = showOrderMarkers ? markers : []
|
||||
|
||||
if (seriesMarkersRef.current) {
|
||||
// 如果已经存在,更新标记
|
||||
// If already exists, update markers
|
||||
seriesMarkersRef.current.setMarkers(markersToShow)
|
||||
} else {
|
||||
// 首次创建标记
|
||||
// First time creating markers
|
||||
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow)
|
||||
}
|
||||
console.log('[AdvancedChart] ✅ Markers updated! Count:', markersToShow.length, 'Visible:', showOrderMarkers)
|
||||
@@ -721,7 +722,7 @@ export function AdvancedChart({
|
||||
})
|
||||
}
|
||||
|
||||
// 只在初始加载时自动适配视图,避免刷新时抖动
|
||||
// Auto-fit view only on initial load, avoid jitter on refresh
|
||||
if (isInitialLoadRef.current) {
|
||||
chartRef.current?.timeScale().fitContent()
|
||||
isInitialLoadRef.current = false
|
||||
@@ -734,26 +735,26 @@ export function AdvancedChart({
|
||||
}
|
||||
}
|
||||
|
||||
loadData(false) // 首次加载
|
||||
loadData(false) // Initial load
|
||||
|
||||
// 实时自动刷新 (5秒更新一次)
|
||||
// Real-time auto-refresh (every 5 seconds)
|
||||
const refreshInterval = setInterval(() => loadData(true), 5000)
|
||||
return () => clearInterval(refreshInterval)
|
||||
}, [symbol, interval, traderID, exchange])
|
||||
|
||||
// 单独刷新挂单价格线 (60秒刷新一次,避免频繁调用交易所API)
|
||||
// Refresh open order price lines separately (every 60s, avoid frequent exchange API calls)
|
||||
useEffect(() => {
|
||||
if (!traderID || !candlestickSeriesRef.current) return
|
||||
|
||||
// 加载挂单并显示价格线
|
||||
// Load open orders and display price lines
|
||||
const loadOpenOrders = async () => {
|
||||
try {
|
||||
// 先清除旧的价格线
|
||||
// Clear old price lines first
|
||||
priceLinesRef.current.forEach(line => {
|
||||
try {
|
||||
candlestickSeriesRef.current?.removePriceLine(line)
|
||||
} catch (e) {
|
||||
// 忽略清除错误
|
||||
// Ignore clear error
|
||||
}
|
||||
})
|
||||
priceLinesRef.current = []
|
||||
@@ -763,28 +764,28 @@ export function AdvancedChart({
|
||||
|
||||
if (openOrders.length > 0 && candlestickSeriesRef.current) {
|
||||
openOrders.forEach(order => {
|
||||
// 获取触发价格 (止损/止盈用 stop_price,限价单用 price)
|
||||
// Get trigger price (SL/TP use stop_price, limit orders use price)
|
||||
const linePrice = order.stop_price > 0 ? order.stop_price : order.price
|
||||
if (linePrice <= 0) return
|
||||
|
||||
// 判断订单类型
|
||||
// Determine order type
|
||||
const isStopLoss = order.type.includes('STOP') || order.type.includes('SL')
|
||||
const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP')
|
||||
const isLimit = order.type === 'LIMIT'
|
||||
|
||||
// 设置价格线样式
|
||||
let lineColor = '#F0B90B' // 默认黄色
|
||||
const lineStyle = 2 // 虚线
|
||||
// Set price line style
|
||||
let lineColor = '#F0B90B' // Default yellow
|
||||
const lineStyle = 2 // dashed
|
||||
let title = ''
|
||||
|
||||
if (isStopLoss) {
|
||||
lineColor = '#F6465D' // 红色 - 止损
|
||||
lineColor = '#F6465D' // red - stop loss
|
||||
title = `SL ${order.quantity}`
|
||||
} else if (isTakeProfit) {
|
||||
lineColor = '#0ECB81' // 绿色 - 止盈
|
||||
lineColor = '#0ECB81' // green - take profit
|
||||
title = `TP ${order.quantity}`
|
||||
} else if (isLimit) {
|
||||
lineColor = '#F0B90B' // 黄色 - 限价单
|
||||
lineColor = '#F0B90B' // yellow - limit order
|
||||
title = `Limit ${order.side} ${order.quantity}`
|
||||
} else {
|
||||
title = `${order.type} ${order.quantity}`
|
||||
@@ -810,10 +811,10 @@ export function AdvancedChart({
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载 (延迟1秒等待图表初始化完成)
|
||||
// Initial load (delay 1s to wait for chart initialization)
|
||||
const initialTimeout = setTimeout(loadOpenOrders, 1000)
|
||||
|
||||
// 60秒刷新一次挂单
|
||||
// Refresh open orders every 60 seconds
|
||||
const openOrdersInterval = setInterval(loadOpenOrders, 60000)
|
||||
|
||||
return () => {
|
||||
@@ -822,7 +823,7 @@ export function AdvancedChart({
|
||||
}
|
||||
}, [symbol, traderID])
|
||||
|
||||
// 单独处理订单标记的显示/隐藏,避免重新加载数据
|
||||
// Handle order marker show/hide separately to avoid reloading data
|
||||
useEffect(() => {
|
||||
if (!seriesMarkersRef.current) return
|
||||
|
||||
@@ -835,17 +836,17 @@ export function AdvancedChart({
|
||||
}
|
||||
}, [showOrderMarkers])
|
||||
|
||||
// 更新指标
|
||||
// Update indicators
|
||||
const updateIndicators = (klineData: Kline[]) => {
|
||||
if (!chartRef.current) return
|
||||
|
||||
// 清除旧指标
|
||||
// Clear old indicators
|
||||
indicatorSeriesRef.current.forEach(series => {
|
||||
chartRef.current?.removeSeries(series as any)
|
||||
})
|
||||
indicatorSeriesRef.current.clear()
|
||||
|
||||
// 添加启用的指标
|
||||
// Add enabled indicators
|
||||
indicators.forEach(indicator => {
|
||||
if (!indicator.enabled || !chartRef.current) return
|
||||
|
||||
@@ -864,7 +865,7 @@ export function AdvancedChart({
|
||||
color: indicator.color,
|
||||
lineWidth: 2,
|
||||
title: indicator.name,
|
||||
lineStyle: 2, // 虚线
|
||||
lineStyle: 2, // dashed
|
||||
})
|
||||
series.setData(emaData as any)
|
||||
indicatorSeriesRef.current.set(indicator.id, series)
|
||||
@@ -900,7 +901,7 @@ export function AdvancedChart({
|
||||
})
|
||||
}
|
||||
|
||||
// 切换指标
|
||||
// Toggle indicator
|
||||
const toggleIndicator = (id: string) => {
|
||||
setIndicators(prev =>
|
||||
prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind))
|
||||
@@ -980,7 +981,7 @@ export function AdvancedChart({
|
||||
<div className="flex items-center gap-1.5">
|
||||
{loading && (
|
||||
<span className="text-[10px] text-yellow-400 animate-pulse mr-2">
|
||||
{language === 'zh' ? '更新中...' : 'Updating...'}
|
||||
{t('advancedChart.updating', language)}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
@@ -992,7 +993,7 @@ export function AdvancedChart({
|
||||
}}
|
||||
>
|
||||
<Settings className="w-3 h-3" />
|
||||
<span>{language === 'zh' ? '指标' : 'Indicators'}</span>
|
||||
<span>{t('advancedChart.indicators', language)}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -1002,14 +1003,14 @@ export function AdvancedChart({
|
||||
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
|
||||
color: showOrderMarkers ? '#10B981' : '#6B7280',
|
||||
}}
|
||||
title={language === 'zh' ? '订单标记' : 'Order Markers'}
|
||||
title={t('advancedChart.orderMarkers', language)}
|
||||
>
|
||||
<span>B/S</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 指标面板 - 专业化设计 */}
|
||||
{/* Indicator panel - professional design */}
|
||||
{showIndicatorPanel && (
|
||||
<div
|
||||
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
|
||||
@@ -1021,7 +1022,7 @@ export function AdvancedChart({
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
{/* Title bar */}
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-3 border-b"
|
||||
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
|
||||
@@ -1029,7 +1030,7 @@ export function AdvancedChart({
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart2 className="w-4 h-4 text-yellow-400" />
|
||||
<h4 className="text-sm font-bold text-white">
|
||||
{language === 'zh' ? '技术指标' : 'Technical Indicators'}
|
||||
{t('advancedChart.technicalIndicators', language)}
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
@@ -1040,7 +1041,7 @@ export function AdvancedChart({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 指标列表 */}
|
||||
{/* Indicator list */}
|
||||
<div className="p-3 space-y-1">
|
||||
{indicators.map(indicator => (
|
||||
<label
|
||||
@@ -1069,17 +1070,17 @@ export function AdvancedChart({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 底部提示 */}
|
||||
{/* Bottom hint */}
|
||||
<div
|
||||
className="px-4 py-2 text-xs text-gray-500 border-t"
|
||||
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
|
||||
>
|
||||
{language === 'zh' ? '点击选择需要显示的指标' : 'Click to toggle indicators'}
|
||||
{t('advancedChart.clickToToggle', language)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图表容器 */}
|
||||
{/* Chart container */}
|
||||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||||
<div ref={chartContainerRef} style={{ height: '100%', width: '100%' }} />
|
||||
|
||||
@@ -1151,7 +1152,7 @@ export function AdvancedChart({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* NOFX 水印 */}
|
||||
{/* NOFX watermark */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -1177,7 +1178,7 @@ export function AdvancedChart({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
|
||||
@@ -3,14 +3,15 @@ import { EquityChart } from './EquityChart'
|
||||
import { AdvancedChart } from './AdvancedChart'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { chartTabs, ts } from '../../i18n/strategy-translations'
|
||||
import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
interface ChartTabsProps {
|
||||
traderId: string
|
||||
selectedSymbol?: string // 从外部选择的币种
|
||||
updateKey?: number // 强制更新的 key
|
||||
exchangeId?: string // 交易所ID
|
||||
selectedSymbol?: string // Externally selected symbol
|
||||
updateKey?: number // Force update key
|
||||
exchangeId?: string // Exchange ID
|
||||
}
|
||||
|
||||
type ChartTab = 'equity' | 'kline'
|
||||
@@ -23,13 +24,13 @@ interface SymbolInfo {
|
||||
category: string
|
||||
}
|
||||
|
||||
// 市场类型配置
|
||||
// Market type configuration
|
||||
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 },
|
||||
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', label: { zh: '美股', en: 'Stocks' }, color: 'green', hasDropdown: false },
|
||||
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', label: { zh: '外汇', en: 'Forex' }, color: 'blue', hasDropdown: false },
|
||||
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', label: { zh: '金属', en: 'Metals' }, color: 'amber', hasDropdown: false },
|
||||
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', labelKey: 'hyperliquid' as const, color: 'cyan', hasDropdown: true },
|
||||
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', labelKey: 'crypto' as const, color: 'yellow', hasDropdown: false },
|
||||
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', labelKey: 'stocks' as const, color: 'green', hasDropdown: false },
|
||||
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', labelKey: 'forex' as const, color: 'blue', hasDropdown: false },
|
||||
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', labelKey: 'metals' as const, color: 'amber', hasDropdown: false },
|
||||
}
|
||||
|
||||
const INTERVALS: { value: Interval; label: string }[] = [
|
||||
@@ -42,12 +43,12 @@ const INTERVALS: { value: Interval; label: string }[] = [
|
||||
{ value: '1d', label: '1d' },
|
||||
]
|
||||
|
||||
// 根据交易所ID推断市场类型
|
||||
// Infer market type from exchange ID
|
||||
function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType {
|
||||
if (!exchangeId) return 'hyperliquid'
|
||||
const lower = exchangeId.toLowerCase()
|
||||
if (lower.includes('hyperliquid')) return 'hyperliquid'
|
||||
// 其他交易所默认使用 crypto 类型
|
||||
// Other exchanges default to crypto type
|
||||
return 'crypto'
|
||||
}
|
||||
|
||||
@@ -63,25 +64,25 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
const [searchFilter, setSearchFilter] = useState('')
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 当交易所ID变化时,自动切换市场类型
|
||||
// Auto-switch market type when exchange ID changes
|
||||
useEffect(() => {
|
||||
const newMarketType = getMarketTypeFromExchange(exchangeId)
|
||||
setMarketType(newMarketType)
|
||||
}, [exchangeId])
|
||||
|
||||
// 根据市场类型确定交易所
|
||||
// Determine exchange from market type
|
||||
const marketConfig = MARKET_CONFIG[marketType]
|
||||
// 优先使用传入的 exchangeId(非 hyperliquid 时)
|
||||
// Prefer passed-in exchangeId (when not hyperliquid)
|
||||
const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange)
|
||||
|
||||
// 获取可用币种列表
|
||||
// Fetch available symbol list
|
||||
useEffect(() => {
|
||||
if (marketConfig.hasDropdown) {
|
||||
fetch(`/api/symbols?exchange=${marketConfig.exchange}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.symbols) {
|
||||
// 按类别排序: crypto > stock > forex > commodity > index
|
||||
// Sort by category: crypto > stock > forex > commodity > index
|
||||
const categoryOrder: Record<string, number> = { 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 +97,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
}
|
||||
}, [marketType, marketConfig.exchange, marketConfig.hasDropdown])
|
||||
|
||||
// 点击外部关闭下拉
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
@@ -107,33 +108,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// 切换市场类型时更新默认符号
|
||||
// Update default symbol when switching market type
|
||||
const handleMarketTypeChange = (type: MarketType) => {
|
||||
setMarketType(type)
|
||||
setChartSymbol(MARKET_CONFIG[type].defaultSymbol)
|
||||
setShowDropdown(false)
|
||||
}
|
||||
|
||||
// 过滤后的币种列表
|
||||
// Filtered symbol list
|
||||
const filteredSymbols = availableSymbols.filter(s =>
|
||||
s.symbol.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
)
|
||||
|
||||
// 当从外部选择币种时,自动切换到K线图
|
||||
// Auto-switch to kline chart when symbol selected externally
|
||||
useEffect(() => {
|
||||
if (selectedSymbol) {
|
||||
console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey)
|
||||
console.log('[ChartTabs] Symbol selected:', selectedSymbol, 'updateKey:', updateKey)
|
||||
setChartSymbol(selectedSymbol)
|
||||
setActiveTab('kline')
|
||||
}
|
||||
}, [selectedSymbol, updateKey])
|
||||
|
||||
// 处理手动输入符号
|
||||
// Handle manual symbol input
|
||||
const handleSymbolSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (symbolInput.trim()) {
|
||||
let symbol = symbolInput.trim().toUpperCase()
|
||||
// 加密货币自动加 USDT 后缀
|
||||
// Auto-append USDT suffix for crypto
|
||||
if (marketType === 'crypto' && !symbol.endsWith('USDT')) {
|
||||
symbol = symbol + 'USDT'
|
||||
}
|
||||
@@ -198,7 +199,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1 opacity-70">{config.icon}</span>
|
||||
{language === 'zh' ? config.label.zh : config.label.en}
|
||||
{ts(chartTabs[config.labelKey], language)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
} from 'lightweight-charts'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { httpClient } from '../../lib/httpClient'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
// 订单接口定义
|
||||
// Order marker interface
|
||||
interface OrderMarker {
|
||||
time: number // Unix timestamp (seconds)
|
||||
price: number
|
||||
@@ -21,7 +22,7 @@ interface OrderMarker {
|
||||
symbol: string
|
||||
}
|
||||
|
||||
// K线数据接口
|
||||
// Kline data interface
|
||||
interface KlineData {
|
||||
time: UTCTimestamp
|
||||
open: number
|
||||
@@ -34,9 +35,9 @@ interface KlineData {
|
||||
interface ChartWithOrdersProps {
|
||||
symbol: string
|
||||
interval?: string // 1m, 5m, 15m, 1h, 4h, 1d
|
||||
traderID?: string // 用于获取该trader的订单
|
||||
traderID?: string // Used to fetch orders for this trader
|
||||
height?: number
|
||||
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
||||
exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
||||
}
|
||||
|
||||
export function ChartWithOrders({
|
||||
@@ -44,7 +45,7 @@ export function ChartWithOrders({
|
||||
interval = '5m',
|
||||
traderID,
|
||||
height = 500,
|
||||
exchange = 'binance', // 默认使用 binance
|
||||
exchange = 'binance', // Default to binance
|
||||
}: ChartWithOrdersProps) {
|
||||
const { language } = useLanguage()
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -56,16 +57,16 @@ export function ChartWithOrders({
|
||||
const [tooltipData, setTooltipData] = useState<any>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 解析时间:支持 Unix 时间戳(数字)或字符串格式
|
||||
// Parse time: supports Unix timestamp (number) or string format
|
||||
const parseCustomTime = (time: any): number => {
|
||||
if (!time) {
|
||||
console.warn('[ChartWithOrders] Empty time value')
|
||||
return 0
|
||||
}
|
||||
|
||||
// 如果已经是数字(Unix 时间戳)
|
||||
// If already a number (Unix timestamp)
|
||||
if (typeof time === 'number') {
|
||||
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳)
|
||||
// Determine ms vs seconds: if > 10^12, treat as milliseconds
|
||||
if (time > 1000000000000) {
|
||||
const seconds = Math.floor(time / 1000)
|
||||
console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
|
||||
@@ -78,7 +79,7 @@ export function ChartWithOrders({
|
||||
const timeStr = String(time)
|
||||
console.log('[ChartWithOrders] Parsing time string:', timeStr)
|
||||
|
||||
// 尝试标准ISO格式
|
||||
// Try standard ISO format
|
||||
const isoTime = new Date(timeStr).getTime()
|
||||
if (!isNaN(isoTime) && isoTime > 0) {
|
||||
const timestamp = Math.floor(isoTime / 1000)
|
||||
@@ -86,7 +87,7 @@ export function ChartWithOrders({
|
||||
return timestamp
|
||||
}
|
||||
|
||||
// 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据)
|
||||
// Parse custom format "MM-DD HH:mm UTC" (for legacy data)
|
||||
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
|
||||
if (match) {
|
||||
const currentYear = new Date().getFullYear()
|
||||
@@ -107,10 +108,10 @@ export function ChartWithOrders({
|
||||
return 0
|
||||
}
|
||||
|
||||
// 从我们的服务获取K线数据
|
||||
// Fetch kline data from our service
|
||||
const fetchKlineData = async (symbol: string, interval: string): Promise<KlineData[]> => {
|
||||
try {
|
||||
const limit = 2000 // 获取最近2000根K线 (更多历史数据)
|
||||
const limit = 2000 // Fetch recent 2000 candles (more historical data)
|
||||
const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}&exchange=${exchange}`
|
||||
|
||||
const result = await httpClient.get(klineUrl)
|
||||
@@ -121,10 +122,10 @@ export function ChartWithOrders({
|
||||
|
||||
const data = result.data
|
||||
|
||||
// 转换后端数据格式到 lightweight-charts 格式
|
||||
// 后端返回的是 market.Kline 格式: {OpenTime, Open, High, Low, Close, Volume, ...}
|
||||
// Convert backend data format to lightweight-charts format
|
||||
// Backend returns market.Kline format: {OpenTime, Open, High, Low, Close, Volume, ...}
|
||||
return data.map((candle: any) => ({
|
||||
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // 毫秒转秒
|
||||
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // ms to seconds
|
||||
open: candle.open,
|
||||
high: candle.high,
|
||||
low: candle.low,
|
||||
@@ -137,10 +138,10 @@ export function ChartWithOrders({
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单数据
|
||||
// Fetch order data
|
||||
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
|
||||
try {
|
||||
// 从后端 API 获取该 trader 的订单记录(只获取已成交的订单)
|
||||
// Fetch filled orders for this trader from backend API
|
||||
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=50`)
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
@@ -151,7 +152,7 @@ export function ChartWithOrders({
|
||||
const orders = result.data
|
||||
const markers: OrderMarker[] = []
|
||||
|
||||
// 转换订单数据为标记格式
|
||||
// Convert order data to marker format
|
||||
orders.forEach((order: any) => {
|
||||
const createdAt = order.created_at || order.CreatedAt
|
||||
const filledAt = order.filled_at || order.FilledAt
|
||||
@@ -162,14 +163,14 @@ export function ChartWithOrders({
|
||||
const status = order.status || order.Status
|
||||
const symbol = order.symbol || order.Symbol
|
||||
|
||||
// 使用成交时间(如果有)或创建时间
|
||||
// Use fill time (if available) or creation time
|
||||
const orderTime = filledAt || createdAt
|
||||
if (!orderTime) return
|
||||
|
||||
const timeSeconds = parseCustomTime(orderTime)
|
||||
if (timeSeconds === 0) return
|
||||
|
||||
// 使用平均成交价(如果有)或订单价格
|
||||
// Use average fill price (if available) or order price
|
||||
const orderPrice = avgPrice || price
|
||||
if (!orderPrice || orderPrice === 0) return
|
||||
|
||||
@@ -191,7 +192,7 @@ export function ChartWithOrders({
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
// Initialize chart
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current) {
|
||||
console.error('[ChartWithOrders] Container ref is null')
|
||||
@@ -201,7 +202,7 @@ export function ChartWithOrders({
|
||||
console.log('[ChartWithOrders] Initializing chart for', symbol, interval)
|
||||
|
||||
try {
|
||||
// 创建图表
|
||||
// Create chart
|
||||
const chart = createChart(chartContainerRef.current, {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: height,
|
||||
@@ -240,7 +241,7 @@ export function ChartWithOrders({
|
||||
|
||||
chartRef.current = chart
|
||||
|
||||
// 创建K线系列 (使用 v5 API)
|
||||
// Create candlestick series (using v5 API)
|
||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||
upColor: '#0ECB81',
|
||||
downColor: '#F6465D',
|
||||
@@ -252,7 +253,7 @@ export function ChartWithOrders({
|
||||
|
||||
candlestickSeriesRef.current = candlestickSeries as any
|
||||
|
||||
// 响应式调整
|
||||
// Responsive resize
|
||||
const handleResize = () => {
|
||||
if (chartContainerRef.current && chartRef.current) {
|
||||
chartRef.current.applyOptions({
|
||||
@@ -263,7 +264,7 @@ export function ChartWithOrders({
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
// 监听鼠标移动,显示 OHLC 信息
|
||||
// Listen for crosshair movement to show OHLC info
|
||||
chart.subscribeCrosshairMove((param) => {
|
||||
if (!param.time || !param.point || !candlestickSeriesRef.current) {
|
||||
setTooltipData(null)
|
||||
@@ -298,7 +299,7 @@ export function ChartWithOrders({
|
||||
}
|
||||
}, [height])
|
||||
|
||||
// 加载数据
|
||||
// Load data
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (!candlestickSeriesRef.current) {
|
||||
@@ -311,22 +312,22 @@ export function ChartWithOrders({
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// 1. 获取K线数据
|
||||
// 1. Fetch kline data
|
||||
console.log('[ChartWithOrders] Fetching kline data...')
|
||||
const klineData = await fetchKlineData(symbol, interval)
|
||||
console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles')
|
||||
candlestickSeriesRef.current.setData(klineData)
|
||||
|
||||
// 构建 K 线时间集合,用于快速查找
|
||||
// Build kline time set for quick lookup
|
||||
const klineTimeSet = new Set(klineData.map(k => k.time as number))
|
||||
const klineMinTime = klineData.length > 0 ? klineData[0].time : 0
|
||||
const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0
|
||||
console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length)
|
||||
|
||||
// 计算时间周期的秒数
|
||||
// Calculate interval in seconds
|
||||
const getIntervalSeconds = (interval: string): number => {
|
||||
const match = interval.match(/(\d+)([smhd])/)
|
||||
if (!match) return 60 // 默认1分钟
|
||||
if (!match) return 60 // Default 1 minute
|
||||
const [, num, unit] = match
|
||||
const n = parseInt(num)
|
||||
switch (unit) {
|
||||
@@ -340,7 +341,7 @@ export function ChartWithOrders({
|
||||
const intervalSeconds = getIntervalSeconds(interval)
|
||||
console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds')
|
||||
|
||||
// 2. 获取订单数据并添加标记
|
||||
// 2. Fetch order data and add markers
|
||||
if (traderID) {
|
||||
console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol)
|
||||
const orders = await fetchOrders(traderID, symbol)
|
||||
@@ -350,7 +351,7 @@ export function ChartWithOrders({
|
||||
console.log('[ChartWithOrders] No orders to display')
|
||||
}
|
||||
|
||||
// 转换订单为图表标记,并对齐到 K 线时间
|
||||
// Convert orders to chart markers, aligned to kline time
|
||||
const markers: Array<{
|
||||
time: Time
|
||||
position: 'belowBar'
|
||||
@@ -362,10 +363,10 @@ export function ChartWithOrders({
|
||||
}> = []
|
||||
|
||||
orders.forEach((order) => {
|
||||
// 将订单时间对齐到 K 线周期(向下取整)
|
||||
// Align order time to kline interval (floor)
|
||||
const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds
|
||||
|
||||
// 检查对齐后的时间是否在 K 线数据中存在
|
||||
// Check if aligned time exists in kline data
|
||||
if (!klineTimeSet.has(alignedTime)) {
|
||||
console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:',
|
||||
order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')')
|
||||
@@ -389,12 +390,12 @@ export function ChartWithOrders({
|
||||
console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart')
|
||||
|
||||
try {
|
||||
// 使用 v5 API: createSeriesMarkers
|
||||
// Using v5 API: createSeriesMarkers
|
||||
if (seriesMarkersRef.current) {
|
||||
// 如果已经存在,更新标记
|
||||
// If already exists, update markers
|
||||
seriesMarkersRef.current.setMarkers(markers)
|
||||
} else {
|
||||
// 首次创建标记
|
||||
// First time creating markers
|
||||
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers)
|
||||
}
|
||||
console.log('[ChartWithOrders] ✅ Markers set successfully!')
|
||||
@@ -403,23 +404,23 @@ export function ChartWithOrders({
|
||||
}
|
||||
}
|
||||
|
||||
// 自动适配视图
|
||||
// Auto-fit view
|
||||
chartRef.current?.timeScale().fitContent()
|
||||
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
console.error('Error loading chart data:', err)
|
||||
setError(language === 'zh' ? '加载图表数据失败' : 'Failed to load chart data')
|
||||
setError(t('chartWithOrders.failedToLoad', language))
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadData()
|
||||
|
||||
// 自动刷新 - 每30秒更新一次K线数据
|
||||
// Auto-refresh - update kline data every 30 seconds
|
||||
const refreshInterval = setInterval(() => {
|
||||
loadData()
|
||||
}, 30000) // 30秒
|
||||
}, 30000) // 30 seconds
|
||||
|
||||
return () => {
|
||||
clearInterval(refreshInterval)
|
||||
@@ -428,7 +429,7 @@ export function ChartWithOrders({
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
{/* 标题栏 */}
|
||||
{/* Title bar */}
|
||||
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">📈</span>
|
||||
@@ -438,12 +439,12 @@ export function ChartWithOrders({
|
||||
</div>
|
||||
{loading && (
|
||||
<div className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '加载中...' : 'Loading...'}
|
||||
{t('chartWithOrders.loading', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 图表容器 */}
|
||||
{/* Chart container */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div ref={chartContainerRef} />
|
||||
|
||||
@@ -498,7 +499,7 @@ export function ChartWithOrders({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{/* Error display */}
|
||||
{error && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
@@ -511,15 +512,15 @@ export function ChartWithOrders({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图例说明 */}
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
|
||||
<span>{language === 'zh' ? 'BUY (买入)' : 'BUY'}</span>
|
||||
<span>{t('chartWithOrders.buy', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold" style={{ color: '#F6465D' }}>S</span>
|
||||
<span>{language === 'zh' ? 'SELL (卖出)' : 'SELL'}</span>
|
||||
<span>{t('chartWithOrders.sell', language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,12 +21,12 @@ import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react'
|
||||
|
||||
// Time period options: 1D, 3D, 7D, 30D, All
|
||||
const TIME_PERIODS = [
|
||||
{ key: '1d', hours: 24, label: { en: '1D', zh: '1天' } },
|
||||
{ key: '3d', hours: 72, label: { en: '3D', zh: '3天' } },
|
||||
{ key: '7d', hours: 168, label: { en: '7D', zh: '7天' } },
|
||||
{ key: '30d', hours: 720, label: { en: '30D', zh: '30天' } },
|
||||
{ key: 'all', hours: 0, label: { en: 'All', zh: '全部' } },
|
||||
]
|
||||
{ key: '1d', hours: 24 },
|
||||
{ key: '3d', hours: 72 },
|
||||
{ key: '7d', hours: 168 },
|
||||
{ key: '30d', hours: 720 },
|
||||
{ key: 'all', hours: 0 },
|
||||
] as const
|
||||
|
||||
interface ComparisonChartProps {
|
||||
traders: CompetitionTraderData[]
|
||||
@@ -352,7 +352,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? period.label.zh : period.label.en}
|
||||
{t(`comparisonChart.${period.key}`, language)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -58,8 +58,8 @@ export function ConfirmDialogProvider({
|
||||
const [state, setState] = useState<ConfirmState>({
|
||||
isOpen: false,
|
||||
message: '',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
okText: 'Confirm',
|
||||
cancelText: 'Cancel',
|
||||
})
|
||||
|
||||
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||||
@@ -68,14 +68,14 @@ export function ConfirmDialogProvider({
|
||||
isOpen: true,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
okText: options.okText || '确认',
|
||||
cancelText: options.cancelText || '取消',
|
||||
okText: options.okText || 'Confirm',
|
||||
cancelText: options.cancelText || 'Cancel',
|
||||
resolve,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 注册全局 confirm 函数
|
||||
// Register global confirm function
|
||||
useEffect(() => {
|
||||
setGlobalConfirm(confirm)
|
||||
}, [confirm])
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
import katex from 'katex'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
export interface MetricDefinition {
|
||||
key: string
|
||||
@@ -241,6 +242,7 @@ export function MetricTooltip({
|
||||
|
||||
const name = language === 'zh' ? metric.nameZh : metric.nameEn
|
||||
const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn
|
||||
const formulaLabel = t('metricTooltip.formula', language as 'en' | 'zh' | 'id')
|
||||
|
||||
const tooltipContent = (
|
||||
<div
|
||||
@@ -292,7 +294,7 @@ export function MetricTooltip({
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
|
||||
{language === 'zh' ? '计算公式' : 'Formula'}
|
||||
{formulaLabel}
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -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, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
interface CoinSourceEditorProps {
|
||||
config: CoinSourceConfig
|
||||
@@ -18,52 +19,6 @@ export function CoinSourceEditor({
|
||||
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 持仓增加', 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' },
|
||||
@@ -82,15 +37,15 @@ export function CoinSourceEditor({
|
||||
totalLimit += config.ai500_limit || 10
|
||||
}
|
||||
if (config.use_oi_top) {
|
||||
sources.push(`${language === 'zh' ? 'OI增' : 'OI↑'}(${config.oi_top_limit || 10})`)
|
||||
sources.push(`${ts(coinSource.oiIncreaseShort, language)}(${config.oi_top_limit || 10})`)
|
||||
totalLimit += config.oi_top_limit || 10
|
||||
}
|
||||
if (config.use_oi_low) {
|
||||
sources.push(`${language === 'zh' ? 'OI减' : 'OI↓'}(${config.oi_low_limit || 10})`)
|
||||
sources.push(`${ts(coinSource.oiDecreaseShort, language)}(${config.oi_low_limit || 10})`)
|
||||
totalLimit += config.oi_low_limit || 10
|
||||
}
|
||||
if ((config.static_coins || []).length > 0) {
|
||||
sources.push(`${language === 'zh' ? '自定义' : 'Custom'}(${config.static_coins?.length || 0})`)
|
||||
sources.push(`${ts(coinSource.custom, language)}(${config.static_coins?.length || 0})`)
|
||||
totalLimit += config.static_coins?.length || 0
|
||||
}
|
||||
|
||||
@@ -191,7 +146,7 @@ export function CoinSourceEditor({
|
||||
{/* Source Type Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('sourceType')}
|
||||
{ts(coinSource.sourceType, language)}
|
||||
</label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
||||
@@ -209,10 +164,10 @@ export function CoinSourceEditor({
|
||||
>
|
||||
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
|
||||
<div className="text-sm font-medium text-nofx-text">
|
||||
{t(value)}
|
||||
{ts(coinSource[value as keyof typeof coinSource], language)}
|
||||
</div>
|
||||
<div className="text-xs mt-1 text-nofx-text-muted">
|
||||
{t(`${value}Desc`)}
|
||||
{ts(coinSource[`${value}Desc` as keyof typeof coinSource], language)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -223,7 +178,7 @@ export function CoinSourceEditor({
|
||||
{config.source_type === 'static' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('staticCoins')}
|
||||
{ts(coinSource.staticCoins, language)}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(config.static_coins || []).map((coin) => (
|
||||
@@ -258,7 +213,7 @@ 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"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('addCoin')}
|
||||
{ts(coinSource.addCoin, language)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -270,11 +225,11 @@ export function CoinSourceEditor({
|
||||
<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')}
|
||||
{ts(coinSource.excludedCoins, language)}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs mb-3 text-nofx-text-muted">
|
||||
{t('excludedCoinsDesc')}
|
||||
{ts(coinSource.excludedCoinsDesc, language)}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(config.excluded_coins || []).map((coin) => (
|
||||
@@ -295,7 +250,7 @@ export function CoinSourceEditor({
|
||||
))}
|
||||
{(config.excluded_coins || []).length === 0 && (
|
||||
<span className="text-xs italic text-nofx-text-muted">
|
||||
{language === 'zh' ? '无' : 'None'}
|
||||
{ts(coinSource.excludedNone, language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -314,7 +269,7 @@ 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"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
{t('addExcludedCoin')}
|
||||
{ts(coinSource.addExcludedCoin, language)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -329,7 +284,7 @@ export function CoinSourceEditor({
|
||||
<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')}
|
||||
AI500 {ts(coinSource.dataSourceConfig, language)}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
</div>
|
||||
@@ -346,13 +301,13 @@ export function CoinSourceEditor({
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-nofx-gold"
|
||||
/>
|
||||
<span className="text-nofx-text">{t('useAI500')}</span>
|
||||
<span className="text-nofx-text">{ts(coinSource.useAI500, language)}</span>
|
||||
</label>
|
||||
|
||||
{config.use_ai500 && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{t('ai500Limit')}:
|
||||
{ts(coinSource.ai500Limit, language)}:
|
||||
</span>
|
||||
<select
|
||||
value={config.ai500_limit || 10}
|
||||
@@ -371,7 +326,7 @@ export function CoinSourceEditor({
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
{ts(coinSource.nofxosNote, language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,7 +341,7 @@ export function CoinSourceEditor({
|
||||
<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 {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
|
||||
{ts(coinSource.oiIncreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
</div>
|
||||
@@ -403,13 +358,13 @@ export function CoinSourceEditor({
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-nofx-success"
|
||||
/>
|
||||
<span className="text-nofx-text">{t('useOITop')}</span>
|
||||
<span className="text-nofx-text">{ts(coinSource.useOITop, language)}</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')}:
|
||||
{ts(coinSource.oiTopLimit, language)}:
|
||||
</span>
|
||||
<select
|
||||
value={config.oi_top_limit || 10}
|
||||
@@ -428,7 +383,7 @@ export function CoinSourceEditor({
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
{ts(coinSource.nofxosNote, language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -443,7 +398,7 @@ export function CoinSourceEditor({
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')}
|
||||
{ts(coinSource.oiDecreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
</div>
|
||||
@@ -460,13 +415,13 @@ export function CoinSourceEditor({
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-red-500"
|
||||
/>
|
||||
<span className="text-nofx-text">{t('useOILow')}</span>
|
||||
<span className="text-nofx-text">{ts(coinSource.useOILow, language)}</span>
|
||||
</label>
|
||||
|
||||
{config.use_oi_low && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{t('oiLowLimit')}:
|
||||
{ts(coinSource.oiLowLimit, language)}:
|
||||
</span>
|
||||
<select
|
||||
value={config.oi_low_limit || 10}
|
||||
@@ -485,7 +440,7 @@ export function CoinSourceEditor({
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
{ts(coinSource.nofxosNote, language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -497,7 +452,7 @@ export function CoinSourceEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Shuffle className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
{t('mixedConfig')}
|
||||
{ts(coinSource.mixedConfig, language)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -566,11 +521,11 @@ export function CoinSourceEditor({
|
||||
/>
|
||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
{language === 'zh' ? 'OI 增加' : 'OI Increase'}
|
||||
{ts(coinSource.oiIncreaseLabel, language)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||
{language === 'zh' ? '适合做多' : 'For long'}
|
||||
{ts(coinSource.forLong, language)}
|
||||
</p>
|
||||
{config.use_oi_top && (
|
||||
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||
@@ -613,11 +568,11 @@ export function CoinSourceEditor({
|
||||
/>
|
||||
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
{language === 'zh' ? 'OI 减少' : 'OI Decrease'}
|
||||
{ts(coinSource.oiDecreaseLabel, language)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||
{language === 'zh' ? '适合做空' : 'For short'}
|
||||
{ts(coinSource.forShort, language)}
|
||||
</p>
|
||||
{config.use_oi_low && (
|
||||
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||
@@ -651,7 +606,7 @@ export function CoinSourceEditor({
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<List className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
{language === 'zh' ? '自定义' : 'Custom'}
|
||||
{ts(coinSource.custom, language)}
|
||||
</span>
|
||||
{(config.static_coins || []).length > 0 && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
|
||||
@@ -720,13 +675,13 @@ export function CoinSourceEditor({
|
||||
return (
|
||||
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-nofx-text-muted">{t('mixedSummary')}:</span>
|
||||
<span className="text-nofx-text-muted">{ts(coinSource.mixedSummary, language)}:</span>
|
||||
<span className="text-nofx-text font-medium">
|
||||
{sources.join(' + ')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-nofx-text-muted mt-1">
|
||||
{t('maxCoins')} {totalLimit} {t('coins')}
|
||||
{ts(coinSource.maxCoins, language)} {totalLimit} {ts(coinSource.coins, language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react'
|
||||
import type { GridStrategyConfig } from '../../types'
|
||||
import { gridConfig, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
interface GridConfigEditorProps {
|
||||
config: GridStrategyConfig
|
||||
@@ -8,7 +9,7 @@ interface GridConfigEditorProps {
|
||||
language: string
|
||||
}
|
||||
|
||||
// Default grid config
|
||||
// Default grid configuration
|
||||
export const defaultGridConfig: GridStrategyConfig = {
|
||||
symbol: 'BTCUSDT',
|
||||
grid_count: 10,
|
||||
@@ -33,71 +34,6 @@ export function GridConfigEditor({
|
||||
disabled,
|
||||
language,
|
||||
}: GridConfigEditorProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
// 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 = <K extends keyof GridStrategyConfig>(
|
||||
key: K,
|
||||
value: GridStrategyConfig[K]
|
||||
@@ -125,7 +61,7 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('tradingPair')}
|
||||
{ts(gridConfig.tradingPair, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -133,10 +69,10 @@ export function GridConfigEditor({
|
||||
{/* Symbol */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('symbol')}
|
||||
{ts(gridConfig.symbol, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('symbolDesc')}
|
||||
{ts(gridConfig.symbolDesc, language)}
|
||||
</p>
|
||||
<select
|
||||
value={config.symbol}
|
||||
@@ -157,10 +93,10 @@ export function GridConfigEditor({
|
||||
{/* Investment */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('totalInvestment')}
|
||||
{ts(gridConfig.totalInvestment, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('totalInvestmentDesc')}
|
||||
{ts(gridConfig.totalInvestmentDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -177,10 +113,10 @@ export function GridConfigEditor({
|
||||
{/* Leverage */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('leverage')}
|
||||
{ts(gridConfig.leverage, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('leverageDesc')}
|
||||
{ts(gridConfig.leverageDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -201,7 +137,7 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Grid className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('gridParameters')}
|
||||
{ts(gridConfig.gridParameters, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -209,10 +145,10 @@ export function GridConfigEditor({
|
||||
{/* Grid Count */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('gridCount')}
|
||||
{ts(gridConfig.gridCount, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('gridCountDesc')}
|
||||
{ts(gridConfig.gridCountDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -229,10 +165,10 @@ export function GridConfigEditor({
|
||||
{/* Distribution */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('distribution')}
|
||||
{ts(gridConfig.distribution, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('distributionDesc')}
|
||||
{ts(gridConfig.distributionDesc, language)}
|
||||
</p>
|
||||
<select
|
||||
value={config.distribution}
|
||||
@@ -241,9 +177,9 @@ export function GridConfigEditor({
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={inputStyle}
|
||||
>
|
||||
<option value="uniform">{t('uniform')}</option>
|
||||
<option value="gaussian">{t('gaussian')}</option>
|
||||
<option value="pyramid">{t('pyramid')}</option>
|
||||
<option value="uniform">{ts(gridConfig.uniform, language)}</option>
|
||||
<option value="gaussian">{ts(gridConfig.gaussian, language)}</option>
|
||||
<option value="pyramid">{ts(gridConfig.pyramid, language)}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,7 +190,7 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('priceBounds')}
|
||||
{ts(gridConfig.priceBounds, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -263,10 +199,10 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
||||
{t('useAtrBounds')}
|
||||
{ts(gridConfig.useAtrBounds, language)}
|
||||
</label>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('useAtrBoundsDesc')}
|
||||
{ts(gridConfig.useAtrBoundsDesc, language)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
@@ -285,10 +221,10 @@ export function GridConfigEditor({
|
||||
{config.use_atr_bounds ? (
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('atrMultiplier')}
|
||||
{ts(gridConfig.atrMultiplier, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('atrMultiplierDesc')}
|
||||
{ts(gridConfig.atrMultiplierDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -306,10 +242,10 @@ export function GridConfigEditor({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('upperPrice')}
|
||||
{ts(gridConfig.upperPrice, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('upperPriceDesc')}
|
||||
{ts(gridConfig.upperPriceDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -324,10 +260,10 @@ export function GridConfigEditor({
|
||||
</div>
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('lowerPrice')}
|
||||
{ts(gridConfig.lowerPrice, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('lowerPriceDesc')}
|
||||
{ts(gridConfig.lowerPriceDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -349,17 +285,17 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('riskControl')}
|
||||
{ts(gridConfig.riskControl, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('maxDrawdown')}
|
||||
{ts(gridConfig.maxDrawdown, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('maxDrawdownDesc')}
|
||||
{ts(gridConfig.maxDrawdownDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -375,10 +311,10 @@ export function GridConfigEditor({
|
||||
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('stopLoss')}
|
||||
{ts(gridConfig.stopLoss, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('stopLossDesc')}
|
||||
{ts(gridConfig.stopLossDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -394,10 +330,10 @@ export function GridConfigEditor({
|
||||
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('dailyLossLimit')}
|
||||
{ts(gridConfig.dailyLossLimit, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('dailyLossLimitDesc')}
|
||||
{ts(gridConfig.dailyLossLimitDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -417,10 +353,10 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
||||
{t('useMakerOnly')}
|
||||
{ts(gridConfig.useMakerOnly, language)}
|
||||
</label>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('useMakerOnlyDesc')}
|
||||
{ts(gridConfig.useMakerOnlyDesc, language)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
@@ -442,7 +378,7 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Compass className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('directionAdjust')}
|
||||
{ts(gridConfig.directionAdjust, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -451,10 +387,10 @@ export function GridConfigEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
||||
{t('enableDirectionAdjust')}
|
||||
{ts(gridConfig.enableDirectionAdjust, language)}
|
||||
</label>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('enableDirectionAdjustDesc')}
|
||||
{ts(gridConfig.enableDirectionAdjustDesc, language)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
@@ -475,30 +411,30 @@ export function GridConfigEditor({
|
||||
{/* Direction Modes Explanation */}
|
||||
<div className="p-4 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}>
|
||||
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
||||
📊 {t('directionModes')}
|
||||
📊 {ts(gridConfig.directionModes, language)}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}>
|
||||
<div>• {t('modeNeutral')}</div>
|
||||
<div>• <span style={{ color: '#0ECB81' }}>{t('modeLongBias')}</span></div>
|
||||
<div>• <span style={{ color: '#0ECB81' }}>{t('modeLong')}</span></div>
|
||||
<div>• <span style={{ color: '#F6465D' }}>{t('modeShortBias')}</span></div>
|
||||
<div>• <span style={{ color: '#F6465D' }}>{t('modeShort')}</span></div>
|
||||
<div>• {ts(gridConfig.modeNeutral, language)}</div>
|
||||
<div>• <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLongBias, language)}</span></div>
|
||||
<div>• <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLong, language)}</span></div>
|
||||
<div>• <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShortBias, language)}</span></div>
|
||||
<div>• <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShort, language)}</span></div>
|
||||
</div>
|
||||
<p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}>
|
||||
💡 {t('directionExplain')}
|
||||
💡 {ts(gridConfig.directionExplain, language)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bias Strength */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('directionBiasRatio')} (X)
|
||||
{ts(gridConfig.directionBiasRatio, language)} (X)
|
||||
</label>
|
||||
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
{t('directionBiasRatioDesc')}
|
||||
{ts(gridConfig.directionBiasRatioDesc, language)}
|
||||
</p>
|
||||
<p className="text-xs mb-3" style={{ color: '#F0B90B' }}>
|
||||
{t('directionBiasExplain')}
|
||||
{ts(gridConfig.directionBiasExplain, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
@@ -518,12 +454,12 @@ export function GridConfigEditor({
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}>
|
||||
<span style={{ color: '#0ECB81' }}>偏多/Long Bias: </span>
|
||||
<span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 买 + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 卖</span>
|
||||
<span style={{ color: '#0ECB81' }}>Long Bias: </span>
|
||||
<span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% {ts(gridConfig.buy, language)} + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% {ts(gridConfig.sell, language)}</span>
|
||||
</div>
|
||||
<div className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}>
|
||||
<span style={{ color: '#F6465D' }}>偏空/Short Bias: </span>
|
||||
<span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 买 + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 卖</span>
|
||||
<span style={{ color: '#F6465D' }}>Short Bias: </span>
|
||||
<span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% {ts(gridConfig.buy, language)} + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% {ts(gridConfig.sell, language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import type { GridRiskInfo } from '../../types'
|
||||
import { gridRisk, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
interface GridRiskPanelProps {
|
||||
traderId: string
|
||||
@@ -18,66 +19,6 @@ export function GridRiskPanel({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
// 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')
|
||||
@@ -153,7 +94,7 @@ export function GridRiskPanel({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('loading')}
|
||||
{ts(gridRisk.loading, language)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -161,7 +102,7 @@ export function GridRiskPanel({
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
|
||||
{t('error')}: {error}
|
||||
{ts(gridRisk.error, language)}: {error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -169,7 +110,7 @@ export function GridRiskPanel({
|
||||
if (!riskInfo) {
|
||||
return (
|
||||
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('noData')}
|
||||
{ts(gridRisk.noData, language)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -184,7 +125,7 @@ export function GridRiskPanel({
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||
{t('gridRisk')}
|
||||
{ts(gridRisk.gridRisk, language)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -194,7 +135,7 @@ export function GridRiskPanel({
|
||||
className="px-2 py-0.5 rounded"
|
||||
style={{ background: getRegimeColor(riskInfo.regime_level) + '20', color: getRegimeColor(riskInfo.regime_level) }}
|
||||
>
|
||||
{t(riskInfo.regime_level || 'standard')}
|
||||
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
|
||||
</span>
|
||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||
{riskInfo.effective_leverage.toFixed(1)}x
|
||||
@@ -223,19 +164,19 @@ export function GridRiskPanel({
|
||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
<TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('leverageInfo')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.leverageInfo, language)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('currentLeverage')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentLeverage, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('effectiveLeverage')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('recommendedLeverage')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
|
||||
<div
|
||||
className="font-mono"
|
||||
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}
|
||||
@@ -250,19 +191,19 @@ export function GridRiskPanel({
|
||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
<Activity className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('positionInfo')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.positionInfo, language)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('currentPosition')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPosition, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('maxPosition')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.maxPosition, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('positionPercent')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.positionPercent, language)}</div>
|
||||
<div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}>
|
||||
{riskInfo.position_percent.toFixed(1)}%
|
||||
</div>
|
||||
@@ -284,32 +225,32 @@ export function GridRiskPanel({
|
||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
<Shield className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('marketState')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.marketState, language)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('regimeLevel')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.regimeLevel, language)}</div>
|
||||
<div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}>
|
||||
{t(riskInfo.regime_level || 'standard')}
|
||||
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('currentPrice')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPrice, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('breakoutLevel')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutLevel, language)}</div>
|
||||
<div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}>
|
||||
{t(riskInfo.breakout_level || 'none')}
|
||||
{ts(gridRisk[(riskInfo.breakout_level || 'none') as keyof typeof gridRisk], language)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('breakoutDirection')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutDirection, language)}</div>
|
||||
<div
|
||||
className="font-medium"
|
||||
style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }}
|
||||
>
|
||||
{riskInfo.breakout_direction ? t(riskInfo.breakout_direction) : '-'}
|
||||
{riskInfo.breakout_direction ? ts(gridRisk[riskInfo.breakout_direction as keyof typeof gridRisk], language) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,17 +260,17 @@ export function GridRiskPanel({
|
||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
<AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('liquidationInfo')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.liquidationInfo, language)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('liquidationPrice')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationPrice, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#F6465D' }}>
|
||||
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#5E6673' }}>{t('liquidationDistance')}</div>
|
||||
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationDistance, language)}</div>
|
||||
<div className="font-mono" style={{ color: '#F6465D' }}>
|
||||
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
|
||||
</div>
|
||||
@@ -342,23 +283,23 @@ export function GridRiskPanel({
|
||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
<Box className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('boxState')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.boxState, language)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span style={{ color: '#5E6673' }}>{t('shortBox')}</span>
|
||||
<span style={{ color: '#5E6673' }}>{ts(gridRisk.shortBox, language)}</span>
|
||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span style={{ color: '#5E6673' }}>{t('midBox')}</span>
|
||||
<span style={{ color: '#5E6673' }}>{ts(gridRisk.midBox, language)}</span>
|
||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span style={{ color: '#5E6673' }}>{t('longBox')}</span>
|
||||
<span style={{ color: '#5E6673' }}>{ts(gridRisk.longBox, language)}</span>
|
||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react'
|
||||
import type { IndicatorConfig } from '../../types'
|
||||
import { indicator, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
// Default NofxOS API Key
|
||||
const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c'
|
||||
@@ -11,7 +12,7 @@ interface IndicatorEditorProps {
|
||||
language: string
|
||||
}
|
||||
|
||||
// 所有可用时间周期
|
||||
// All available timeframes
|
||||
const allTimeframes = [
|
||||
{ value: '1m', label: '1m', category: 'scalp' },
|
||||
{ value: '3m', label: '3m', category: 'scalp' },
|
||||
@@ -35,92 +36,10 @@ export function IndicatorEditor({
|
||||
disabled,
|
||||
language,
|
||||
}: IndicatorEditorProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 获取当前选中的时间周期
|
||||
// Get currently selected timeframes
|
||||
const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe]
|
||||
|
||||
// 切换时间周期选择
|
||||
// Toggle timeframe selection
|
||||
const toggleTimeframe = (tf: string) => {
|
||||
if (disabled) return
|
||||
const current = [...selectedTimeframes]
|
||||
@@ -153,7 +72,7 @@ export function IndicatorEditor({
|
||||
}
|
||||
}
|
||||
|
||||
// 设置主时间周期
|
||||
// Set primary timeframe
|
||||
const setPrimaryTimeframe = (tf: string) => {
|
||||
if (disabled) return
|
||||
onChange({
|
||||
@@ -218,10 +137,10 @@ export function IndicatorEditor({
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{t('nofxosTitle')}
|
||||
{ts(indicator.nofxosTitle, language)}
|
||||
</h3>
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{t('nofxosFeatures')}
|
||||
{ts(indicator.nofxosFeatures, language)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,12 +150,12 @@ export function IndicatorEditor({
|
||||
{hasApiKey ? (
|
||||
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
||||
<Check className="w-3 h-3" />
|
||||
{t('connected')}
|
||||
{ts(indicator.connected, language)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{t('notConfigured')}
|
||||
{ts(indicator.notConfigured, language)}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
@@ -250,7 +169,7 @@ export function IndicatorEditor({
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{t('viewApiDocs')}
|
||||
{ts(indicator.viewApiDocs, language)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,7 +183,7 @@ export function IndicatorEditor({
|
||||
value={config.nofxos_api_key || ''}
|
||||
onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })}
|
||||
disabled={disabled}
|
||||
placeholder={t('apiKeyPlaceholder')}
|
||||
placeholder={ts(indicator.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)',
|
||||
@@ -283,7 +202,7 @@ export function IndicatorEditor({
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{t('fillDefault')}
|
||||
{ts(indicator.fillDefault, language)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -291,7 +210,7 @@ export function IndicatorEditor({
|
||||
{/* NofxOS Data Sources Grid */}
|
||||
<div className="mt-4">
|
||||
<div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('nofxosDataSources')}
|
||||
{ts(indicator.nofxosDataSources, language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Quant Data */}
|
||||
@@ -307,7 +226,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.quantData, language)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -317,7 +236,7 @@ export function IndicatorEditor({
|
||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('quantDataDesc')}</p>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.quantDataDesc, language)}</p>
|
||||
{config.enable_quant_data && (
|
||||
<div className="flex gap-3 mt-2">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
@@ -362,7 +281,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.oiRanking, language)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -377,7 +296,7 @@ export function IndicatorEditor({
|
||||
className="w-3.5 h-3.5 rounded accent-green-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('oiRankingDesc')}</p>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.oiRankingDesc, language)}</p>
|
||||
{config.enable_oi_ranking && (
|
||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
@@ -422,7 +341,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#f59e0b' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('netflowRanking')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.netflowRanking, language)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -437,7 +356,7 @@ export function IndicatorEditor({
|
||||
className="w-3.5 h-3.5 rounded accent-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('netflowRankingDesc')}</p>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.netflowRankingDesc, language)}</p>
|
||||
{config.enable_netflow_ranking && (
|
||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
@@ -482,7 +401,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#ec4899' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('priceRanking')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.priceRanking, language)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -497,7 +416,7 @@ export function IndicatorEditor({
|
||||
className="w-3.5 h-3.5 rounded accent-pink-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('priceRankingDesc')}</p>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.priceRankingDesc, language)}</p>
|
||||
{config.enable_price_ranking && (
|
||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
@@ -510,7 +429,7 @@ export function IndicatorEditor({
|
||||
<option value="1h">1h</option>
|
||||
<option value="4h">4h</option>
|
||||
<option value="24h">24h</option>
|
||||
<option value="1h,4h,24h">{t('priceRankingMulti')}</option>
|
||||
<option value="1h,4h,24h">{ts(indicator.priceRankingMulti, language)}</option>
|
||||
</select>
|
||||
<select
|
||||
value={config.price_ranking_limit || 10}
|
||||
@@ -531,7 +450,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center gap-2 mt-3 p-2 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" style={{ color: '#F6465D' }} />
|
||||
<span className="text-[10px]" style={{ color: '#F6465D' }}>
|
||||
{language === 'zh' ? '请配置 API Key 以启用 NofxOS 数据源' : 'Please configure API Key to enable NofxOS data sources'}
|
||||
{ts(indicator.configureApiKey, language)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -545,8 +464,8 @@ export function IndicatorEditor({
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketData')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketDataDesc')}</span>
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketData, language)}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketDataDesc, language)}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-4">
|
||||
@@ -558,13 +477,13 @@ export function IndicatorEditor({
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('rawKlines')}</span>
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.rawKlines, language)}</span>
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex items-center gap-1" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}>
|
||||
<Lock className="w-2.5 h-2.5" />
|
||||
{t('required')}
|
||||
{ts(indicator.required, language)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{t('rawKlinesDesc')}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{ts(indicator.rawKlinesDesc, language)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
@@ -580,10 +499,10 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-3.5 h-3.5" style={{ color: '#848E9C' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('timeframes')}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.timeframes, language)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>{t('klineCount')}:</span>
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.klineCount, language)}:</span>
|
||||
<input
|
||||
type="number"
|
||||
value={config.klines.primary_count}
|
||||
@@ -602,7 +521,7 @@ export function IndicatorEditor({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{t('timeframesDesc')}</p>
|
||||
<p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{ts(indicator.timeframesDesc, language)}</p>
|
||||
|
||||
{/* Timeframe Grid */}
|
||||
<div className="space-y-1.5">
|
||||
@@ -611,7 +530,7 @@ export function IndicatorEditor({
|
||||
return (
|
||||
<div key={category} className="flex items-center gap-2">
|
||||
<span className="text-[10px] w-10 flex-shrink-0" style={{ color: categoryColors[category] }}>
|
||||
{t(category)}
|
||||
{ts(indicator[category], language)}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{categoryTfs.map((tf) => {
|
||||
@@ -654,15 +573,15 @@ export function IndicatorEditor({
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('technicalIndicators')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('technicalIndicatorsDesc')}</span>
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.technicalIndicators, language)}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.technicalIndicatorsDesc, language)}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
{/* Tip */}
|
||||
<div className="flex items-start gap-2 mb-3 p-2 rounded" style={{ background: 'rgba(14, 203, 129, 0.05)' }}>
|
||||
<Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: '#0ECB81' }} />
|
||||
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('aiCanCalculate')}</p>
|
||||
<p className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.aiCanCalculate, language)}</p>
|
||||
</div>
|
||||
|
||||
{/* Indicator Grid */}
|
||||
@@ -685,7 +604,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t(label)}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator[label as keyof typeof indicator], language)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -695,7 +614,7 @@ export function IndicatorEditor({
|
||||
className="w-4 h-4 rounded accent-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{t(desc)}</p>
|
||||
<p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{ts(indicator[desc as keyof typeof indicator], language)}</p>
|
||||
{periodKey && config[key as keyof IndicatorConfig] && (
|
||||
<input
|
||||
type="text"
|
||||
@@ -726,8 +645,8 @@ export function IndicatorEditor({
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketSentiment')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketSentimentDesc')}</span>
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketSentiment, language)}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketSentimentDesc, language)}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
@@ -748,7 +667,7 @@ export function IndicatorEditor({
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t(label)}</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator[label as keyof typeof indicator], language)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -758,7 +677,7 @@ export function IndicatorEditor({
|
||||
className="w-4 h-4 rounded accent-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px]" style={{ color: '#5E6673' }}>{t(desc)}</p>
|
||||
<p className="text-[10px]" style={{ color: '#5E6673' }}>{ts(indicator[desc as keyof typeof indicator], language)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react'
|
||||
import type { PromptSectionsConfig } from '../../types'
|
||||
import { promptSections as promptSectionsI18n, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
interface PromptSectionsEditorProps {
|
||||
config: PromptSectionsConfig | undefined
|
||||
@@ -54,29 +55,11 @@ export function PromptSectionsEditor({
|
||||
decision_process: false,
|
||||
})
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
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 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: ts(promptSectionsI18n.roleDefinition, language), desc: ts(promptSectionsI18n.roleDefinitionDesc, language) },
|
||||
{ key: 'trading_frequency', label: ts(promptSectionsI18n.tradingFrequency, language), desc: ts(promptSectionsI18n.tradingFrequencyDesc, language) },
|
||||
{ key: 'entry_standards', label: ts(promptSectionsI18n.entryStandards, language), desc: ts(promptSectionsI18n.entryStandardsDesc, language) },
|
||||
{ key: 'decision_process', label: ts(promptSectionsI18n.decisionProcess, language), desc: ts(promptSectionsI18n.decisionProcessDesc, language) },
|
||||
]
|
||||
|
||||
const currentConfig = config || {}
|
||||
@@ -107,10 +90,10 @@ export function PromptSectionsEditor({
|
||||
<FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} />
|
||||
<div>
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('promptSections')}
|
||||
{ts(promptSectionsI18n.promptSections, language)}
|
||||
</h3>
|
||||
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
{t('promptSectionsDesc')}
|
||||
{ts(promptSectionsI18n.promptSectionsDesc, language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,12 +129,12 @@ export function PromptSectionsEditor({
|
||||
className="px-1.5 py-0.5 text-[10px] rounded"
|
||||
style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }}
|
||||
>
|
||||
{language === 'zh' ? '已修改' : 'Modified'}
|
||||
{ts(promptSectionsI18n.modified, language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{value.length} {t('chars')}
|
||||
{value.length} {ts(promptSectionsI18n.chars, language)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -181,7 +164,7 @@ export function PromptSectionsEditor({
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
{t('resetToDefault')}
|
||||
{ts(promptSectionsI18n.resetToDefault, language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Globe, Lock, Eye, EyeOff } from 'lucide-react'
|
||||
import { publishSettings, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
interface PublishSettingsEditorProps {
|
||||
isPublic: boolean
|
||||
@@ -17,23 +18,9 @@ export function PublishSettingsEditor({
|
||||
disabled = false,
|
||||
language,
|
||||
}: PublishSettingsEditorProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
publishToMarket: { zh: '发布到策略市场', en: 'Publish to Market' },
|
||||
publishDesc: { zh: '策略将在市场公开展示,其他用户可发现并使用', en: 'Strategy will be publicly visible in the marketplace' },
|
||||
showConfig: { zh: '公开配置参数', en: 'Show Config' },
|
||||
showConfigDesc: { zh: '允许他人查看和复制详细配置', en: 'Allow others to view and clone config details' },
|
||||
private: { zh: '私有', en: 'PRIVATE' },
|
||||
public: { zh: '公开', en: 'PUBLIC' },
|
||||
hidden: { zh: '隐藏', en: 'HIDDEN' },
|
||||
visible: { zh: '可见', en: 'VISIBLE' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 发布开关 */}
|
||||
{/* Publish toggle */}
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
style={{
|
||||
@@ -73,10 +60,10 @@ export function PublishSettingsEditor({
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('publishToMarket')}
|
||||
{ts(publishSettings.publishToMarket, language)}
|
||||
</div>
|
||||
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||
{t('publishDesc')}
|
||||
{ts(publishSettings.publishDesc, language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +74,7 @@ export function PublishSettingsEditor({
|
||||
className="text-[10px] font-mono font-bold tracking-wider"
|
||||
style={{ color: isPublic ? '#0ECB81' : '#848E9C' }}
|
||||
>
|
||||
{isPublic ? t('public') : t('private')}
|
||||
{isPublic ? ts(publishSettings.public, language) : ts(publishSettings.private, language)}
|
||||
</span>
|
||||
<div
|
||||
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
||||
@@ -111,7 +98,7 @@ export function PublishSettingsEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 配置可见性开关 - 仅在公开时显示 */}
|
||||
{/* Config visibility toggle - only shown when public */}
|
||||
{isPublic && (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
@@ -152,10 +139,10 @@ export function PublishSettingsEditor({
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('showConfig')}
|
||||
{ts(publishSettings.showConfig, language)}
|
||||
</div>
|
||||
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||
{t('showConfigDesc')}
|
||||
{ts(publishSettings.showConfigDesc, language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +153,7 @@ export function PublishSettingsEditor({
|
||||
className="text-[10px] font-mono font-bold tracking-wider"
|
||||
style={{ color: configVisible ? '#a855f7' : '#848E9C' }}
|
||||
>
|
||||
{configVisible ? t('visible') : t('hidden')}
|
||||
{configVisible ? ts(publishSettings.visible, language) : ts(publishSettings.hidden, language)}
|
||||
</span>
|
||||
<div
|
||||
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Shield, AlertTriangle } from 'lucide-react'
|
||||
import type { RiskControlConfig } from '../../types'
|
||||
import { riskControl, ts } from '../../i18n/strategy-translations'
|
||||
|
||||
interface RiskControlEditorProps {
|
||||
config: RiskControlConfig
|
||||
@@ -14,38 +15,6 @@ export function RiskControlEditor({
|
||||
disabled,
|
||||
language,
|
||||
}: RiskControlEditorProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
positionLimits: { zh: '仓位限制', en: 'Position Limits' },
|
||||
maxPositions: { zh: '最大持仓数量', en: 'Max Positions' },
|
||||
maxPositionsDesc: { zh: '同时持有的最大币种数量', en: 'Maximum coins held simultaneously' },
|
||||
// Trading leverage (exchange leverage)
|
||||
tradingLeverage: { zh: '交易杠杆(交易所杠杆)', en: 'Trading Leverage (Exchange)' },
|
||||
btcEthLeverage: { zh: 'BTC/ETH 交易杠杆', en: 'BTC/ETH Trading Leverage' },
|
||||
btcEthLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
|
||||
altcoinLeverage: { zh: '山寨币交易杠杆', en: 'Altcoin Trading Leverage' },
|
||||
altcoinLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
|
||||
// Position value ratio (risk control) - CODE ENFORCED
|
||||
positionValueRatio: { zh: '仓位价值比例(代码强制)', en: 'Position Value Ratio (CODE ENFORCED)' },
|
||||
positionValueRatioDesc: { zh: '单仓位名义价值 / 账户净值,由代码强制执行', en: 'Position notional value / equity, enforced by code' },
|
||||
btcEthPositionValueRatio: { zh: 'BTC/ETH 仓位价值比例', en: 'BTC/ETH Position Value Ratio' },
|
||||
btcEthPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
|
||||
altcoinPositionValueRatio: { zh: '山寨币仓位价值比例', en: 'Altcoin Position Value Ratio' },
|
||||
altcoinPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
|
||||
riskParameters: { zh: '风险参数', en: 'Risk Parameters' },
|
||||
minRiskReward: { zh: '最小风险回报比', en: 'Min Risk/Reward Ratio' },
|
||||
minRiskRewardDesc: { zh: '开仓要求的最低盈亏比', en: 'Minimum profit ratio for opening' },
|
||||
maxMarginUsage: { zh: '最大保证金使用率(代码强制)', en: 'Max Margin Usage (CODE ENFORCED)' },
|
||||
maxMarginUsageDesc: { zh: '保证金使用率上限,由代码强制执行', en: 'Maximum margin utilization, enforced by code' },
|
||||
entryRequirements: { zh: '开仓要求', en: 'Entry Requirements' },
|
||||
minPositionSize: { zh: '最小开仓金额', en: 'Min Position Size' },
|
||||
minPositionSizeDesc: { zh: 'USDT 最小名义价值', en: 'Minimum notional value in USDT' },
|
||||
minConfidence: { zh: '最小信心度', en: 'Min Confidence' },
|
||||
minConfidenceDesc: { zh: 'AI 开仓信心度阈值', en: 'AI confidence threshold for entry' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
|
||||
const updateField = <K extends keyof RiskControlConfig>(
|
||||
key: K,
|
||||
value: RiskControlConfig[K]
|
||||
@@ -62,7 +31,7 @@ export function RiskControlEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('positionLimits')}
|
||||
{ts(riskControl.positionLimits, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -72,10 +41,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('maxPositions')}
|
||||
{ts(riskControl.maxPositions, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('maxPositionsDesc')}
|
||||
{ts(riskControl.maxPositionsDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
@@ -99,7 +68,7 @@ export function RiskControlEditor({
|
||||
{/* Trading Leverage (Exchange) */}
|
||||
<div className="mb-2">
|
||||
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
||||
{t('tradingLeverage')}
|
||||
{ts(riskControl.tradingLeverage, language)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
@@ -108,10 +77,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('btcEthLeverage')}
|
||||
{ts(riskControl.btcEthLeverage, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('btcEthLeverageDesc')}
|
||||
{ts(riskControl.btcEthLeverageDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -139,10 +108,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('altcoinLeverage')}
|
||||
{ts(riskControl.altcoinLeverage, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('altcoinLeverageDesc')}
|
||||
{ts(riskControl.altcoinLeverageDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -169,10 +138,10 @@ export function RiskControlEditor({
|
||||
{/* Position Value Ratio (Risk Control - CODE ENFORCED) */}
|
||||
<div className="mb-2">
|
||||
<p className="text-xs font-medium" style={{ color: '#0ECB81' }}>
|
||||
{t('positionValueRatio')}
|
||||
{ts(riskControl.positionValueRatio, language)}
|
||||
</p>
|
||||
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
{t('positionValueRatioDesc')}
|
||||
{ts(riskControl.positionValueRatioDesc, language)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -181,10 +150,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('btcEthPositionValueRatio')}
|
||||
{ts(riskControl.btcEthPositionValueRatio, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('btcEthPositionValueRatioDesc')}
|
||||
{ts(riskControl.btcEthPositionValueRatioDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -213,10 +182,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('altcoinPositionValueRatio')}
|
||||
{ts(riskControl.altcoinPositionValueRatio, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('altcoinPositionValueRatioDesc')}
|
||||
{ts(riskControl.altcoinPositionValueRatioDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -247,7 +216,7 @@ export function RiskControlEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('riskParameters')}
|
||||
{ts(riskControl.riskParameters, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -257,10 +226,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('minRiskReward')}
|
||||
{ts(riskControl.minRiskReward, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('minRiskRewardDesc')}
|
||||
{ts(riskControl.minRiskRewardDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<span style={{ color: '#848E9C' }}>1:</span>
|
||||
@@ -289,10 +258,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('maxMarginUsage')}
|
||||
{ts(riskControl.maxMarginUsage, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('maxMarginUsageDesc')}
|
||||
{ts(riskControl.maxMarginUsageDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -319,7 +288,7 @@ export function RiskControlEditor({
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Shield className="w-5 h-5" style={{ color: '#0ECB81' }} />
|
||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('entryRequirements')}
|
||||
{ts(riskControl.entryRequirements, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -329,10 +298,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('minPositionSize')}
|
||||
{ts(riskControl.minPositionSize, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('minPositionSizeDesc')}
|
||||
{ts(riskControl.minPositionSizeDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
@@ -362,10 +331,10 @@ export function RiskControlEditor({
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('minConfidence')}
|
||||
{ts(riskControl.minConfidence, language)}
|
||||
</label>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('minConfidenceDesc')}
|
||||
{ts(riskControl.minConfidenceDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
|
||||
@@ -212,9 +212,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await toast.promise(api.createTrader(data), {
|
||||
loading: '正在创建…',
|
||||
success: '创建成功',
|
||||
error: '创建失败',
|
||||
loading: t('aiTradersToast.creating', language),
|
||||
success: t('aiTradersToast.created', language),
|
||||
error: t('aiTradersToast.createFailed', language),
|
||||
})
|
||||
setShowCreateModal(false)
|
||||
await mutateTraders()
|
||||
@@ -269,9 +269,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
console.log('🔥 handleSaveEditTrader - request:', request)
|
||||
|
||||
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
||||
loading: '正在保存…',
|
||||
success: '保存成功',
|
||||
error: '保存失败',
|
||||
loading: t('aiTradersToast.saving', language),
|
||||
success: t('aiTradersToast.saved', language),
|
||||
error: t('aiTradersToast.saveFailed', language),
|
||||
})
|
||||
setShowEditModal(false)
|
||||
setEditingTrader(null)
|
||||
@@ -290,9 +290,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
try {
|
||||
await toast.promise(api.deleteTrader(traderId), {
|
||||
loading: '正在删除…',
|
||||
success: '删除成功',
|
||||
error: '删除失败',
|
||||
loading: t('aiTradersToast.deleting', language),
|
||||
success: t('aiTradersToast.deleted', language),
|
||||
error: t('aiTradersToast.deleteFailed', language),
|
||||
})
|
||||
|
||||
await mutateTraders()
|
||||
@@ -306,15 +306,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
try {
|
||||
if (running) {
|
||||
await toast.promise(api.stopTrader(traderId), {
|
||||
loading: '正在停止…',
|
||||
success: '已停止',
|
||||
error: '停止失败',
|
||||
loading: t('aiTradersToast.stopping', language),
|
||||
success: t('aiTradersToast.stopped', language),
|
||||
error: t('aiTradersToast.stopFailed', language),
|
||||
})
|
||||
} else {
|
||||
await toast.promise(api.startTrader(traderId), {
|
||||
loading: '正在启动…',
|
||||
success: '已启动',
|
||||
error: '启动失败',
|
||||
loading: t('aiTradersToast.starting', language),
|
||||
success: t('aiTradersToast.started', language),
|
||||
error: t('aiTradersToast.startFailed', language),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -329,9 +329,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
try {
|
||||
const newValue = !currentShowInCompetition
|
||||
await toast.promise(api.toggleCompetition(traderId, newValue), {
|
||||
loading: '正在更新…',
|
||||
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏',
|
||||
error: '更新失败',
|
||||
loading: t('aiTradersToast.updating', language),
|
||||
success: newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language),
|
||||
error: t('aiTradersToast.updateFailed', language),
|
||||
})
|
||||
|
||||
await mutateTraders()
|
||||
@@ -393,9 +393,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
const request = config.buildRequest(updatedItems)
|
||||
await toast.promise(config.updateApi(request), {
|
||||
loading: '正在更新配置…',
|
||||
success: '配置已更新',
|
||||
error: '更新配置失败',
|
||||
loading: t('aiTradersToast.updatingConfig', language),
|
||||
success: t('aiTradersToast.configUpdated', language),
|
||||
error: t('aiTradersToast.configUpdateFailed', language),
|
||||
})
|
||||
|
||||
const refreshedItems = await config.refreshApi()
|
||||
@@ -506,9 +506,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await toast.promise(api.updateModelConfigs(request), {
|
||||
loading: '正在更新模型配置…',
|
||||
success: '模型配置已更新',
|
||||
error: '更新模型配置失败',
|
||||
loading: t('aiTradersToast.updatingModelConfig', language),
|
||||
success: t('aiTradersToast.modelConfigUpdated', language),
|
||||
error: t('aiTradersToast.modelConfigUpdateFailed', language),
|
||||
})
|
||||
|
||||
const refreshedModels = await api.getModelConfigs()
|
||||
@@ -536,9 +536,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
try {
|
||||
await toast.promise(api.deleteExchange(exchangeId), {
|
||||
loading: language === 'zh' ? '正在删除交易所账户…' : 'Deleting exchange account...',
|
||||
success: language === 'zh' ? '交易所账户已删除' : 'Exchange account deleted',
|
||||
error: language === 'zh' ? '删除交易所账户失败' : 'Failed to delete exchange account',
|
||||
loading: t('aiTradersToast.deletingExchange', language),
|
||||
success: t('aiTradersToast.exchangeDeleted', language),
|
||||
error: t('aiTradersToast.exchangeDeleteFailed', language),
|
||||
})
|
||||
|
||||
const refreshedExchanges = await api.getExchangeConfigs()
|
||||
@@ -598,9 +598,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await toast.promise(api.updateExchangeConfigsEncrypted(request), {
|
||||
loading: language === 'zh' ? '正在更新交易所配置…' : 'Updating exchange config...',
|
||||
success: language === 'zh' ? '交易所配置已更新' : 'Exchange config updated',
|
||||
error: language === 'zh' ? '更新交易所配置失败' : 'Failed to update exchange config',
|
||||
loading: t('aiTradersToast.updatingExchangeConfig', language),
|
||||
success: t('aiTradersToast.exchangeConfigUpdated', language),
|
||||
error: t('aiTradersToast.exchangeConfigUpdateFailed', language),
|
||||
})
|
||||
} else {
|
||||
const createRequest = {
|
||||
@@ -622,9 +622,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await toast.promise(api.createExchangeEncrypted(createRequest), {
|
||||
loading: language === 'zh' ? '正在创建交易所账户…' : 'Creating exchange account...',
|
||||
success: language === 'zh' ? '交易所账户已创建' : 'Exchange account created',
|
||||
error: language === 'zh' ? '创建交易所账户失败' : 'Failed to create exchange account',
|
||||
loading: t('aiTradersToast.creatingExchange', language),
|
||||
success: t('aiTradersToast.exchangeCreated', language),
|
||||
error: t('aiTradersToast.exchangeCreateFailed', language),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ export function ExchangeConfigModal({
|
||||
toast.success(t('ipCopied', language))
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('copyIPFailed', language) || `复制失败: ${ip}`)
|
||||
toast.error(t('copyIPFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ export function ExchangeConfigModal({
|
||||
|
||||
const trimmedAccountName = accountName.trim()
|
||||
if (!trimmedAccountName) {
|
||||
toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name')
|
||||
toast.error(t('exchangeConfig.pleaseEnterAccountName', language))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ export function ExchangeConfigModal({
|
||||
}
|
||||
}
|
||||
|
||||
const stepLabels = language === 'zh' ? ['选择交易所', '配置账户'] : ['Select Exchange', 'Configure']
|
||||
const stepLabels = [t('exchangeConfig.selectExchange', language), t('exchangeConfig.configure', language)]
|
||||
const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex')
|
||||
const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex')
|
||||
|
||||
@@ -412,13 +412,13 @@ export function ExchangeConfigModal({
|
||||
{/* Exchange Grid */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '选择您的交易所' : 'Choose Your Exchange'}
|
||||
{t('exchangeConfig.chooseExchange', language)}
|
||||
</div>
|
||||
|
||||
{/* CEX */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
|
||||
{language === 'zh' ? '中心化交易所 (CEX)' : 'Centralized Exchanges'}
|
||||
{t('exchangeConfig.centralizedExchanges', language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
||||
{cexExchanges.map((template) => (
|
||||
@@ -436,7 +436,7 @@ export function ExchangeConfigModal({
|
||||
{/* DEX */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
|
||||
{language === 'zh' ? '去中心化交易所 (DEX)' : 'Decentralized Exchanges'}
|
||||
{t('exchangeConfig.decentralizedExchanges', language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
||||
{dexExchanges.map((template) => (
|
||||
@@ -477,11 +477,11 @@ export function ExchangeConfigModal({
|
||||
>
|
||||
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
|
||||
{language === 'zh' ? '注册' : 'Register'}
|
||||
{t('exchangeConfig.register', language)}
|
||||
</span>
|
||||
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}>
|
||||
{language === 'zh' ? '优惠' : 'Bonus'}
|
||||
{t('exchangeConfig.bonus', language)}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
@@ -491,13 +491,13 @@ export function ExchangeConfigModal({
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
{language === 'zh' ? '账户名称' : 'Account Name'} *
|
||||
{t('exchangeConfig.accountName', language)} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={accountName}
|
||||
onChange={(e) => setAccountName(e.target.value)}
|
||||
placeholder={language === 'zh' ? '例如:主账户、套利账户' : 'e.g., Main Account'}
|
||||
placeholder={t('exchangeConfig.accountNamePlaceholder', language)}
|
||||
className="w-full px-4 py-3 rounded-xl text-base"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
@@ -517,7 +517,7 @@ export function ExchangeConfigModal({
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ color: '#58a6ff' }}>ℹ️</span>
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '币安用户必读:使用「现货与合约交易」API' : 'Use "Spot & Futures Trading" API'}
|
||||
{t('exchangeConfig.useBinanceFuturesApi', language)}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
|
||||
@@ -532,7 +532,7 @@ export function ExchangeConfigModal({
|
||||
style={{ color: '#58a6ff' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{language === 'zh' ? '查看官方教程' : 'View Tutorial'} <ExternalLink className="w-3 h-3" />
|
||||
{t('exchangeConfig.viewTutorial', language)} <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -696,10 +696,10 @@ export function ExchangeConfigModal({
|
||||
<span style={{ fontSize: '16px' }}>🔐</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}>
|
||||
{language === 'zh' ? 'Lighter API Key 配置' : 'Lighter API Key Setup'}
|
||||
{t('exchangeConfig.lighterApiKeySetup', language)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '请在 Lighter 网站生成 API Key' : 'Generate an API Key on Lighter website'}
|
||||
{t('exchangeConfig.lighterApiKeyDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,8 +717,8 @@ export function ExchangeConfigModal({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? 'API Key 索引' : 'API Key Index'}
|
||||
<Tooltip content={language === 'zh' ? 'API Key 索引从0开始' : 'API Key index starts from 0'}>
|
||||
{t('exchangeConfig.apiKeyIndex', language)}
|
||||
<Tooltip content={t('exchangeConfig.apiKeyIndexTooltip', language)}>
|
||||
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
|
||||
</Tooltip>
|
||||
</label>
|
||||
@@ -730,7 +730,7 @@ export function ExchangeConfigModal({
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
{editingExchangeId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
||||
{editingExchangeId ? t('cancel', language) : t('exchangeConfig.back', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -738,7 +738,7 @@ export function ExchangeConfigModal({
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
{isSaving ? (t('saving', language) || '保存中...') : (
|
||||
{isSaving ? t('saving', language) : (
|
||||
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ModelConfigModal({
|
||||
|
||||
const availableModels = allModels || []
|
||||
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
|
||||
const stepLabels = language === 'zh' ? ['选择模型', '配置 API'] : ['Select Model', 'Configure API']
|
||||
const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
|
||||
@@ -192,7 +192,7 @@ function ModelSelectionStep({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{language === 'zh' ? '选择 AI 模型提供商' : 'Choose Your AI Provider'}
|
||||
{t('modelConfig.chooseProvider', language)}
|
||||
</div>
|
||||
|
||||
{/* Claw402 Featured Card */}
|
||||
@@ -217,9 +217,7 @@ function ModelSelectionStep({
|
||||
<a href="https://claw402.ai" target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded" style={{ color: '#60A5FA', background: 'rgba(96, 165, 250, 0.1)' }}>↗ claw402.ai</a>
|
||||
</div>
|
||||
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh'
|
||||
? 'USDC 按次付费 · 支持全部 AI 模型 · 无需 API Key'
|
||||
: 'Pay-per-call USDC · All AI Models · No API Key'}
|
||||
{t('modelConfig.payPerCall', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,7 +226,7 @@ function ModelSelectionStep({
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
|
||||
)}
|
||||
<div className="px-3 py-1.5 rounded-full text-xs font-bold" style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff' }}>
|
||||
{language === 'zh' ? '🔥 推荐' : '🔥 Best'}
|
||||
{'🔥 ' + t('modelConfig.recommended', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,7 +254,7 @@ function ModelSelectionStep({
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '通过钱包支付' : 'Via BlockRun Wallet'}
|
||||
{t('modelConfig.viaBlockrunWallet', language)}
|
||||
</span>
|
||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||
</div>
|
||||
@@ -274,7 +272,7 @@ function ModelSelectionStep({
|
||||
</>
|
||||
)}
|
||||
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '带金色标记的模型已配置' : 'Models with gold badge are already configured'}
|
||||
{t('modelConfig.modelsConfigured', language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -310,9 +308,7 @@ function Claw402ConfigForm({
|
||||
Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}>↗</span>
|
||||
</a>
|
||||
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh'
|
||||
? '用 USDC 按次付费,支持所有主流 AI 模型'
|
||||
: 'Pay-per-call with USDC — supports all major AI models'}
|
||||
{t('modelConfig.allModelsClaw', language)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
|
||||
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => (
|
||||
@@ -327,12 +323,10 @@ function Claw402ConfigForm({
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
|
||||
{language === 'zh' ? '① 选择 AI 模型' : '① Choose AI Model'}
|
||||
{t('modelConfig.selectAiModel', language)}
|
||||
</label>
|
||||
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '所有模型通过 Claw402 统一调用,创建后可随时切换'
|
||||
: 'All models unified via Claw402. Switch anytime after setup.'}
|
||||
{t('modelConfig.allModelsUnified', language)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{CLAW402_MODELS.map((m) => {
|
||||
@@ -372,34 +366,28 @@ function Claw402ConfigForm({
|
||||
<svg className="w-4 h-4" style={{ color: '#2563EB' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
{language === 'zh' ? '② 设置钱包' : '② Setup Wallet'}
|
||||
{t('modelConfig.setupWallet', language)}
|
||||
</label>
|
||||
|
||||
<div className="p-3 rounded-xl" style={{ background: 'rgba(37, 99, 235, 0.06)', border: '1px solid rgba(37, 99, 235, 0.15)' }}>
|
||||
<div className="text-xs mb-2" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh'
|
||||
? '💡 Claw402 使用 Base 链上的 USDC 付费,你需要一个 EVM 钱包'
|
||||
: '💡 Claw402 uses USDC on Base chain. You need an EVM wallet.'}
|
||||
{t('modelConfig.walletInfo', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span style={{ color: '#00E096' }}>•</span>
|
||||
{language === 'zh'
|
||||
? '可以用 MetaMask、Rabby 等钱包导出私钥'
|
||||
: 'Export private key from MetaMask, Rabby, etc.'}
|
||||
{t('modelConfig.exportKey', language)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span style={{ color: '#00E096' }}>•</span>
|
||||
{language === 'zh'
|
||||
? '建议新建一个专用钱包,充入少量 USDC 即可'
|
||||
: 'Recommended: create a dedicated wallet with a small USDC balance'}
|
||||
{t('modelConfig.dedicatedWallet', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
|
||||
{language === 'zh' ? '钱包私钥(Base 链 EVM)' : 'Wallet Private Key (Base Chain EVM)'}
|
||||
{t('modelConfig.walletPrivateKey', language)}
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
@@ -413,9 +401,7 @@ function Claw402ConfigForm({
|
||||
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
|
||||
<span className="mt-px">🔒</span>
|
||||
<span>
|
||||
{language === 'zh'
|
||||
? '私钥仅在本地签名使用,不会上传或发送交易。无需 ETH,无 Gas 费用。'
|
||||
: 'Private key is only used locally for signing. Never uploaded. No ETH or gas needed.'}
|
||||
{t('modelConfig.privateKeyNote', language)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -424,20 +410,20 @@ function Claw402ConfigForm({
|
||||
{/* USDC Recharge Guide */}
|
||||
<div className="p-4 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.15)' }}>
|
||||
<div className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: '#00E096' }}>
|
||||
💰 {language === 'zh' ? '如何充值 USDC' : 'How to Fund USDC'}
|
||||
{'💰 ' + t('modelConfig.howToFundUsdc', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>1.</span>
|
||||
<span>{language === 'zh' ? '从交易所(Binance / OKX / Coinbase)提 USDC 到你的钱包地址' : 'Withdraw USDC from exchange (Binance/OKX/Coinbase) to your wallet'}</span>
|
||||
<span>{t('modelConfig.fundStep1', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>2.</span>
|
||||
<span>{language === 'zh' ? '选择 Base 网络(手续费极低)' : 'Select Base network (very low fees)'}</span>
|
||||
<span>{t('modelConfig.fundStep2', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>3.</span>
|
||||
<span>{language === 'zh' ? '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)' : '$5-10 USDC lasts a long time (~$0.003/call)'}</span>
|
||||
<span>{t('modelConfig.fundStep3', language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,7 +431,7 @@ function Claw402ConfigForm({
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
||||
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -453,7 +439,7 @@ function Claw402ConfigForm({
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
||||
>
|
||||
{language === 'zh' ? '🚀 开始交易' : '🚀 Start Trading'}
|
||||
{'🚀 ' + t('modelConfig.startTrading', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -513,8 +499,8 @@ function StandardProviderConfigForm({
|
||||
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
|
||||
{selectedModel.provider?.startsWith('blockrun')
|
||||
? (language === 'zh' ? '开始使用' : 'Get Started')
|
||||
: (language === 'zh' ? '获取 API Key' : 'Get API Key')}
|
||||
? t('modelConfig.getStarted', language)
|
||||
: t('modelConfig.getApiKey', language)}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
@@ -539,7 +525,7 @@ function StandardProviderConfigForm({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
{selectedModel.provider?.startsWith('blockrun')
|
||||
? (language === 'zh' ? '钱包私钥 *' : 'Wallet Private Key *')
|
||||
? t('modelConfig.walletPrivateKeyLabel', language)
|
||||
: 'API Key *'}
|
||||
</label>
|
||||
<input
|
||||
@@ -612,7 +598,7 @@ function StandardProviderConfigForm({
|
||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{language === 'zh' ? '选择模型' : 'Select Model'}
|
||||
{t('modelConfig.selectModelLabel', language)}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{BLOCKRUN_MODELS.map((m) => {
|
||||
@@ -655,7 +641,7 @@ function StandardProviderConfigForm({
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
||||
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Check, ChevronLeft, ExternalLink, MessageCircle, Unlink, ArrowRight } f
|
||||
import { toast } from 'sonner'
|
||||
import { api } from '../../lib/api'
|
||||
import type { TelegramConfig, AIModel } from '../../types'
|
||||
import type { Language } from '../../i18n/translations'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
|
||||
// Step indicator (reused pattern from ExchangeConfigModal)
|
||||
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
|
||||
@@ -55,8 +55,6 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isUnbinding, setIsUnbinding] = useState(false)
|
||||
|
||||
const zh = language === 'zh'
|
||||
|
||||
// Load current config and available models
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -84,20 +82,20 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
|
||||
// Basic format validation: looks like "123456789:ABCdef..."
|
||||
if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) {
|
||||
toast.error(zh ? 'Bot Token 格式不正确,应为 "数字:字母数字串"' : 'Invalid Bot Token format. Expected "numbers:alphanumeric"')
|
||||
toast.error(t('telegram.invalidTokenFormat', language))
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await api.updateTelegramConfig(token.trim(), selectedModelId || undefined)
|
||||
toast.success(zh ? 'Bot Token 已保存,等待绑定' : 'Bot Token saved, waiting for binding')
|
||||
toast.success(t('telegram.tokenSaved', language))
|
||||
const updated = await api.getTelegramConfig()
|
||||
setConfig(updated)
|
||||
setToken('')
|
||||
setStep(1)
|
||||
} catch (err) {
|
||||
toast.error(zh ? '保存失败,请检查 Token 是否正确' : 'Save failed, please verify the token')
|
||||
toast.error(t('telegram.saveFailed', language))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -108,33 +106,31 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
setIsUnbinding(true)
|
||||
try {
|
||||
await api.unbindTelegram()
|
||||
toast.success(zh ? '已解绑 Telegram 账号' : 'Telegram account unbound')
|
||||
toast.success(t('telegram.unbound', language))
|
||||
const updated = await api.getTelegramConfig()
|
||||
setConfig(updated)
|
||||
setStep(updated.token_masked ? 1 : 0)
|
||||
} catch {
|
||||
toast.error(zh ? '解绑失败' : 'Unbind failed')
|
||||
toast.error(t('telegram.unbindFailed', language))
|
||||
} finally {
|
||||
setIsUnbinding(false)
|
||||
}
|
||||
}
|
||||
|
||||
const stepLabels = zh
|
||||
? ['创建 Bot', '绑定账号', '完成']
|
||||
: ['Create Bot', 'Bind Account', 'Done']
|
||||
const stepLabels = [t('telegram.createBot', language), t('telegram.bindAccount', language), t('telegram.done', language)]
|
||||
|
||||
// Model selector shared between steps
|
||||
const ModelSelector = () => (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? '选择 AI 模型(可选)' : 'Select AI Model (optional)'}
|
||||
{t('telegram.selectAiModel', language)}
|
||||
</label>
|
||||
{models.length === 0 ? (
|
||||
<div
|
||||
className="px-4 py-3 rounded-xl text-xs"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{zh ? '暂无启用的模型,请先在「AI 模型」中配置' : 'No enabled models. Configure one in AI Models first.'}
|
||||
{t('telegram.noEnabledModels', language)}
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
@@ -147,7 +143,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
color: selectedModelId ? '#EAECEF' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<option value="">{zh ? '— 自动选择(推荐)' : '— Auto-select (recommended)'}</option>
|
||||
<option value="">{t('telegram.autoSelect', language)}</option>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
|
||||
@@ -156,9 +152,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
</select>
|
||||
)}
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{zh
|
||||
? '不选则自动使用已启用的模型'
|
||||
: 'Leave blank to auto-use any enabled model'}
|
||||
{t('telegram.autoUseEnabled', language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -184,7 +178,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
|
||||
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? 'Telegram Bot 配置' : 'Telegram Bot Setup'}
|
||||
{t('telegram.botSetup', language)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +201,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="px-6 pb-6 space-y-5">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
|
||||
{zh ? '加载中...' : 'Loading...'}
|
||||
{t('telegram.loading', language)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -222,13 +216,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<span className="text-2xl">🤖</span>
|
||||
<div>
|
||||
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
|
||||
{zh ? '第一步:在 Telegram 创建你的 Bot' : 'Step 1: Create your Bot in Telegram'}
|
||||
{t('telegram.step1Title', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>1. {zh ? '打开 Telegram,搜索' : 'Open Telegram, search for'} <code className="text-blue-400">@BotFather</code></div>
|
||||
<div>2. {zh ? '发送' : 'Send'} <code className="text-blue-400">/newbot</code> {zh ? '命令' : 'command'}</div>
|
||||
<div>3. {zh ? '按提示输入 Bot 名称和用户名' : 'Follow prompts to set bot name and username'}</div>
|
||||
<div>4. {zh ? 'BotFather 会返回一个 Token,复制它' : 'BotFather will return a Token, copy it'}</div>
|
||||
<div>1. {t('telegram.step1Desc1', language)} <code className="text-blue-400">@BotFather</code></div>
|
||||
<div>2. {t('telegram.step1Desc2', language)} <code className="text-blue-400">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
|
||||
<div>3. {t('telegram.step1Desc3', language)}</div>
|
||||
<div>4. {t('telegram.step1Desc4', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,12 +236,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#2AABEE', color: '#000' }}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{zh ? '打开 @BotFather' : 'Open @BotFather'}
|
||||
{t('telegram.openBotFather', language)}
|
||||
</a>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? '粘贴 Bot Token' : 'Paste Bot Token'}
|
||||
{t('telegram.pasteToken', language)}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -258,7 +252,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{zh ? 'Token 格式:数字:字母数字串,如 123456789:ABCdef...' : 'Format: numbers:alphanumeric, e.g. 123456789:ABCdef...'}
|
||||
{t('telegram.tokenFormat', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -271,8 +265,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#2AABEE', color: '#000' }}
|
||||
>
|
||||
{isSaving
|
||||
? (zh ? '保存中...' : 'Saving...')
|
||||
: (<>{zh ? '保存并继续' : 'Save & Continue'} <ArrowRight className="w-4 h-4" /></>)
|
||||
? t('telegram.savingToken', language)
|
||||
: (<>{t('telegram.saveAndContinue', language)} <ArrowRight className="w-4 h-4" /></>)
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -289,12 +283,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<span className="text-2xl">📱</span>
|
||||
<div>
|
||||
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
|
||||
{zh ? '第二步:向你的 Bot 发送 /start' : 'Step 2: Send /start to your Bot'}
|
||||
{t('telegram.step2Title', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>1. {zh ? '在 Telegram 中搜索你刚创建的 Bot' : 'Search for your newly created Bot in Telegram'}</div>
|
||||
<div>2. {zh ? '点击 Start 或发送' : 'Click Start or send'} <code className="text-green-400">/start</code></div>
|
||||
<div>3. {zh ? 'Bot 会自动绑定到你的账号' : 'Bot will automatically bind to your account'}</div>
|
||||
<div>1. {t('telegram.step2Desc1', language)}</div>
|
||||
<div>2. {t('telegram.step2Desc2', language)} <code className="text-green-400">/start</code></div>
|
||||
<div>3. {t('telegram.step2Desc3', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,7 +302,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
|
||||
<div>
|
||||
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
||||
{zh ? '当前 Token' : 'Current Token'}
|
||||
{t('telegram.currentToken', language)}
|
||||
</div>
|
||||
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
|
||||
{config.token_masked}
|
||||
@@ -322,9 +316,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||
>
|
||||
<div className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
{zh
|
||||
? '⏳ 等待你发送 /start... 发送后刷新页面查看状态'
|
||||
: '⏳ Waiting for you to send /start... Refresh page after sending'}
|
||||
{t('telegram.waitingForStart', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -334,7 +326,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{zh ? '重新配置 Token' : 'Reconfigure Token'}
|
||||
{t('telegram.reconfigureToken', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -343,19 +335,19 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
setConfig(updated)
|
||||
if (updated.is_bound) {
|
||||
setStep(2)
|
||||
toast.success(zh ? '绑定成功!' : 'Bound successfully!')
|
||||
toast.success(t('telegram.bindSuccess', language))
|
||||
} else {
|
||||
toast.info(zh ? '尚未收到 /start,请先向 Bot 发送 /start' : 'No /start received yet. Please send /start to your Bot first')
|
||||
toast.info(t('telegram.noStartReceived', language))
|
||||
}
|
||||
} catch {
|
||||
toast.error(zh ? '检查失败' : 'Check failed')
|
||||
toast.error(t('telegram.checkFailed', language))
|
||||
}
|
||||
}}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
||||
style={{ background: '#0ECB81', color: '#000' }}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
{zh ? '检查绑定状态' : 'Check Status'}
|
||||
{t('telegram.checkStatus', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,12 +362,10 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
>
|
||||
<div className="text-4xl">🎉</div>
|
||||
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
|
||||
{zh ? 'Telegram Bot 已绑定!' : 'Telegram Bot is Active!'}
|
||||
{t('telegram.botActive', language)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{zh
|
||||
? '你现在可以通过 Telegram 用自然语言控制交易系统'
|
||||
: 'You can now control the trading system via natural language in Telegram'}
|
||||
{t('telegram.botActiveDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -387,7 +377,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
||||
{zh ? 'Bot Token' : 'Bot Token'}
|
||||
Bot Token
|
||||
</div>
|
||||
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
|
||||
{config.token_masked}
|
||||
@@ -398,7 +388,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
|
||||
{/* AI Model selector — works on active bot */}
|
||||
<BoundModelSelector
|
||||
zh={zh}
|
||||
language={language}
|
||||
models={models}
|
||||
currentModelId={config?.model_id ?? ''}
|
||||
onSaved={(modelId) => {
|
||||
@@ -412,14 +402,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
|
||||
{zh ? '支持的命令' : 'Supported Commands'}
|
||||
{t('telegram.supportedCommands', language)}
|
||||
</div>
|
||||
{[
|
||||
{ cmd: '/help', desc: zh ? '查看所有命令' : 'Show all commands' },
|
||||
{ cmd: zh ? '查看交易员状态' : 'Show trader status', desc: zh ? '自然语言查询' : 'Natural language' },
|
||||
{ cmd: zh ? '启动/停止交易员' : 'Start/stop trader', desc: zh ? '自然语言控制' : 'Natural language control' },
|
||||
{ cmd: zh ? '查看持仓' : 'View positions', desc: zh ? '实时持仓查询' : 'Real-time position query' },
|
||||
{ cmd: zh ? '配置策略' : 'Configure strategy', desc: zh ? '修改交易策略' : 'Modify trading strategy' },
|
||||
{ cmd: '/help', desc: t('telegram.cmdHelp', language) },
|
||||
{ cmd: t('telegram.cmdStatus', language), desc: t('telegram.cmdNaturalLang', language) },
|
||||
{ cmd: t('telegram.cmdStartStop', language), desc: t('telegram.cmdControl', language) },
|
||||
{ cmd: t('telegram.cmdPositions', language), desc: t('telegram.cmdPositionsDesc', language) },
|
||||
{ cmd: t('telegram.cmdStrategy', language), desc: t('telegram.cmdStrategyDesc', language) },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-2 text-xs">
|
||||
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
|
||||
@@ -438,14 +428,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
||||
>
|
||||
<Unlink className="w-4 h-4" />
|
||||
{isUnbinding ? (zh ? '解绑中...' : 'Unbinding...') : (zh ? '解绑账号' : 'Unbind Account')}
|
||||
{isUnbinding ? t('telegram.unbinding', language) : t('telegram.unbindAccount', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
||||
style={{ background: '#2AABEE', color: '#000' }}
|
||||
>
|
||||
{zh ? '完成' : 'Done'}
|
||||
{t('telegram.done', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -461,12 +451,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
||||
// BoundModelSelector — lets the user change the AI model when the bot is already active.
|
||||
// It updates the model_id without requiring re-entry of the bot token.
|
||||
function BoundModelSelector({
|
||||
zh,
|
||||
language,
|
||||
models,
|
||||
currentModelId,
|
||||
onSaved,
|
||||
}: {
|
||||
zh: boolean
|
||||
language: Language
|
||||
models: AIModel[]
|
||||
currentModelId: string
|
||||
onSaved: (modelId: string) => void
|
||||
@@ -483,9 +473,9 @@ function BoundModelSelector({
|
||||
// POST /api/telegram/model — lightweight endpoint for model-only update
|
||||
await api.updateTelegramModel(modelId)
|
||||
onSaved(modelId)
|
||||
toast.success(zh ? 'AI 模型已更新' : 'AI model updated')
|
||||
toast.success(t('telegram.modelUpdated', language))
|
||||
} catch {
|
||||
toast.error(zh ? '更新失败' : 'Update failed')
|
||||
toast.error(t('telegram.modelUpdateFailed', language))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -496,7 +486,7 @@ function BoundModelSelector({
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{zh ? 'AI 模型(用于自然语言解析)' : 'AI Model (for natural language)'}
|
||||
{t('telegram.aiModelLabel', language)}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
@@ -509,7 +499,7 @@ function BoundModelSelector({
|
||||
color: modelId ? '#EAECEF' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<option value="">{zh ? '— 自动选择' : '— Auto-select'}</option>
|
||||
<option value="">{t('telegram.aiModelAutoSelect', language)}</option>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
|
||||
@@ -522,7 +512,7 @@ function BoundModelSelector({
|
||||
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{isSaving ? '...' : (zh ? '保存' : 'Save')}
|
||||
{isSaving ? '...' : t('telegram.save', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { TraderConfigData } from '../../types'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar'
|
||||
|
||||
// 提取下划线后面的名称部分
|
||||
// Extract the name part after the last underscore
|
||||
function getShortName(fullName: string): string {
|
||||
const parts = fullName.split('_')
|
||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
||||
@@ -18,6 +20,7 @@ export function TraderConfigViewModal({
|
||||
onClose,
|
||||
traderData,
|
||||
}: TraderConfigViewModalProps) {
|
||||
const { language } = useLanguage()
|
||||
if (!isOpen || !traderData) return null
|
||||
|
||||
const InfoRow = ({
|
||||
@@ -30,7 +33,7 @@ export function TraderConfigViewModal({
|
||||
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
|
||||
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
|
||||
<span className="text-sm text-[#EAECEF] font-mono text-right">
|
||||
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
|
||||
{typeof value === 'boolean' ? (value ? t('traderConfigView.yes', language) : t('traderConfigView.no', language)) : value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -50,9 +53,9 @@ export function TraderConfigViewModal({
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-[#EAECEF]">交易员配置</h2>
|
||||
<h2 className="text-xl font-bold text-[#EAECEF]">{t('traderConfigView.traderConfig', language)}</h2>
|
||||
<p className="text-sm text-[#848E9C] mt-1">
|
||||
{traderData.trader_name} 的配置信息
|
||||
{t('traderConfigView.configInfo', language, { name: traderData.trader_name })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +70,7 @@ export function TraderConfigViewModal({
|
||||
}
|
||||
>
|
||||
<span>{traderData.is_running ? '●' : '○'}</span>
|
||||
{traderData.is_running ? '运行中' : '已停止'}
|
||||
{traderData.is_running ? t('traderConfigView.running', language) : t('traderConfigView.stopped', language)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -83,32 +86,32 @@ export function TraderConfigViewModal({
|
||||
{/* Basic Info */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
🤖 基础信息
|
||||
{'🤖 ' + t('traderConfigView.basicInfo', language)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="交易员名称"
|
||||
label={t('traderConfigView.traderName', language)}
|
||||
value={traderData.trader_name}
|
||||
/>
|
||||
<InfoRow
|
||||
label="AI模型"
|
||||
label={t('traderConfigView.aiModel', language)}
|
||||
value={getShortName(traderData.ai_model).toUpperCase()}
|
||||
/>
|
||||
<InfoRow
|
||||
label="交易所"
|
||||
label={t('traderConfigView.exchange', language)}
|
||||
value={getShortName(traderData.exchange_id).toUpperCase()}
|
||||
/>
|
||||
<InfoRow
|
||||
label="初始余额"
|
||||
label={t('traderConfigView.initialBalance', language)}
|
||||
value={`$${traderData.initial_balance.toLocaleString()}`}
|
||||
/>
|
||||
<InfoRow
|
||||
label="保证金模式"
|
||||
value={traderData.is_cross_margin ? '全仓' : '逐仓'}
|
||||
label={t('traderConfigView.marginMode', language)}
|
||||
value={traderData.is_cross_margin ? t('traderConfigView.crossMargin', language) : t('traderConfigView.isolatedMargin', language)}
|
||||
/>
|
||||
<InfoRow
|
||||
label="扫描间隔"
|
||||
value={`${traderData.scan_interval_minutes || 3} 分钟`}
|
||||
label={t('traderConfigView.scanIntervalLabel', language)}
|
||||
value={t('traderConfigView.scanInterval', language, { minutes: traderData.scan_interval_minutes || 3 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,11 +120,11 @@ export function TraderConfigViewModal({
|
||||
{traderData.strategy_id && (
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
📋 使用策略
|
||||
{'📋 ' + t('traderConfigView.strategyUsed', language)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="策略名称"
|
||||
label={t('traderConfigView.strategyName', language)}
|
||||
value={traderData.strategy_name || traderData.strategy_id}
|
||||
/>
|
||||
</div>
|
||||
@@ -135,7 +138,7 @@ export function TraderConfigViewModal({
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
||||
>
|
||||
关闭
|
||||
{t('traderConfigView.close', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,10 +45,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
// Reset 401 flag on page load to allow fresh 401 handling
|
||||
reset401Flag()
|
||||
|
||||
// 先检查是否为管理员模式(使用带缓存的系统配置获取)
|
||||
// Check if admin mode is active (uses cached system config)
|
||||
getSystemConfig()
|
||||
.then(() => {
|
||||
// 不再在管理员模式下模拟登录;统一检查本地存储
|
||||
// No longer simulate login in admin mode; check local storage uniformly
|
||||
const savedToken = localStorage.getItem('auth_token')
|
||||
const savedUser = localStorage.getItem('auth_user')
|
||||
if (savedToken && savedUser) {
|
||||
@@ -60,7 +60,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to fetch system config:', err)
|
||||
// 发生错误时,继续检查本地存储
|
||||
// On error, continue checking local storage
|
||||
const savedToken = localStorage.getItem('auth_token')
|
||||
const savedUser = localStorage.getItem('auth_user')
|
||||
|
||||
@@ -119,7 +119,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
window.history.pushState({}, '', returnUrl)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
} else {
|
||||
// 跳转到配置页面
|
||||
// Redirect to config page
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
// Unexpected success response
|
||||
return { success: false, message: data.message || '登录响应异常' }
|
||||
return { success: false, message: data.message || 'Unexpected login response' }
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
@@ -136,7 +136,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, message: '登录失败,请重试' }
|
||||
return { success: false, message: 'Login failed, please try again' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,16 +168,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
window.history.pushState({}, '', returnUrl)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
} else {
|
||||
// 跳转到仪表盘
|
||||
// Redirect to dashboard
|
||||
window.history.pushState({}, '', '/dashboard')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, message: data.error || '登录失败' }
|
||||
return { success: false, message: data.error || 'Login failed' }
|
||||
}
|
||||
} catch (e) {
|
||||
return { success: false, message: '登录失败,请重试' }
|
||||
return { success: false, message: 'Login failed, please try again' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
window.history.pushState({}, '', returnUrl)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
} else {
|
||||
// 跳转到配置页面
|
||||
// Redirect to config page
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
@@ -269,7 +269,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
return { success: false, message: data.error }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, message: '密码重置失败,请重试' }
|
||||
return { success: false, message: 'Password reset failed, please try again' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,20 @@ export const coinSource = {
|
||||
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded', es: 'Estas monedas serán excluidas de todas las fuentes' },
|
||||
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded', es: 'Agregar Excluida' },
|
||||
nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)', es: 'Usa API Key de NofxOS' },
|
||||
ai500Desc: { zh: '使用 AI500 智能筛选的热门币种', en: 'Use AI500 smart-filtered popular coins', es: 'Monedas filtradas por AI500' },
|
||||
oiTopDesc: { zh: '持仓增加榜,适合做多', en: 'OI increase ranking, for long', es: 'Ranking OI creciente, para largo' },
|
||||
oi_lowDesc: { zh: '持仓减少榜,适合做空', en: 'OI decrease ranking, for short', es: 'Ranking OI decreciente, para corto' },
|
||||
mixedDesc: { zh: '组合多种数据源', en: 'Combine multiple sources', es: 'Combinar fuentes múltiples' },
|
||||
oiIncreaseShort: { zh: 'OI增', en: 'OI↑', es: 'OI↑' },
|
||||
oiDecreaseShort: { zh: 'OI减', en: 'OI↓', es: 'OI↓' },
|
||||
custom: { zh: '自定义', en: 'Custom', es: 'Personalizado' },
|
||||
excludedNone: { zh: '无', en: 'None', es: 'Ninguno' },
|
||||
oiIncreaseTitle: { zh: 'OI 持仓增加榜', en: 'OI Increase', es: 'OI Aumento' },
|
||||
oiDecreaseTitle: { zh: 'OI 持仓减少榜', en: 'OI Decrease', es: 'OI Disminución' },
|
||||
oiIncreaseLabel: { zh: 'OI 增加', en: 'OI Increase', es: 'OI Aumento' },
|
||||
forLong: { zh: '适合做多', en: 'For long', es: 'Para largo' },
|
||||
oiDecreaseLabel: { zh: 'OI 减少', en: 'OI Decrease', es: 'OI Disminución' },
|
||||
forShort: { zh: '适合做空', en: 'For short', es: 'Para corto' },
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -83,6 +97,8 @@ export const gridConfig = {
|
||||
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell', es: 'Largo: 100% compra' },
|
||||
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell', es: 'Sesgo Corto: X% venta' },
|
||||
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell', es: 'Corto: 100% venta' },
|
||||
buy: { zh: '买', en: 'buy', es: 'compra' },
|
||||
sell: { zh: '卖', en: 'sell', es: 'venta' },
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -172,6 +188,7 @@ export const promptSections = {
|
||||
decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process', es: 'Establecer proceso' },
|
||||
resetToDefault: { zh: '重置为默认', en: 'Reset to Default', es: 'Restablecer' },
|
||||
chars: { zh: '字符', en: 'chars', es: 'caracteres' },
|
||||
modified: { zh: '已修改', en: 'Modified', es: 'Modificado' },
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -235,6 +252,7 @@ export const indicator = {
|
||||
connected: { zh: '已配置', en: 'Configured', es: 'Configurado' },
|
||||
notConfigured: { zh: '未配置', en: 'Not Configured', es: 'No Configurado' },
|
||||
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources', es: 'Fuentes NofxOS' },
|
||||
configureApiKey: { zh: '请配置 API Key 以启用 NofxOS 数据源', en: 'Please configure API Key to enable NofxOS data sources', es: 'Configure API Key para habilitar NofxOS' },
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -262,6 +280,14 @@ export const chartTabs = {
|
||||
hyperliquid: { zh: 'HL', en: 'HL', es: 'HL' },
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTION
|
||||
// ============================================================================
|
||||
|
||||
export function ts(entry: { zh: string; en: string; [k: string]: string }, lang: string): string {
|
||||
return entry[lang] ?? entry.en ?? ''
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AGGREGATED EXPORTS FOR TRANSLATIONS.TS
|
||||
// ============================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -183,13 +183,13 @@ export const backtestApi = {
|
||||
try {
|
||||
const data = text ? JSON.parse(text) : null
|
||||
throw new Error(
|
||||
data?.error || data?.message || text || '导出失败,请稍后再试'
|
||||
data?.error || data?.message || text || 'Export failed, please try again later'
|
||||
)
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message) {
|
||||
throw err
|
||||
}
|
||||
throw new Error(text || '导出失败,请稍后再试')
|
||||
throw new Error(text || 'Export failed, please try again later')
|
||||
}
|
||||
}
|
||||
return res.blob()
|
||||
|
||||
+36
-36
@@ -10,7 +10,7 @@ import { API_BASE, httpClient, CryptoService } from './helpers'
|
||||
export const configApi = {
|
||||
async getModelConfigs(): Promise<AIModel[]> {
|
||||
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
|
||||
if (!result.success) throw new Error('获取模型配置失败')
|
||||
if (!result.success) throw new Error('Failed to fetch model configs')
|
||||
return Array.isArray(result.data) ? result.data : []
|
||||
},
|
||||
|
||||
@@ -18,13 +18,13 @@ export const configApi = {
|
||||
const result = await httpClient.get<AIModel[]>(
|
||||
`${API_BASE}/supported-models`
|
||||
)
|
||||
if (!result.success) throw new Error('获取支持的模型失败')
|
||||
if (!result.success) throw new Error('Failed to fetch supported models')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getPromptTemplates(): Promise<string[]> {
|
||||
const res = await fetch(`${API_BASE}/prompt-templates`)
|
||||
if (!res.ok) throw new Error('获取提示词模板失败')
|
||||
if (!res.ok) throw new Error('Failed to fetch prompt templates')
|
||||
const data = await res.json()
|
||||
if (Array.isArray(data.templates)) {
|
||||
return data.templates.map((item: { name: string }) => item.name)
|
||||
@@ -33,41 +33,41 @@ export const configApi = {
|
||||
},
|
||||
|
||||
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
||||
// 检查是否启用了传输加密
|
||||
// Check if transport encryption is enabled
|
||||
const config = await CryptoService.fetchCryptoConfig()
|
||||
|
||||
if (!config.transport_encryption) {
|
||||
// 传输加密禁用时,直接发送明文
|
||||
// Transport encryption disabled, send plaintext
|
||||
const result = await httpClient.put(`${API_BASE}/models`, request)
|
||||
if (!result.success) throw new Error('更新模型配置失败')
|
||||
if (!result.success) throw new Error('Failed to update model configs')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取RSA公钥
|
||||
// Fetch RSA public key
|
||||
const publicKey = await CryptoService.fetchPublicKey()
|
||||
|
||||
// 初始化加密服务
|
||||
// Initialize crypto service
|
||||
await CryptoService.initialize(publicKey)
|
||||
|
||||
// 获取用户信息(从localStorage或其他地方)
|
||||
// Get user info from localStorage
|
||||
const userId = localStorage.getItem('user_id') || ''
|
||||
const sessionId = sessionStorage.getItem('session_id') || ''
|
||||
|
||||
// 加密敏感数据
|
||||
// Encrypt sensitive data
|
||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||
JSON.stringify(request),
|
||||
userId,
|
||||
sessionId
|
||||
)
|
||||
|
||||
// 发送加密数据
|
||||
// Send encrypted data
|
||||
const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload)
|
||||
if (!result.success) throw new Error('更新模型配置失败')
|
||||
if (!result.success) throw new Error('Failed to update model configs')
|
||||
},
|
||||
|
||||
async getExchangeConfigs(): Promise<Exchange[]> {
|
||||
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
|
||||
if (!result.success) throw new Error('获取交易所配置失败')
|
||||
if (!result.success) throw new Error('Failed to fetch exchange configs')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -75,7 +75,7 @@ export const configApi = {
|
||||
const result = await httpClient.get<Exchange[]>(
|
||||
`${API_BASE}/supported-exchanges`
|
||||
)
|
||||
if (!result.success) throw new Error('获取支持的交易所失败')
|
||||
if (!result.success) throw new Error('Failed to fetch supported exchanges')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -83,93 +83,93 @@ export const configApi = {
|
||||
request: UpdateExchangeConfigRequest
|
||||
): Promise<void> {
|
||||
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
||||
if (!result.success) throw new Error('更新交易所配置失败')
|
||||
if (!result.success) throw new Error('Failed to update exchange configs')
|
||||
},
|
||||
|
||||
async createExchange(request: CreateExchangeRequest): Promise<{ id: string }> {
|
||||
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
|
||||
if (!result.success) throw new Error('创建交易所账户失败')
|
||||
if (!result.success) throw new Error('Failed to create exchange account')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> {
|
||||
// 检查是否启用了传输加密
|
||||
// Check if transport encryption is enabled
|
||||
const config = await CryptoService.fetchCryptoConfig()
|
||||
|
||||
if (!config.transport_encryption) {
|
||||
// 传输加密禁用时,直接发送明文
|
||||
// Transport encryption disabled, send plaintext
|
||||
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
|
||||
if (!result.success) throw new Error('创建交易所账户失败')
|
||||
if (!result.success) throw new Error('Failed to create exchange account')
|
||||
return result.data!
|
||||
}
|
||||
|
||||
// 获取RSA公钥
|
||||
// Fetch RSA public key
|
||||
const publicKey = await CryptoService.fetchPublicKey()
|
||||
|
||||
// 初始化加密服务
|
||||
// Initialize crypto service
|
||||
await CryptoService.initialize(publicKey)
|
||||
|
||||
// 获取用户信息
|
||||
// Get user info
|
||||
const userId = localStorage.getItem('user_id') || ''
|
||||
const sessionId = sessionStorage.getItem('session_id') || ''
|
||||
|
||||
// 加密敏感数据
|
||||
// Encrypt sensitive data
|
||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||
JSON.stringify(request),
|
||||
userId,
|
||||
sessionId
|
||||
)
|
||||
|
||||
// 发送加密数据
|
||||
// Send encrypted data
|
||||
const result = await httpClient.post<{ id: string }>(
|
||||
`${API_BASE}/exchanges`,
|
||||
encryptedPayload
|
||||
)
|
||||
if (!result.success) throw new Error('创建交易所账户失败')
|
||||
if (!result.success) throw new Error('Failed to create exchange account')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async deleteExchange(exchangeId: string): Promise<void> {
|
||||
const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`)
|
||||
if (!result.success) throw new Error('删除交易所账户失败')
|
||||
if (!result.success) throw new Error('Failed to delete exchange account')
|
||||
},
|
||||
|
||||
async updateExchangeConfigsEncrypted(
|
||||
request: UpdateExchangeConfigRequest
|
||||
): Promise<void> {
|
||||
// 检查是否启用了传输加密
|
||||
// Check if transport encryption is enabled
|
||||
const config = await CryptoService.fetchCryptoConfig()
|
||||
|
||||
if (!config.transport_encryption) {
|
||||
// 传输加密禁用时,直接发送明文
|
||||
// Transport encryption disabled, send plaintext
|
||||
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
||||
if (!result.success) throw new Error('更新交易所配置失败')
|
||||
if (!result.success) throw new Error('Failed to update exchange configs')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取RSA公钥
|
||||
// Fetch RSA public key
|
||||
const publicKey = await CryptoService.fetchPublicKey()
|
||||
|
||||
// 初始化加密服务
|
||||
// Initialize crypto service
|
||||
await CryptoService.initialize(publicKey)
|
||||
|
||||
// 获取用户信息(从localStorage或其他地方)
|
||||
// Get user info from localStorage
|
||||
const userId = localStorage.getItem('user_id') || ''
|
||||
const sessionId = sessionStorage.getItem('session_id') || ''
|
||||
|
||||
// 加密敏感数据
|
||||
// Encrypt sensitive data
|
||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||
JSON.stringify(request),
|
||||
userId,
|
||||
sessionId
|
||||
)
|
||||
|
||||
// 发送加密数据
|
||||
// Send encrypted data
|
||||
const result = await httpClient.put(
|
||||
`${API_BASE}/exchanges`,
|
||||
encryptedPayload
|
||||
)
|
||||
if (!result.success) throw new Error('更新交易所配置失败')
|
||||
if (!result.success) throw new Error('Failed to update exchange configs')
|
||||
},
|
||||
|
||||
async getServerIP(): Promise<{
|
||||
@@ -180,7 +180,7 @@ export const configApi = {
|
||||
public_ip: string
|
||||
message: string
|
||||
}>(`${API_BASE}/server-ip`)
|
||||
if (!result.success) throw new Error('获取服务器IP失败')
|
||||
if (!result.success) throw new Error('Failed to fetch server IP')
|
||||
return result.data!
|
||||
},
|
||||
}
|
||||
|
||||
+12
-13
@@ -15,7 +15,7 @@ export const dataApi = {
|
||||
? `${API_BASE}/status?trader_id=${traderId}`
|
||||
: `${API_BASE}/status`
|
||||
const result = await httpClient.get<SystemStatus>(url)
|
||||
if (!result.success) throw new Error('获取系统状态失败')
|
||||
if (!result.success) throw new Error('Failed to fetch system status')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -24,8 +24,7 @@ export const dataApi = {
|
||||
? `${API_BASE}/account?trader_id=${traderId}`
|
||||
: `${API_BASE}/account`
|
||||
const result = await httpClient.get<AccountInfo>(url)
|
||||
if (!result.success) throw new Error('获取账户信息失败')
|
||||
console.log('Account data fetched:', result.data)
|
||||
if (!result.success) throw new Error('Failed to fetch account info')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -34,7 +33,7 @@ export const dataApi = {
|
||||
? `${API_BASE}/positions?trader_id=${traderId}`
|
||||
: `${API_BASE}/positions`
|
||||
const result = await httpClient.get<Position[]>(url)
|
||||
if (!result.success) throw new Error('获取持仓列表失败')
|
||||
if (!result.success) throw new Error('Failed to fetch positions')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -43,7 +42,7 @@ export const dataApi = {
|
||||
? `${API_BASE}/decisions?trader_id=${traderId}`
|
||||
: `${API_BASE}/decisions`
|
||||
const result = await httpClient.get<DecisionRecord[]>(url)
|
||||
if (!result.success) throw new Error('获取决策日志失败')
|
||||
if (!result.success) throw new Error('Failed to fetch decision logs')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -60,7 +59,7 @@ export const dataApi = {
|
||||
const result = await httpClient.get<DecisionRecord[]>(
|
||||
`${API_BASE}/decisions/latest?${params}`
|
||||
)
|
||||
if (!result.success) throw new Error('获取最新决策失败')
|
||||
if (!result.success) throw new Error('Failed to fetch latest decisions')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -69,7 +68,7 @@ export const dataApi = {
|
||||
? `${API_BASE}/statistics?trader_id=${traderId}`
|
||||
: `${API_BASE}/statistics`
|
||||
const result = await httpClient.get<Statistics>(url)
|
||||
if (!result.success) throw new Error('获取统计信息失败')
|
||||
if (!result.success) throw new Error('Failed to fetch statistics')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -78,7 +77,7 @@ export const dataApi = {
|
||||
? `${API_BASE}/equity-history?trader_id=${traderId}`
|
||||
: `${API_BASE}/equity-history`
|
||||
const result = await httpClient.get<any[]>(url)
|
||||
if (!result.success) throw new Error('获取历史数据失败')
|
||||
if (!result.success) throw new Error('Failed to fetch equity history')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -87,13 +86,13 @@ export const dataApi = {
|
||||
`${API_BASE}/equity-history-batch`,
|
||||
{ trader_ids: traderIds, hours: hours || 0 }
|
||||
)
|
||||
if (!result.success) throw new Error('获取批量历史数据失败')
|
||||
if (!result.success) throw new Error('Failed to fetch batch equity history')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getTopTraders(): Promise<any[]> {
|
||||
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
|
||||
if (!result.success) throw new Error('获取前5名交易员失败')
|
||||
if (!result.success) throw new Error('Failed to fetch top traders')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -101,7 +100,7 @@ export const dataApi = {
|
||||
const result = await httpClient.get<any>(
|
||||
`${API_BASE}/trader/${traderId}/config`
|
||||
)
|
||||
if (!result.success) throw new Error('获取公开交易员配置失败')
|
||||
if (!result.success) throw new Error('Failed to fetch public trader config')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -109,7 +108,7 @@ export const dataApi = {
|
||||
const result = await httpClient.get<CompetitionData>(
|
||||
`${API_BASE}/competition`
|
||||
)
|
||||
if (!result.success) throw new Error('获取竞赛数据失败')
|
||||
if (!result.success) throw new Error('Failed to fetch competition data')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -117,7 +116,7 @@ export const dataApi = {
|
||||
const result = await httpClient.get<PositionHistoryResponse>(
|
||||
`${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}`
|
||||
)
|
||||
if (!result.success) throw new Error('获取历史仓位失败')
|
||||
if (!result.success) throw new Error('Failed to fetch position history')
|
||||
return result.data!
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function handleJSONResponse<T>(res: Response): Promise<T> {
|
||||
} catch {
|
||||
/* ignore JSON parse errors */
|
||||
}
|
||||
throw new Error(message || '请求失败')
|
||||
throw new Error(message || 'Request failed')
|
||||
}
|
||||
if (!text) {
|
||||
return {} as T
|
||||
|
||||
@@ -7,26 +7,26 @@ import { API_BASE, httpClient } from './helpers'
|
||||
export const strategyApi = {
|
||||
async getStrategies(): Promise<Strategy[]> {
|
||||
const result = await httpClient.get<{ strategies: Strategy[] }>(`${API_BASE}/strategies`)
|
||||
if (!result.success) throw new Error('获取策略列表失败')
|
||||
if (!result.success) throw new Error('Failed to fetch strategy list')
|
||||
const strategies = result.data?.strategies
|
||||
return Array.isArray(strategies) ? strategies : []
|
||||
},
|
||||
|
||||
async getStrategy(strategyId: string): Promise<Strategy> {
|
||||
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/${strategyId}`)
|
||||
if (!result.success) throw new Error('获取策略失败')
|
||||
if (!result.success) throw new Error('Failed to fetch strategy')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getActiveStrategy(): Promise<Strategy> {
|
||||
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/active`)
|
||||
if (!result.success) throw new Error('获取激活策略失败')
|
||||
if (!result.success) throw new Error('Failed to fetch active strategy')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getDefaultStrategyConfig(): Promise<StrategyConfig> {
|
||||
const result = await httpClient.get<StrategyConfig>(`${API_BASE}/strategies/default-config`)
|
||||
if (!result.success) throw new Error('获取默认策略配置失败')
|
||||
if (!result.success) throw new Error('Failed to fetch default strategy config')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ export const strategyApi = {
|
||||
config: StrategyConfig
|
||||
}): Promise<Strategy> {
|
||||
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies`, data)
|
||||
if (!result.success) throw new Error('创建策略失败')
|
||||
if (!result.success) throw new Error('Failed to create strategy')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -49,24 +49,24 @@ export const strategyApi = {
|
||||
}
|
||||
): Promise<Strategy> {
|
||||
const result = await httpClient.put<Strategy>(`${API_BASE}/strategies/${strategyId}`, data)
|
||||
if (!result.success) throw new Error('更新策略失败')
|
||||
if (!result.success) throw new Error('Failed to update strategy')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async deleteStrategy(strategyId: string): Promise<void> {
|
||||
const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`)
|
||||
if (!result.success) throw new Error('删除策略失败')
|
||||
if (!result.success) throw new Error('Failed to delete strategy')
|
||||
},
|
||||
|
||||
async activateStrategy(strategyId: string): Promise<Strategy> {
|
||||
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/activate`)
|
||||
if (!result.success) throw new Error('激活策略失败')
|
||||
if (!result.success) throw new Error('Failed to activate strategy')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async duplicateStrategy(strategyId: string): Promise<Strategy> {
|
||||
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/duplicate`)
|
||||
if (!result.success) throw new Error('复制策略失败')
|
||||
if (!result.success) throw new Error('Failed to duplicate strategy')
|
||||
return result.data!
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@ import { API_BASE, httpClient } from './helpers'
|
||||
export const telegramApi = {
|
||||
async getTelegramConfig(): Promise<TelegramConfig> {
|
||||
const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`)
|
||||
if (!result.success) throw new Error('获取Telegram配置失败')
|
||||
if (!result.success) throw new Error('Failed to fetch Telegram config')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
|
||||
const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' })
|
||||
if (!result.success) throw new Error('保存Telegram配置失败')
|
||||
if (!result.success) throw new Error('Failed to save Telegram config')
|
||||
},
|
||||
|
||||
async unbindTelegram(): Promise<void> {
|
||||
const result = await httpClient.delete(`${API_BASE}/telegram/binding`)
|
||||
if (!result.success) throw new Error('解绑Telegram失败')
|
||||
if (!result.success) throw new Error('Failed to unbind Telegram')
|
||||
},
|
||||
|
||||
async updateTelegramModel(modelId: string): Promise<void> {
|
||||
const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId })
|
||||
if (!result.success) throw new Error('更新Telegram模型失败')
|
||||
if (!result.success) throw new Error('Failed to update Telegram model')
|
||||
},
|
||||
}
|
||||
|
||||
+11
-11
@@ -8,13 +8,13 @@ import { API_BASE, httpClient } from './helpers'
|
||||
export const traderApi = {
|
||||
async getTraders(): Promise<TraderInfo[]> {
|
||||
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
|
||||
if (!result.success) throw new Error('获取trader列表失败')
|
||||
if (!result.success) throw new Error('Failed to fetch trader list')
|
||||
return Array.isArray(result.data) ? result.data : []
|
||||
},
|
||||
|
||||
async getPublicTraders(): Promise<any[]> {
|
||||
const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
|
||||
if (!result.success) throw new Error('获取公开trader列表失败')
|
||||
if (!result.success) throw new Error('Failed to fetch public trader list')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -23,25 +23,25 @@ export const traderApi = {
|
||||
`${API_BASE}/traders`,
|
||||
request
|
||||
)
|
||||
if (!result.success) throw new Error('创建交易员失败')
|
||||
if (!result.success) throw new Error('Failed to create trader')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async deleteTrader(traderId: string): Promise<void> {
|
||||
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
|
||||
if (!result.success) throw new Error('删除交易员失败')
|
||||
if (!result.success) throw new Error('Failed to delete trader')
|
||||
},
|
||||
|
||||
async startTrader(traderId: string): Promise<void> {
|
||||
const result = await httpClient.post(
|
||||
`${API_BASE}/traders/${traderId}/start`
|
||||
)
|
||||
if (!result.success) throw new Error('启动交易员失败')
|
||||
if (!result.success) throw new Error('Failed to start trader')
|
||||
},
|
||||
|
||||
async stopTrader(traderId: string): Promise<void> {
|
||||
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
|
||||
if (!result.success) throw new Error('停止交易员失败')
|
||||
if (!result.success) throw new Error('Failed to stop trader')
|
||||
},
|
||||
|
||||
async toggleCompetition(traderId: string, showInCompetition: boolean): Promise<void> {
|
||||
@@ -49,7 +49,7 @@ export const traderApi = {
|
||||
`${API_BASE}/traders/${traderId}/competition`,
|
||||
{ show_in_competition: showInCompetition }
|
||||
)
|
||||
if (!result.success) throw new Error('更新竞技场显示设置失败')
|
||||
if (!result.success) throw new Error('Failed to update competition visibility')
|
||||
},
|
||||
|
||||
async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> {
|
||||
@@ -57,7 +57,7 @@ export const traderApi = {
|
||||
`${API_BASE}/traders/${traderId}/close-position`,
|
||||
{ symbol, side }
|
||||
)
|
||||
if (!result.success) throw new Error('平仓失败')
|
||||
if (!result.success) throw new Error('Failed to close position')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -69,14 +69,14 @@ export const traderApi = {
|
||||
`${API_BASE}/traders/${traderId}/prompt`,
|
||||
{ custom_prompt: customPrompt }
|
||||
)
|
||||
if (!result.success) throw new Error('更新自定义策略失败')
|
||||
if (!result.success) throw new Error('Failed to update custom prompt')
|
||||
},
|
||||
|
||||
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
|
||||
const result = await httpClient.get<TraderConfigData>(
|
||||
`${API_BASE}/traders/${traderId}/config`
|
||||
)
|
||||
if (!result.success) throw new Error('获取交易员配置失败')
|
||||
if (!result.success) throw new Error('Failed to fetch trader config')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
@@ -88,7 +88,7 @@ export const traderApi = {
|
||||
`${API_BASE}/traders/${traderId}`,
|
||||
request
|
||||
)
|
||||
if (!result.success) throw new Error('更新交易员失败')
|
||||
if (!result.success) throw new Error('Failed to update trader')
|
||||
return result.data!
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { notify } from './notify'
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板,并显示轻量提示。
|
||||
* Copy text to clipboard and show a toast notification.
|
||||
*/
|
||||
export async function copyWithToast(text: string, successMsg = '已复制') {
|
||||
export async function copyWithToast(text: string, successMsg = 'Copied') {
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
// 兼容降级:创建临时文本域执行复制
|
||||
// Fallback: create temporary textarea for copy
|
||||
const el = document.createElement('textarea')
|
||||
el.value = text
|
||||
el.style.position = 'fixed'
|
||||
@@ -22,7 +22,7 @@ export async function copyWithToast(text: string, successMsg = '已复制') {
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Clipboard copy failed:', err)
|
||||
notify.error('复制失败')
|
||||
notify.error('Copy failed')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
export function DataPage() {
|
||||
const { language } = useLanguage()
|
||||
@@ -7,7 +8,7 @@ export function DataPage() {
|
||||
<div className="w-full h-[calc(100vh-64px)]">
|
||||
<iframe
|
||||
src="https://nofxos.ai/dashboard"
|
||||
title={language === 'zh' ? '数据中心' : 'Data Center'}
|
||||
title={t('dataCenter', language)}
|
||||
className="w-full h-full border-0"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { toast } from 'sonner'
|
||||
import { t } from '../i18n/translations'
|
||||
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
||||
|
||||
interface PublicStrategy {
|
||||
@@ -106,88 +107,7 @@ export function StrategyMarketPage() {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
|
||||
const texts = {
|
||||
zh: {
|
||||
title: '策略市场',
|
||||
subtitle: 'STRATEGY MARKETPLACE',
|
||||
description: '发现、学习并复用社区精英交易员的策略配置',
|
||||
search: '搜索参数...',
|
||||
all: '全部协议',
|
||||
popular: '热门配置',
|
||||
recent: '最新提交',
|
||||
myStrategies: '我的库',
|
||||
noStrategies: '无信号',
|
||||
noStrategiesDesc: '当前频段未检测到策略信号',
|
||||
author: 'OPERATOR',
|
||||
createdAt: 'TIMESTAMP',
|
||||
viewConfig: 'DECRYPT CONFIG',
|
||||
hideConfig: 'ENCRYPT',
|
||||
copyConfig: 'CLONE CONFIG',
|
||||
copied: 'COPIED',
|
||||
configHidden: 'ENCRYPTED',
|
||||
configHiddenDesc: '配置参数已加密',
|
||||
indicators: 'INDICATORS',
|
||||
maxPositions: 'POS_LIMIT',
|
||||
maxLeverage: 'LEV_MAX',
|
||||
shareYours: 'UPLOAD_STRATEGY',
|
||||
makePublic: 'PUBLISH',
|
||||
loading: 'INITIALIZING...'
|
||||
},
|
||||
en: {
|
||||
title: 'STRATEGY MARKET',
|
||||
subtitle: 'GLOBAL STRATEGY DATABASE',
|
||||
description: 'Discover, analyze, and clone high-performance trading algorithms',
|
||||
search: 'SEARCH PARAMETERS...',
|
||||
all: 'ALL PROTOCOLS',
|
||||
popular: 'TRENDING',
|
||||
recent: 'LATEST',
|
||||
myStrategies: 'MY LIBRARY',
|
||||
noStrategies: 'NO SIGNAL',
|
||||
noStrategiesDesc: 'No strategic signals detected in this frequency',
|
||||
author: 'OPERATOR',
|
||||
createdAt: 'TIMESTAMP',
|
||||
viewConfig: 'DECRYPT CONFIG',
|
||||
hideConfig: 'ENCRYPT',
|
||||
copyConfig: 'CLONE CONFIG',
|
||||
copied: 'COPIED',
|
||||
configHidden: 'ENCRYPTED',
|
||||
configHiddenDesc: 'Configuration parameters encrypted',
|
||||
indicators: 'INDICATORS',
|
||||
maxPositions: 'POS_LIMIT',
|
||||
maxLeverage: 'LEV_MAX',
|
||||
shareYours: 'UPLOAD_STRATEGY',
|
||||
makePublic: 'PUBLISH',
|
||||
loading: 'INITIALIZING...'
|
||||
},
|
||||
id: {
|
||||
title: 'PASAR STRATEGI',
|
||||
subtitle: 'DATABASE STRATEGI GLOBAL',
|
||||
description: 'Temukan, analisis, dan kloning algoritma trading berperforma tinggi',
|
||||
search: 'CARI PARAMETER...',
|
||||
all: 'SEMUA PROTOKOL',
|
||||
popular: 'TREN',
|
||||
recent: 'TERBARU',
|
||||
myStrategies: 'PERPUSTAKAAN SAYA',
|
||||
noStrategies: 'TIDAK ADA SINYAL',
|
||||
noStrategiesDesc: 'Tidak ada sinyal strategis terdeteksi pada frekuensi ini',
|
||||
author: 'OPERATOR',
|
||||
createdAt: 'TIMESTAMP',
|
||||
viewConfig: 'DEKRIPSI CONFIG',
|
||||
hideConfig: 'ENKRIPSI',
|
||||
copyConfig: 'KLON CONFIG',
|
||||
copied: 'DISALIN',
|
||||
configHidden: 'TERENKRIPSI',
|
||||
configHiddenDesc: 'Parameter konfigurasi terenkripsi',
|
||||
indicators: 'INDIKATOR',
|
||||
maxPositions: 'BATAS_POS',
|
||||
maxLeverage: 'LEV_MAKS',
|
||||
shareYours: 'UNGGAH_STRATEGI',
|
||||
makePublic: 'PUBLIKASI',
|
||||
loading: 'MENGINISIALISASI...'
|
||||
}
|
||||
}
|
||||
|
||||
const t = texts[language]
|
||||
const tr = (key: string) => t(`strategyMarket.${key}`, language)
|
||||
|
||||
// Fetch public strategies
|
||||
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
|
||||
@@ -218,7 +138,7 @@ export function StrategyMarketPage() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
|
||||
setCopiedId(strategy.id)
|
||||
toast.success(t.copied)
|
||||
toast.success(tr('copied'))
|
||||
setTimeout(() => setCopiedId(null), 2000)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
@@ -271,16 +191,16 @@ export function StrategyMarketPage() {
|
||||
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
|
||||
{t.title}
|
||||
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={tr('title')}>
|
||||
{tr('title')}
|
||||
</h1>
|
||||
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
|
||||
// {t.subtitle}
|
||||
// {tr('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
|
||||
{t.description}
|
||||
{tr('description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -295,7 +215,7 @@ export function StrategyMarketPage() {
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.search}
|
||||
placeholder={tr('search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
|
||||
@@ -324,7 +244,7 @@ export function StrategyMarketPage() {
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
|
||||
<span className="relative z-10">{tr(cat)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -340,7 +260,7 @@ export function StrategyMarketPage() {
|
||||
<Cpu size={24} className="text-nofx-gold/50" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p>
|
||||
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{tr('loading')}</p>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
|
||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||
@@ -357,9 +277,9 @@ export function StrategyMarketPage() {
|
||||
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
|
||||
[{t.noStrategies}]
|
||||
[{tr('noStrategies')}]
|
||||
</h3>
|
||||
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
|
||||
<p className="text-zinc-600 text-xs tracking-wide uppercase">{tr('noStrategiesDesc')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -423,11 +343,11 @@ export function StrategyMarketPage() {
|
||||
{/* Meta Data */}
|
||||
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-zinc-700 uppercase">{t.author}</span>
|
||||
<span className="text-zinc-700 uppercase">{tr('author')}</span>
|
||||
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col text-right">
|
||||
<span className="text-zinc-700 uppercase">{t.createdAt}</span>
|
||||
<span className="text-zinc-700 uppercase">{tr('createdAt')}</span>
|
||||
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -468,7 +388,7 @@ export function StrategyMarketPage() {
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
|
||||
<EyeOff size={16} className="mb-1 opacity-50" />
|
||||
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span>
|
||||
<span className="text-[9px] uppercase tracking-widest">{tr('configHiddenDesc')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -483,19 +403,19 @@ export function StrategyMarketPage() {
|
||||
{copiedId === strategy.id ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 text-emerald-500" />
|
||||
<span className="text-emerald-500">{t.copied}</span>
|
||||
<span className="text-emerald-500">{tr('copied')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
|
||||
{t.copyConfig}
|
||||
{tr('copyConfig')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
|
||||
<Shield size={12} />
|
||||
{t.hideConfig}
|
||||
{tr('hideConfig')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -521,7 +441,7 @@ export function StrategyMarketPage() {
|
||||
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
|
||||
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.shareYours}</div>
|
||||
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{tr('shareYours')}</div>
|
||||
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
|
||||
</div>
|
||||
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
|
||||
|
||||
@@ -39,6 +39,7 @@ import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEdito
|
||||
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
||||
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
|
||||
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||
|
||||
@@ -108,7 +109,7 @@ export function StrategyStudioPage() {
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// 后端返回的是数组,不是 { models: [] }
|
||||
// Backend returns an array, not { models: [] }
|
||||
const allModels = Array.isArray(data) ? data : (data.models || [])
|
||||
const enabledModels = allModels.filter((m: AIModel) => m.enabled)
|
||||
setAiModels(enabledModels)
|
||||
@@ -209,7 +210,7 @@ export function StrategyStudioPage() {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: language === 'zh' ? '新策略' : 'New Strategy',
|
||||
name: tr('newStrategyName'),
|
||||
description: '',
|
||||
config: defaultConfig,
|
||||
}),
|
||||
@@ -222,7 +223,7 @@ export function StrategyStudioPage() {
|
||||
const now = new Date().toISOString()
|
||||
const newStrategy = {
|
||||
id: result.id,
|
||||
name: language === 'zh' ? '新策略' : 'New Strategy',
|
||||
name: tr('newStrategyName'),
|
||||
description: '',
|
||||
is_active: false,
|
||||
is_default: false,
|
||||
@@ -246,11 +247,11 @@ export function StrategyStudioPage() {
|
||||
if (!token) return
|
||||
|
||||
const confirmed = await confirmToast(
|
||||
language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?',
|
||||
tr('confirmDeleteStrategy'),
|
||||
{
|
||||
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
|
||||
okText: language === 'zh' ? '删除' : 'Delete',
|
||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
||||
title: tr('confirmDelete'),
|
||||
okText: tr('delete'),
|
||||
cancelText: tr('cancel'),
|
||||
}
|
||||
)
|
||||
if (!confirmed) return
|
||||
@@ -261,7 +262,7 @@ export function StrategyStudioPage() {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to delete strategy')
|
||||
notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted')
|
||||
notify.success(tr('strategyDeleted'))
|
||||
// Clear selection if deleted strategy was selected
|
||||
if (selectedStrategy?.id === id) {
|
||||
setSelectedStrategy(null)
|
||||
@@ -287,7 +288,7 @@ export function StrategyStudioPage() {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: language === 'zh' ? '策略副本' : 'Strategy Copy',
|
||||
name: tr('strategyCopy'),
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to duplicate strategy')
|
||||
@@ -330,7 +331,7 @@ export function StrategyStudioPage() {
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported')
|
||||
notify.success(tr('strategyExported'))
|
||||
}
|
||||
|
||||
// Import strategy from JSON file
|
||||
@@ -344,7 +345,7 @@ export function StrategyStudioPage() {
|
||||
|
||||
// Validate imported data
|
||||
if (!importData.config || !importData.name) {
|
||||
throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file')
|
||||
throw new Error(tr('invalidStrategyFile'))
|
||||
}
|
||||
|
||||
// Create new strategy with imported config
|
||||
@@ -355,14 +356,14 @@ export function StrategyStudioPage() {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`,
|
||||
name: `${importData.name} (${tr('imported')})`,
|
||||
description: importData.description || '',
|
||||
config: importData.config,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to import strategy')
|
||||
|
||||
notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported')
|
||||
notify.success(tr('strategyImported'))
|
||||
await fetchStrategies()
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
|
||||
@@ -402,7 +403,7 @@ export function StrategyStudioPage() {
|
||||
)
|
||||
if (!response.ok) throw new Error('Failed to save strategy')
|
||||
setHasChanges(false)
|
||||
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved')
|
||||
notify.success(tr('strategySaved'))
|
||||
await fetchStrategies()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
@@ -482,51 +483,7 @@ export function StrategyStudioPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
strategyStudio: { zh: '策略工作室', en: 'Strategy Studio' },
|
||||
subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' },
|
||||
strategies: { zh: '策略', en: 'Strategies' },
|
||||
newStrategy: { zh: '新建', en: 'New' },
|
||||
strategyType: { zh: '策略类型', en: 'Strategy Type' },
|
||||
aiTrading: { zh: 'AI 智能交易', en: 'AI Trading' },
|
||||
aiTradingDesc: { zh: 'AI 分析市场并自主决策买卖', en: 'AI analyzes market and makes trading decisions' },
|
||||
gridTrading: { zh: 'AI 网格交易', en: 'AI Grid Trading' },
|
||||
gridTradingDesc: { zh: 'AI 控制网格策略,在震荡市场获利', en: 'AI-controlled grid strategy for ranging markets' },
|
||||
gridConfig: { zh: '网格配置', en: 'Grid Configuration' },
|
||||
coinSource: { zh: '币种来源', en: 'Coin Source' },
|
||||
indicators: { zh: '技术指标', en: 'Indicators' },
|
||||
riskControl: { zh: '风控参数', en: 'Risk Control' },
|
||||
promptSections: { zh: 'Prompt 编辑', en: 'Prompt Editor' },
|
||||
customPrompt: { zh: '附加提示', en: 'Extra Prompt' },
|
||||
save: { zh: '保存', en: 'Save' },
|
||||
saving: { zh: '保存中...', en: 'Saving...' },
|
||||
activate: { zh: '激活', en: 'Activate' },
|
||||
active: { zh: '激活中', en: 'Active' },
|
||||
default: { zh: '默认', en: 'Default' },
|
||||
promptPreview: { zh: 'Prompt 预览', en: 'Prompt Preview' },
|
||||
aiTestRun: { zh: 'AI 测试', en: 'AI Test' },
|
||||
systemPrompt: { zh: 'System Prompt', en: 'System Prompt' },
|
||||
userPrompt: { zh: 'User Prompt', en: 'User Prompt' },
|
||||
loadPrompt: { zh: '生成 Prompt', en: 'Generate Prompt' },
|
||||
refreshPrompt: { zh: '刷新', en: 'Refresh' },
|
||||
promptVariant: { zh: '风格', en: 'Style' },
|
||||
balanced: { zh: '平衡', en: 'Balanced' },
|
||||
aggressive: { zh: '激进', en: 'Aggressive' },
|
||||
conservative: { zh: '保守', en: 'Conservative' },
|
||||
selectModel: { zh: '选择 AI 模型', en: 'Select AI Model' },
|
||||
runTest: { zh: '运行 AI 测试', en: 'Run AI Test' },
|
||||
running: { zh: '运行中...', en: 'Running...' },
|
||||
aiOutput: { zh: 'AI 输出', en: 'AI Output' },
|
||||
reasoning: { zh: '思维链', en: 'Reasoning' },
|
||||
decisions: { zh: '决策', en: 'Decisions' },
|
||||
duration: { zh: '耗时', en: 'Duration' },
|
||||
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
|
||||
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
|
||||
publishSettings: { zh: '发布设置', en: 'Publish' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
const tr = (key: string) => t(`strategyStudio.${key}`, language)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -550,7 +507,7 @@ export function StrategyStudioPage() {
|
||||
key: 'gridConfig' as const,
|
||||
icon: Activity,
|
||||
color: '#0ECB81',
|
||||
title: t('gridConfig'),
|
||||
title: tr('gridConfig'),
|
||||
forStrategyType: 'grid_trading' as const,
|
||||
content: editingConfig?.grid_config && (
|
||||
<GridConfigEditor
|
||||
@@ -566,7 +523,7 @@ export function StrategyStudioPage() {
|
||||
key: 'coinSource' as const,
|
||||
icon: Target,
|
||||
color: '#F0B90B',
|
||||
title: t('coinSource'),
|
||||
title: tr('coinSource'),
|
||||
forStrategyType: 'ai_trading' as const,
|
||||
content: editingConfig && (
|
||||
<CoinSourceEditor
|
||||
@@ -581,7 +538,7 @@ export function StrategyStudioPage() {
|
||||
key: 'indicators' as const,
|
||||
icon: BarChart3,
|
||||
color: '#0ECB81',
|
||||
title: t('indicators'),
|
||||
title: tr('indicators'),
|
||||
forStrategyType: 'ai_trading' as const,
|
||||
content: editingConfig && (
|
||||
<IndicatorEditor
|
||||
@@ -596,7 +553,7 @@ export function StrategyStudioPage() {
|
||||
key: 'riskControl' as const,
|
||||
icon: Shield,
|
||||
color: '#F6465D',
|
||||
title: t('riskControl'),
|
||||
title: tr('riskControl'),
|
||||
forStrategyType: 'ai_trading' as const,
|
||||
content: editingConfig && (
|
||||
<RiskControlEditor
|
||||
@@ -611,7 +568,7 @@ export function StrategyStudioPage() {
|
||||
key: 'promptSections' as const,
|
||||
icon: FileText,
|
||||
color: '#a855f7',
|
||||
title: t('promptSections'),
|
||||
title: tr('promptSections'),
|
||||
forStrategyType: 'ai_trading' as const,
|
||||
content: editingConfig && (
|
||||
<PromptSectionsEditor
|
||||
@@ -626,18 +583,18 @@ export function StrategyStudioPage() {
|
||||
key: 'customPrompt' as const,
|
||||
icon: Settings,
|
||||
color: '#60a5fa',
|
||||
title: t('customPrompt'),
|
||||
title: tr('customPrompt'),
|
||||
forStrategyType: 'ai_trading' as const,
|
||||
content: editingConfig && (
|
||||
<div>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '附加在 System Prompt 末尾的额外提示,用于补充个性化交易风格' : 'Extra prompt appended to System Prompt for personalized trading style'}
|
||||
{tr('customPromptDesc')}
|
||||
</p>
|
||||
<textarea
|
||||
value={editingConfig.custom_prompt || ''}
|
||||
onChange={(e) => updateConfig('custom_prompt', e.target.value)}
|
||||
disabled={selectedStrategy?.is_default}
|
||||
placeholder={language === 'zh' ? '输入自定义提示词...' : 'Enter custom prompt...'}
|
||||
placeholder={tr('customPromptPlaceholder')}
|
||||
className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
@@ -648,7 +605,7 @@ export function StrategyStudioPage() {
|
||||
key: 'publishSettings' as const,
|
||||
icon: Globe,
|
||||
color: '#0ECB81',
|
||||
title: t('publishSettings'),
|
||||
title: tr('publishSettings'),
|
||||
forStrategyType: 'both' as const,
|
||||
content: selectedStrategy && (
|
||||
<PublishSettingsEditor
|
||||
@@ -683,8 +640,8 @@ export function StrategyStudioPage() {
|
||||
<Sparkles className="w-5 h-5 text-black" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
|
||||
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
|
||||
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
|
||||
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
@@ -702,10 +659,10 @@ export function StrategyStudioPage() {
|
||||
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
|
||||
<div className="p-2">
|
||||
<div className="flex items-center justify-between mb-2 px-2">
|
||||
<span className="text-xs font-medium text-nofx-text-muted">{t('strategies')}</span>
|
||||
<span className="text-xs font-medium text-nofx-text-muted">{tr('strategies')}</span>
|
||||
<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 text-nofx-text-muted hover:text-white" title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
|
||||
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={tr('importStrategy')}>
|
||||
<Upload className="w-4 h-4" />
|
||||
<input
|
||||
type="file"
|
||||
@@ -717,7 +674,7 @@ export function StrategyStudioPage() {
|
||||
<button
|
||||
onClick={handleCreateStrategy}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
|
||||
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
||||
title={tr('newStrategyTooltip')}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -745,7 +702,7 @@ export function StrategyStudioPage() {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
||||
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
||||
title={language === 'zh' ? '导出' : 'Export'}
|
||||
title={tr('export')}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -754,14 +711,14 @@ export function StrategyStudioPage() {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
||||
title={language === 'zh' ? '复制' : 'Duplicate'}
|
||||
title={tr('duplicate')}
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
|
||||
title={language === 'zh' ? '删除' : 'Delete'}
|
||||
title={tr('deleteTooltip')}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -772,18 +729,18 @@ export function StrategyStudioPage() {
|
||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||
{strategy.is_active && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
|
||||
{t('active')}
|
||||
{tr('active')}
|
||||
</span>
|
||||
)}
|
||||
{strategy.is_default && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
|
||||
{t('default')}
|
||||
{tr('default')}
|
||||
</span>
|
||||
)}
|
||||
{strategy.is_public && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
|
||||
<Globe className="w-2.5 h-2.5" />
|
||||
{language === 'zh' ? '公开' : 'Public'}
|
||||
{tr('public')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -818,11 +775,11 @@ export function StrategyStudioPage() {
|
||||
setHasChanges(true)
|
||||
}}
|
||||
disabled={selectedStrategy.is_default}
|
||||
placeholder={language === 'zh' ? '添加策略简介...' : 'Add strategy description...'}
|
||||
placeholder={tr('addDescription')}
|
||||
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
|
||||
/>
|
||||
{hasChanges && (
|
||||
<span className="text-xs text-nofx-gold">● {language === 'zh' ? '未保存' : 'Unsaved'}</span>
|
||||
<span className="text-xs text-nofx-gold">● {tr('unsaved')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
@@ -832,7 +789,7 @@ export function StrategyStudioPage() {
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
{t('activate')}
|
||||
{tr('activate')}
|
||||
</button>
|
||||
)}
|
||||
{!selectedStrategy.is_default && (
|
||||
@@ -843,7 +800,7 @@ export function StrategyStudioPage() {
|
||||
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
|
||||
>
|
||||
<Save className="w-3 h-3" />
|
||||
{isSaving ? t('saving') : t('save')}
|
||||
{isSaving ? tr('saving') : tr('save')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -854,7 +811,7 @@ export function StrategyStudioPage() {
|
||||
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium text-nofx-text">{t('strategyType')}</span>
|
||||
<span className="text-sm font-medium text-nofx-text">{tr('strategyType')}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
@@ -874,9 +831,9 @@ export function StrategyStudioPage() {
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Bot className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium text-nofx-text">{t('aiTrading')}</span>
|
||||
<span className="text-sm font-medium text-nofx-text">{tr('aiTrading')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-nofx-text-muted text-left">{t('aiTradingDesc')}</p>
|
||||
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -897,9 +854,9 @@ export function StrategyStudioPage() {
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
<span className="text-sm font-medium text-nofx-text">{t('gridTrading')}</span>
|
||||
<span className="text-sm font-medium text-nofx-text">{tr('gridTrading')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-nofx-text-muted text-left">{t('gridTradingDesc')}</p>
|
||||
<p className="text-xs text-nofx-text-muted text-left">{tr('gridTradingDesc')}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -940,7 +897,7 @@ export function StrategyStudioPage() {
|
||||
<div className="text-center">
|
||||
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
|
||||
<p className="text-sm text-nofx-text-muted">
|
||||
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
|
||||
{tr('selectOrCreate')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -957,7 +914,7 @@ export function StrategyStudioPage() {
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{t('promptPreview')}
|
||||
{tr('promptPreview')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveRightTab('test')}
|
||||
@@ -965,7 +922,7 @@ export function StrategyStudioPage() {
|
||||
}`}
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
{t('aiTestRun')}
|
||||
{tr('aiTestRun')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -981,9 +938,9 @@ export function StrategyStudioPage() {
|
||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
|
||||
>
|
||||
<option value="balanced">{t('balanced')}</option>
|
||||
<option value="aggressive">{t('aggressive')}</option>
|
||||
<option value="conservative">{t('conservative')}</option>
|
||||
<option value="balanced">{tr('balanced')}</option>
|
||||
<option value="aggressive">{tr('aggressive')}</option>
|
||||
<option value="conservative">{tr('conservative')}</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={fetchPromptPreview}
|
||||
@@ -991,7 +948,7 @@ export function StrategyStudioPage() {
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
||||
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
|
||||
{promptPreview ? tr('refreshPrompt') : tr('loadPrompt')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1018,7 +975,7 @@ export function StrategyStudioPage() {
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FileText className="w-3 h-3 text-purple-500" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span>
|
||||
<span className="text-xs font-medium text-nofx-text">{tr('systemPrompt')}</span>
|
||||
</div>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
|
||||
{promptPreview.system_prompt.length.toLocaleString()} chars
|
||||
@@ -1035,7 +992,7 @@ export function StrategyStudioPage() {
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
||||
<Eye className="w-10 h-10 mb-2 opacity-30" />
|
||||
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
|
||||
<p className="text-sm">{tr('generatePromptPreview')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1046,7 +1003,7 @@ export function StrategyStudioPage() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="w-4 h-4 text-green-500" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span>
|
||||
<span className="text-xs font-medium text-nofx-text">{tr('selectModel')}</span>
|
||||
</div>
|
||||
{aiModels.length > 0 ? (
|
||||
<select
|
||||
@@ -1062,7 +1019,7 @@ export function StrategyStudioPage() {
|
||||
</select>
|
||||
) : (
|
||||
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
|
||||
{t('noModel')}
|
||||
{tr('noModel')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1072,9 +1029,9 @@ export function StrategyStudioPage() {
|
||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
<option value="balanced">{t('balanced')}</option>
|
||||
<option value="aggressive">{t('aggressive')}</option>
|
||||
<option value="conservative">{t('conservative')}</option>
|
||||
<option value="balanced">{tr('balanced')}</option>
|
||||
<option value="aggressive">{tr('aggressive')}</option>
|
||||
<option value="conservative">{tr('conservative')}</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={runAiTest}
|
||||
@@ -1084,17 +1041,17 @@ export function StrategyStudioPage() {
|
||||
{isRunningAiTest ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{t('running')}
|
||||
{tr('running')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4" />
|
||||
{t('runTest')}
|
||||
{tr('runTest')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
|
||||
<p className="text-[10px] text-nofx-text-muted">{tr('testNote')}</p>
|
||||
</div>
|
||||
|
||||
{/* Test Results */}
|
||||
@@ -1110,7 +1067,7 @@ export function StrategyStudioPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-3 h-3 text-nofx-text-muted" />
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
||||
{tr('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1120,7 +1077,7 @@ export function StrategyStudioPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Terminal className="w-3 h-3 text-blue-400" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span>
|
||||
<span className="text-xs font-medium text-nofx-text">{tr('userPrompt')} (Input)</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
@@ -1136,7 +1093,7 @@ export function StrategyStudioPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Sparkles className="w-3 h-3 text-nofx-gold" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span>
|
||||
<span className="text-xs font-medium text-nofx-text">{tr('reasoning')}</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
|
||||
@@ -1152,7 +1109,7 @@ export function StrategyStudioPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Activity className="w-3 h-3 text-green-500" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span>
|
||||
<span className="text-xs font-medium text-nofx-text">{tr('decisions')}</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
|
||||
@@ -1168,7 +1125,7 @@ export function StrategyStudioPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<FileText className="w-3 h-3 text-nofx-text-muted" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span>
|
||||
<span className="text-xs font-medium text-nofx-text">{tr('aiOutput')} (Raw)</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
@@ -1184,7 +1141,7 @@ export function StrategyStudioPage() {
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
||||
<Play className="w-10 h-10 mb-2 opacity-30" />
|
||||
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
|
||||
<p className="text-sm">{tr('runAiTestHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
// 获取友好的AI模型名称
|
||||
// Get friendly AI model display name
|
||||
function getModelDisplayName(modelId: string): string {
|
||||
switch (modelId.toLowerCase()) {
|
||||
case 'deepseek':
|
||||
@@ -189,19 +189,17 @@ export function TraderDashboardPage({
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 平仓操作
|
||||
// Close position handler
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
if (!selectedTraderId) return
|
||||
|
||||
const confirmMsg =
|
||||
language === 'zh'
|
||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
||||
const sideLabel = side === 'LONG' ? 'LONG' : 'SHORT'
|
||||
const confirmMsg = t('traderDashboard.confirmClosePosition', language, { symbol, side: sideLabel })
|
||||
|
||||
const confirmed = await confirmToast(confirmMsg, {
|
||||
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
|
||||
okText: language === 'zh' ? '确认' : 'Confirm',
|
||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
||||
title: t('traderDashboard.confirmClose', language),
|
||||
okText: t('traderDashboard.confirm', language),
|
||||
cancelText: t('traderDashboard.cancel', language),
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
@@ -209,10 +207,8 @@ export function TraderDashboardPage({
|
||||
setClosingPosition(symbol)
|
||||
try {
|
||||
await api.closePosition(selectedTraderId, symbol, side)
|
||||
notify.success(
|
||||
language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
||||
)
|
||||
// 使用 SWR mutate 刷新数据而非重新加载页面
|
||||
notify.success(t('traderDashboard.positionClosed', language))
|
||||
// Use SWR mutate to refresh data instead of reloading page
|
||||
await Promise.all([
|
||||
mutate(`positions-${selectedTraderId}`),
|
||||
mutate(`account-${selectedTraderId}`),
|
||||
@@ -221,9 +217,7 @@ export function TraderDashboardPage({
|
||||
const errorMsg =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: language === 'zh'
|
||||
? '平仓失败'
|
||||
: 'Failed to close position'
|
||||
: t('traderDashboard.closeFailed', language)
|
||||
notify.error(errorMsg)
|
||||
} finally {
|
||||
setClosingPosition(null)
|
||||
@@ -257,18 +251,16 @@ export function TraderDashboardPage({
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
|
||||
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}
|
||||
{t('traderDashboard.connectionFailed', language)}
|
||||
</h2>
|
||||
<p className="text-base mb-6 text-nofx-text-muted">
|
||||
{language === 'zh'
|
||||
? '请确认后端服务已启动。'
|
||||
: 'Please check if the backend service is running.'}
|
||||
{t('traderDashboard.connectionFailedDesc', language)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
|
||||
>
|
||||
{language === 'zh' ? '重试' : 'Retry'}
|
||||
{t('traderDashboard.retry', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,12 +406,8 @@ export function TraderDashboardPage({
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
title={
|
||||
showWalletAddress
|
||||
? language === 'zh'
|
||||
? '隐藏地址'
|
||||
: 'Hide address'
|
||||
: language === 'zh'
|
||||
? '显示完整地址'
|
||||
: 'Show full address'
|
||||
? t('traderDashboard.hideAddress', language)
|
||||
: t('traderDashboard.showFullAddress', language)
|
||||
}
|
||||
>
|
||||
{showWalletAddress ? (
|
||||
@@ -432,7 +420,7 @@ export function TraderDashboardPage({
|
||||
type="button"
|
||||
onClick={handleCopyAddress}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
title={language === 'zh' ? '复制地址' : 'Copy address'}
|
||||
title={t('traderDashboard.copyAddress', language)}
|
||||
>
|
||||
{copiedAddress ? (
|
||||
<Check className="w-3.5 h-3.5 text-nofx-green" />
|
||||
@@ -443,7 +431,7 @@ export function TraderDashboardPage({
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{language === 'zh' ? '未配置地址' : 'No address configured'}
|
||||
{t('traderDashboard.noAddressConfigured', language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -599,14 +587,14 @@ export function TraderDashboardPage({
|
||||
<tr>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{language === 'zh' ? '操作' : 'Action'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{language === 'zh' ? '入场价' : 'Entry'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{language === 'zh' ? '数量' : 'Qty'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{language === 'zh' ? '价值' : 'Value'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{language === 'zh' ? '杠杆' : 'Lev.'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{language === 'zh' ? '未实现盈亏' : 'uPnL'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{language === 'zh' ? '强平价' : 'Liq.'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('traderDashboard.action', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{t('traderDashboard.entry', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{t('traderDashboard.mark', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{t('traderDashboard.qty', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{t('traderDashboard.value', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{t('traderDashboard.lev', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{t('traderDashboard.uPnL', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{t('traderDashboard.liq', language)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -644,14 +632,14 @@ export function TraderDashboardPage({
|
||||
}}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
title={t('traderDashboard.closePosition', language)}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
{t('traderDashboard.close', language)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
|
||||
@@ -678,13 +666,11 @@ export function TraderDashboardPage({
|
||||
{totalPositions > 10 && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
|
||||
<span>
|
||||
{language === 'zh'
|
||||
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
|
||||
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
|
||||
{t('traderDashboard.showingPositions', language, { shown: paginatedPositions.length, total: totalPositions })}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{language === 'zh' ? '每页' : 'Per page'}:</span>
|
||||
<span>{t('traderDashboard.perPage', language)}:</span>
|
||||
<select
|
||||
value={positionsPageSize}
|
||||
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
|
||||
|
||||
Reference in New Issue
Block a user