mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat: redesign indicator editor with required raw klines and improved UX
Backend: - Add enable_raw_klines field to IndicatorConfig (always true, required) - Change defaults: disable EMA/MACD/RSI/ATR, keep volume/OI/funding enabled Frontend: - Completely redesign IndicatorEditor with 4 clear sections: 1. Market Data: Raw OHLCV (required, locked) + timeframe selection 2. Technical Indicators: EMA/MACD/RSI/ATR (optional, AI can calculate) 3. Market Sentiment: Volume/OI/Funding Rate 4. Quant Data: External API integration - Add helpful tips and descriptions in both Chinese and English - Improve visual hierarchy with section headers and color coding - Auto-ensure enable_raw_klines is always true
This commit is contained in:
+10
-7
@@ -75,13 +75,15 @@ type CoinSourceConfig struct {
|
||||
type IndicatorConfig struct {
|
||||
// K-line configuration
|
||||
Klines KlineConfig `json:"klines"`
|
||||
// raw kline data (OHLCV) - always enabled, required for AI analysis
|
||||
EnableRawKlines bool `json:"enable_raw_klines"`
|
||||
// technical indicator switches
|
||||
EnableEMA bool `json:"enable_ema"`
|
||||
EnableMACD bool `json:"enable_macd"`
|
||||
EnableRSI bool `json:"enable_rsi"`
|
||||
EnableATR bool `json:"enable_atr"`
|
||||
EnableVolume bool `json:"enable_volume"`
|
||||
EnableOI bool `json:"enable_oi"` // open interest
|
||||
EnableOI bool `json:"enable_oi"` // open interest
|
||||
EnableFundingRate bool `json:"enable_funding_rate"` // funding rate
|
||||
// EMA period configuration
|
||||
EMAPeriods []int `json:"ema_periods,omitempty"` // default [20, 50]
|
||||
@@ -92,8 +94,8 @@ type IndicatorConfig struct {
|
||||
// external data sources
|
||||
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
|
||||
// quantitative data sources (capital flow, position changes, price changes)
|
||||
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
|
||||
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address
|
||||
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
|
||||
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address
|
||||
}
|
||||
|
||||
// KlineConfig K-line configuration
|
||||
@@ -203,10 +205,11 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||||
EnableMultiTimeframe: true,
|
||||
SelectedTimeframes: []string{"5m", "15m", "1h", "4h"},
|
||||
},
|
||||
EnableEMA: true,
|
||||
EnableMACD: true,
|
||||
EnableRSI: true,
|
||||
EnableATR: true,
|
||||
EnableRawKlines: true, // Required - raw OHLCV data for AI analysis
|
||||
EnableEMA: false,
|
||||
EnableMACD: false,
|
||||
EnableRSI: false,
|
||||
EnableATR: false,
|
||||
EnableVolume: true,
|
||||
EnableOI: true,
|
||||
EnableFundingRate: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Clock, Activity, Database } from 'lucide-react'
|
||||
import { Clock, Activity, Database, TrendingUp, BarChart2, Info, Lock } from 'lucide-react'
|
||||
import type { IndicatorConfig } from '../../types'
|
||||
|
||||
// Default API URL for quant data (must contain {symbol} placeholder)
|
||||
@@ -37,27 +37,53 @@ export function IndicatorEditor({
|
||||
}: IndicatorEditorProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
timeframes: { zh: '时间周期', en: 'Timeframes' },
|
||||
timeframesDesc: { zh: '选择要分析的K线周期(可多选)', en: 'Select K-line timeframes to analyze (multi-select)' },
|
||||
primaryTimeframe: { zh: '主周期', en: 'Primary' },
|
||||
klineCount: { zh: 'K线数量', en: 'K-line Count' },
|
||||
// Section titles
|
||||
marketData: { zh: '市场数据', en: 'Market Data' },
|
||||
marketDataDesc: { zh: 'AI 分析所需的核心价格数据', en: 'Core price data for AI analysis' },
|
||||
technicalIndicators: { zh: '技术指标', en: 'Technical Indicators' },
|
||||
ema: { zh: 'EMA 均线', en: 'EMA' },
|
||||
macd: { zh: 'MACD', en: 'MACD' },
|
||||
rsi: { zh: 'RSI', en: 'RSI' },
|
||||
atr: { zh: 'ATR', en: 'ATR' },
|
||||
volume: { zh: '成交量', en: 'Volume' },
|
||||
oi: { zh: '持仓量', en: 'OI' },
|
||||
fundingRate: { zh: '资金费率', en: 'Funding' },
|
||||
periods: { zh: '周期', en: 'Periods' },
|
||||
scalp: { zh: '剥头皮', en: 'Scalp' },
|
||||
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: 'Third-party: 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' },
|
||||
quantData: { zh: '量化数据', en: 'Quant Data' },
|
||||
quantDataDesc: { zh: '资金流向、持仓变化、价格变化(按币种查询)', en: 'Netflow, OI delta, price change (per coin)' },
|
||||
quantDataUrl: { zh: '量化数据 API', en: 'Quant Data API' },
|
||||
|
||||
// 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' },
|
||||
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' },
|
||||
|
||||
// Quant data
|
||||
quantDataUrl: { zh: '数据接口 URL', en: 'Data API URL' },
|
||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||
symbolPlaceholder: { zh: '{symbol} 会被替换为币种', en: '{symbol} will be replaced with coin' },
|
||||
|
||||
// Tips
|
||||
aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
@@ -72,10 +98,8 @@ export function IndicatorEditor({
|
||||
const index = current.indexOf(tf)
|
||||
|
||||
if (index >= 0) {
|
||||
// 如果已选中,取消选择(但保留至少一个)
|
||||
if (current.length > 1) {
|
||||
current.splice(index, 1)
|
||||
// 如果取消的是主周期,则选第一个为主周期
|
||||
const newPrimary = tf === config.klines.primary_timeframe ? current[0] : config.klines.primary_timeframe
|
||||
onChange({
|
||||
...config,
|
||||
@@ -88,7 +112,6 @@ export function IndicatorEditor({
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 添加新的时间周期
|
||||
current.push(tf)
|
||||
onChange({
|
||||
...config,
|
||||
@@ -113,16 +136,6 @@ export function IndicatorEditor({
|
||||
})
|
||||
}
|
||||
|
||||
const indicators = [
|
||||
{ key: 'enable_ema', label: 'ema', color: '#F0B90B', periodKey: 'ema_periods' },
|
||||
{ key: 'enable_macd', label: 'macd', color: '#0ECB81' },
|
||||
{ key: 'enable_rsi', label: 'rsi', color: '#F6465D', periodKey: 'rsi_periods' },
|
||||
{ key: 'enable_atr', label: 'atr', color: '#60a5fa', periodKey: 'atr_periods' },
|
||||
{ key: 'enable_volume', label: 'volume', color: '#c084fc' },
|
||||
{ key: 'enable_oi', label: 'oi', color: '#34d399' },
|
||||
{ key: 'enable_funding_rate', label: 'fundingRate', color: '#fbbf24' },
|
||||
]
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
scalp: '#F6465D',
|
||||
intraday: '#F0B90B',
|
||||
@@ -130,112 +143,176 @@ export function IndicatorEditor({
|
||||
position: '#60a5fa',
|
||||
}
|
||||
|
||||
// Ensure enable_raw_klines is always true
|
||||
const ensureRawKlines = () => {
|
||||
if (!config.enable_raw_klines) {
|
||||
onChange({ ...config, enable_raw_klines: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Call on mount if needed
|
||||
if (config.enable_raw_klines === undefined || config.enable_raw_klines === false) {
|
||||
ensureRawKlines()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Timeframe Selection */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('timeframes')}</span>
|
||||
<div className="space-y-5">
|
||||
{/* Section 1: Market Data (Required) */}
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>{t('timeframesDesc')}</p>
|
||||
|
||||
{/* Timeframe Grid by Category */}
|
||||
<div className="space-y-2">
|
||||
{(['scalp', 'intraday', 'swing', 'position'] as const).map((category) => {
|
||||
const categoryTfs = allTimeframes.filter((tf) => tf.category === category)
|
||||
return (
|
||||
<div key={category} className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-[10px] w-14 flex-shrink-0"
|
||||
style={{ color: categoryColors[category] }}
|
||||
>
|
||||
{t(category)}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{categoryTfs.map((tf) => {
|
||||
const isSelected = selectedTimeframes.includes(tf.value)
|
||||
const isPrimary = config.klines.primary_timeframe === tf.value
|
||||
return (
|
||||
<div key={tf.value} className="relative">
|
||||
<button
|
||||
onClick={() => toggleTimeframe(tf.value)}
|
||||
onDoubleClick={() => setPrimaryTimeframe(tf.value)}
|
||||
disabled={disabled}
|
||||
className={`px-2.5 py-1 rounded text-xs font-medium transition-all ${
|
||||
isSelected ? 'ring-1' : 'opacity-50 hover:opacity-100'
|
||||
}`}
|
||||
style={{
|
||||
background: isSelected ? `${categoryColors[category]}20` : '#0B0E11',
|
||||
border: `1px solid ${isSelected ? categoryColors[category] : '#2B3139'}`,
|
||||
color: isSelected ? categoryColors[category] : '#848E9C',
|
||||
boxShadow: isPrimary ? `0 0 0 2px ${categoryColors[category]}` : undefined,
|
||||
}}
|
||||
title={isPrimary ? `${tf.label} (${t('primaryTimeframe')})` : tf.label}
|
||||
>
|
||||
{tf.label}
|
||||
{isPrimary && (
|
||||
<span className="ml-1 text-[8px]">★</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="p-3 space-y-4">
|
||||
{/* Raw Klines - Required, Always On */}
|
||||
<div className="flex items-center justify-between p-3 rounded-lg" style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ background: 'rgba(240, 185, 11, 0.15)' }}>
|
||||
<TrendingUp className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('rawKlines')}</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')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{t('rawKlinesDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
className="w-5 h-5 rounded accent-yellow-500 cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] mt-2" style={{ color: '#5E6673' }}>
|
||||
{language === 'zh' ? '★ = 主周期 (双击设置)' : '★ = Primary (double-click to set)'}
|
||||
</p>
|
||||
{/* Timeframe Selection */}
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>{t('klineCount')}:</span>
|
||||
<input
|
||||
type="number"
|
||||
value={config.klines.primary_count}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({
|
||||
...config,
|
||||
klines: { ...config.klines, primary_count: parseInt(e.target.value) || 30 },
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
min={10}
|
||||
max={200}
|
||||
className="w-16 px-2 py-1 rounded text-xs text-center"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{t('timeframesDesc')}</p>
|
||||
|
||||
{/* K-line Count */}
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>{t('klineCount')}:</span>
|
||||
<input
|
||||
type="number"
|
||||
value={config.klines.primary_count}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({
|
||||
...config,
|
||||
klines: { ...config.klines, primary_count: parseInt(e.target.value) || 30 },
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
min={10}
|
||||
max={200}
|
||||
className="w-20 px-2 py-1 rounded text-xs"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
{/* Timeframe Grid */}
|
||||
<div className="space-y-1.5">
|
||||
{(['scalp', 'intraday', 'swing', 'position'] as const).map((category) => {
|
||||
const categoryTfs = allTimeframes.filter((tf) => tf.category === category)
|
||||
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)}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{categoryTfs.map((tf) => {
|
||||
const isSelected = selectedTimeframes.includes(tf.value)
|
||||
const isPrimary = config.klines.primary_timeframe === tf.value
|
||||
return (
|
||||
<button
|
||||
key={tf.value}
|
||||
onClick={() => toggleTimeframe(tf.value)}
|
||||
onDoubleClick={() => setPrimaryTimeframe(tf.value)}
|
||||
disabled={disabled}
|
||||
className={`px-2 py-1 rounded text-xs font-medium transition-all ${
|
||||
isSelected ? '' : 'opacity-40 hover:opacity-70'
|
||||
}`}
|
||||
style={{
|
||||
background: isSelected ? `${categoryColors[category]}15` : 'transparent',
|
||||
border: `1px solid ${isSelected ? categoryColors[category] : '#2B3139'}`,
|
||||
color: isSelected ? categoryColors[category] : '#848E9C',
|
||||
boxShadow: isPrimary ? `0 0 0 2px ${categoryColors[category]}` : undefined,
|
||||
}}
|
||||
title={isPrimary ? `${tf.label} (Primary)` : tf.label}
|
||||
>
|
||||
{tf.label}
|
||||
{isPrimary && <span className="ml-0.5 text-[8px]">★</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technical Indicators */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{/* Section 2: Technical Indicators (Optional) */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{indicators.map(({ key, label, color, periodKey }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between p-2 rounded-lg"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
||||
<span className="text-xs" style={{ color: '#EAECEF' }}>{t(label)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Indicator Grid */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ key: 'enable_ema', label: 'ema', desc: 'emaDesc', color: '#F0B90B', periodKey: 'ema_periods', defaultPeriods: '20,50' },
|
||||
{ key: 'enable_macd', label: 'macd', desc: 'macdDesc', color: '#a855f7' },
|
||||
{ key: 'enable_rsi', label: 'rsi', desc: 'rsiDesc', color: '#F6465D', periodKey: 'rsi_periods', defaultPeriods: '7,14' },
|
||||
{ key: 'enable_atr', label: 'atr', desc: 'atrDesc', color: '#60a5fa', periodKey: 'atr_periods', defaultPeriods: '14' },
|
||||
].map(({ key, label, desc, color, periodKey, defaultPeriods }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="p-2.5 rounded-lg transition-all"
|
||||
style={{
|
||||
background: config[key as keyof IndicatorConfig] ? `${color}08` : 'transparent',
|
||||
border: `1px solid ${config[key as keyof IndicatorConfig] ? `${color}30` : '#2B3139'}`,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config[key as keyof IndicatorConfig] as boolean || false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, [key]: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{t(desc)}</p>
|
||||
{periodKey && config[key as keyof IndicatorConfig] && (
|
||||
<input
|
||||
type="text"
|
||||
value={(config[periodKey as keyof IndicatorConfig] as number[])?.join(',') || ''}
|
||||
value={(config[periodKey as keyof IndicatorConfig] as number[])?.join(',') || defaultPeriods}
|
||||
onChange={(e) => {
|
||||
if (disabled) return
|
||||
const periods = e.target.value
|
||||
@@ -245,52 +322,81 @@ export function IndicatorEditor({
|
||||
onChange({ ...config, [periodKey]: periods })
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder="7,14"
|
||||
className="w-16 px-1.5 py-0.5 rounded text-[10px] text-center"
|
||||
placeholder={defaultPeriods}
|
||||
className="w-full px-2 py-1 rounded text-[10px] text-center"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config[key as keyof IndicatorConfig] as boolean}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, [key]: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quant Data Source */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Database className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
{/* Section 3: Market Sentiment */}
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>{t('quantDataDesc')}</p>
|
||||
|
||||
<div
|
||||
className="p-3 rounded-lg space-y-3"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ key: 'enable_volume', label: 'volume', desc: 'volumeDesc', color: '#c084fc' },
|
||||
{ key: 'enable_oi', label: 'oi', desc: 'oiDesc', color: '#34d399' },
|
||||
{ key: 'enable_funding_rate', label: 'fundingRate', desc: 'fundingRateDesc', color: '#fbbf24' },
|
||||
].map(({ key, label, desc, color }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="p-2.5 rounded-lg transition-all"
|
||||
style={{
|
||||
background: config[key as keyof IndicatorConfig] ? `${color}08` : 'transparent',
|
||||
border: `1px solid ${config[key as keyof IndicatorConfig] ? `${color}30` : '#2B3139'}`,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config[key as keyof IndicatorConfig] as boolean || false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, [key]: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px]" style={{ color: '#5E6673' }}>{t(desc)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Quant Data (External API) */}
|
||||
<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' }}>
|
||||
<Database className="w-4 h-4" style={{ color: '#60a5fa' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('quantDataDesc')}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Enable Toggle */}
|
||||
<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" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_data || false}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, enable_quant_data: e.target.checked })
|
||||
}
|
||||
onChange={(e) => !disabled && onChange({ ...config, enable_quant_data: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-green-500"
|
||||
className="w-4 h-4 rounded accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -299,14 +405,14 @@ export function IndicatorEditor({
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{t('quantDataUrl')} <span style={{ color: '#5E6673' }}>({'{symbol}'} = 币种)</span>
|
||||
{t('quantDataUrl')}
|
||||
</label>
|
||||
{!disabled && !config.quant_data_api_url && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, quant_data_api_url: DEFAULT_QUANT_DATA_API_URL })}
|
||||
className="text-[10px] px-2 py-0.5 rounded"
|
||||
style={{ background: '#22c55e20', color: '#22c55e' }}
|
||||
style={{ background: '#60a5fa20', color: '#60a5fa' }}
|
||||
>
|
||||
{t('fillDefault')}
|
||||
</button>
|
||||
@@ -315,14 +421,13 @@ export function IndicatorEditor({
|
||||
<input
|
||||
type="text"
|
||||
value={config.quant_data_api_url || ''}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, quant_data_api_url: e.target.value })
|
||||
}
|
||||
onChange={(e) => !disabled && onChange({ ...config, quant_data_api_url: e.target.value })}
|
||||
disabled={disabled}
|
||||
placeholder="http://example.com/api/coin/{symbol}?include=netflow,oi,price"
|
||||
placeholder="http://example.com/api/coin/{symbol}?include=netflow,oi"
|
||||
className="w-full px-2 py-1.5 rounded text-xs font-mono"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('symbolPlaceholder')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -394,6 +394,9 @@ export interface CoinSourceConfig {
|
||||
|
||||
export interface IndicatorConfig {
|
||||
klines: KlineConfig;
|
||||
// Raw OHLCV kline data - required for AI analysis
|
||||
enable_raw_klines: boolean;
|
||||
// Technical indicators (optional)
|
||||
enable_ema: boolean;
|
||||
enable_macd: boolean;
|
||||
enable_rsi: boolean;
|
||||
|
||||
Reference in New Issue
Block a user