diff --git a/store/strategy.go b/store/strategy.go index 6a58d599..2e504519 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -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, diff --git a/web/src/components/strategy/IndicatorEditor.tsx b/web/src/components/strategy/IndicatorEditor.tsx index 78080516..01353ce4 100644 --- a/web/src/components/strategy/IndicatorEditor.tsx +++ b/web/src/components/strategy/IndicatorEditor.tsx @@ -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> = { - 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 = { 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 ( -
- {/* Timeframe Selection */} -
-
- - {t('timeframes')} +
+ {/* Section 1: Market Data (Required) */} +
+
+ + {t('marketData')} + - {t('marketDataDesc')}
-

{t('timeframesDesc')}

- {/* Timeframe Grid by Category */} -
- {(['scalp', 'intraday', 'swing', 'position'] as const).map((category) => { - const categoryTfs = allTimeframes.filter((tf) => tf.category === category) - return ( -
- - {t(category)} - -
- {categoryTfs.map((tf) => { - const isSelected = selectedTimeframes.includes(tf.value) - const isPrimary = config.klines.primary_timeframe === tf.value - return ( -
- -
- ) - })} -
+
+ {/* Raw Klines - Required, Always On */} +
+
+
+
- ) - })} -
+
+
+ {t('rawKlines')} + + + {t('required')} + +
+

{t('rawKlinesDesc')}

+
+
+ +
-

- {language === 'zh' ? '★ = 主周期 (双击设置)' : '★ = Primary (double-click to set)'} -

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

{t('timeframesDesc')}

- {/* K-line Count */} -
- {t('klineCount')}: - - !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 */} +
+ {(['scalp', 'intraday', 'swing', 'position'] as const).map((category) => { + const categoryTfs = allTimeframes.filter((tf) => tf.category === category) + return ( +
+ + {t(category)} + +
+ {categoryTfs.map((tf) => { + const isSelected = selectedTimeframes.includes(tf.value) + const isPrimary = config.klines.primary_timeframe === tf.value + return ( + + ) + })} +
+
+ ) + })} +
+
- {/* Technical Indicators */} -
-
+ {/* Section 2: Technical Indicators (Optional) */} +
+
{t('technicalIndicators')} + - {t('technicalIndicatorsDesc')}
-
- {indicators.map(({ key, label, color, periodKey }) => ( -
-
-
- {t(label)} -
-
+
+ {/* Tip */} +
+ +

{t('aiCanCalculate')}

+
+ + {/* Indicator Grid */} +
+ {[ + { 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 }) => ( +
+
+
+
+ {t(label)} +
+ !disabled && onChange({ ...config, [key]: e.target.checked })} + disabled={disabled} + className="w-4 h-4 rounded accent-yellow-500" + /> +
+

{t(desc)}

{periodKey && config[key as keyof IndicatorConfig] && ( { if (disabled) return const periods = e.target.value @@ -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' }} /> )} - - !disabled && onChange({ ...config, [key]: e.target.checked }) - } - disabled={disabled} - className="w-4 h-4 rounded accent-yellow-500" - />
-
- ))} + ))} +
- {/* Quant Data Source */} -
-
- - {t('quantData')} + {/* Section 3: Market Sentiment */} +
+
+ + {t('marketSentiment')} + - {t('marketSentimentDesc')}
-

{t('quantDataDesc')}

-
+
+
+ {[ + { 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 }) => ( +
+
+
+
+ {t(label)} +
+ !disabled && onChange({ ...config, [key]: e.target.checked })} + disabled={disabled} + className="w-4 h-4 rounded accent-yellow-500" + /> +
+

{t(desc)}

+
+ ))} +
+
+
+ + {/* Section 4: Quant Data (External API) */} +
+
+ + {t('quantData')} + - {t('quantDataDesc')} +
+ +
{/* Enable Toggle */}
-
- {t('quantData')} +
+ {t('quantData')}
- !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" />
@@ -299,14 +405,14 @@ export function IndicatorEditor({
{!disabled && !config.quant_data_api_url && ( @@ -315,14 +421,13 @@ export function IndicatorEditor({ - !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' }} /> +

{t('symbolPlaceholder')}

)}
diff --git a/web/src/types.ts b/web/src/types.ts index e6aae677..102ac847 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -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;