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:
tinkle-community
2025-12-08 13:02:51 +08:00
parent 7a6e6f2d92
commit 9c53a266c0
3 changed files with 273 additions and 162 deletions
+10 -7
View File
@@ -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,
+260 -155
View File
@@ -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>
+3
View File
@@ -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;