feat: indicator parameterization support for kline chart

This commit is contained in:
TIANHE
2026-02-01 15:02:14 +08:00
parent 5818fbe725
commit 8eb38bc6f8
13 changed files with 1166 additions and 12 deletions
+1 -1
View File
@@ -119,7 +119,7 @@
</div>
<!-- 版本号 -->
<div class="footer-section version">
V2.1.1
V2.1.2
</div>
</div>
</div>
+137 -1
View File
@@ -25,6 +25,7 @@ const locale = {
'menu.home': 'Home',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'AI Analysis',
'menu.dashboard.aiQuant': 'AI Quant',
'menu.dashboard.indicator': 'Indicator Analysis',
'menu.dashboard.community': 'Indicator Market',
'menu.dashboard.tradingAssistant': 'Trading Assistant',
@@ -877,6 +878,9 @@ const locale = {
'dashboard.indicator.guide.section8.a6': 'A: If the chart does not display indicators, it is usually because the Python code has an error. Please check for syntax errors or array index out of bounds. It is recommended to debug the algorithm logic in a local Python environment first, then copy it in.',
'dashboard.indicator.guide.section8.q7': 'Q: Backtest shows 0 signals?',
'dashboard.indicator.guide.section8.a7': "A: Please check the data type of <code>df['open_long']</code>/<code>df['close_long']</code>/<code>df['open_short']</code>/<code>df['close_short']</code>. They should be boolean (True/False), not numeric. Use <code>condition.fillna(False).astype(bool)</code> to ensure correct type.",
'dashboard.indicator.paramsConfig.title': 'Indicator Parameters',
'dashboard.indicator.paramsConfig.noParams': 'No configurable parameters for this indicator',
'dashboard.indicator.paramsConfig.hint': 'Configure parameters and click OK to run the indicator',
'dashboard.indicator.backtest.title': 'Indicator Backtest',
'dashboard.indicator.backtest.config': 'Backtest Parameters',
'dashboard.indicator.backtest.startDate': 'Start Date',
@@ -1302,6 +1306,8 @@ const locale = {
'trading-assistant.form.qdtCostHints': 'Using strategies will consume QDT, please ensure your account has sufficient QDT balance',
'trading-assistant.form.indicatorDescription': 'Indicator Description',
'trading-assistant.form.noDescription': 'No description',
'trading-assistant.form.indicatorParams': 'Indicator Parameters',
'trading-assistant.form.indicatorParamsHint': 'These parameters will be passed to the indicator code. Different strategies can use different parameter values.',
'trading-assistant.form.exchange': 'Select Exchange',
'trading-assistant.form.apiKey': 'API Key',
'trading-assistant.form.secretKey': 'Secret Key',
@@ -2794,7 +2800,137 @@ const locale = {
// Market Overview
'fastAnalysis.marketOverview': 'Market Overview',
'fastAnalysis.selectTip': 'Select a symbol from your watchlist to start AI analysis'
'fastAnalysis.selectTip': 'Select a symbol from your watchlist to start AI analysis',
// ==================== AI Quant ====================
'aiQuant.title': 'AI Quant',
'aiQuant.strategyList': 'Strategies',
'aiQuant.create': 'Create',
'aiQuant.edit': 'Edit',
'aiQuant.delete': 'Delete',
'aiQuant.start': 'Start',
'aiQuant.stop': 'Stop',
'aiQuant.analyze': 'Analyze Now',
'aiQuant.noStrategy': 'No strategies yet, click to create',
'aiQuant.selectStrategy': 'Select a strategy from the left',
'aiQuant.createFirst': 'Create Your First Strategy',
'aiQuant.createStrategy': 'Create Strategy',
'aiQuant.editStrategy': 'Edit Strategy',
'aiQuant.confirmDelete': 'Are you sure you want to delete this strategy?',
'aiQuant.latestAnalysis': 'Latest Analysis',
'aiQuant.analysisHistory': 'Analysis History',
'aiQuant.decision': 'Decision',
'aiQuant.confidence': 'Confidence',
'aiQuant.currentPrice': 'Current Price',
'aiQuant.entryPrice': 'Entry Price',
'aiQuant.stopLoss': 'Stop Loss',
'aiQuant.takeProfit': 'Take Profit',
'aiQuant.reason': 'Reason',
'aiQuant.analyzedAt': 'Analyzed At',
'aiQuant.tradeSettings': 'Trade Settings',
'aiQuant.minutes': 'min',
'aiQuant.hour': 'hour',
'aiQuant.hours': 'hours',
// AI Quant Stats
'aiQuant.stats.totalStrategies': 'Total Strategies',
'aiQuant.stats.runningStrategies': 'Running',
'aiQuant.stats.totalAnalyses': 'Analyses',
'aiQuant.stats.totalPnl': 'Total PnL',
// AI Quant Status
'aiQuant.status.running': 'Running',
'aiQuant.status.stopped': 'Stopped',
'aiQuant.status.paused': 'Paused',
// AI Quant Execution Mode
'aiQuant.executionMode.signal': 'Signal Only',
'aiQuant.executionMode.live': 'Live Trading',
// AI Quant Market Type
'aiQuant.marketType.spot': 'Spot',
'aiQuant.marketType.futures': 'Futures',
// AI Quant Fields
'aiQuant.field.strategyName': 'Strategy Name',
'aiQuant.field.market': 'Market',
'aiQuant.field.symbol': 'Symbol',
'aiQuant.field.marketType': 'Market Type',
'aiQuant.field.aiModel': 'AI Model',
'aiQuant.field.interval': 'Analysis Interval',
'aiQuant.field.aiPrompt': 'AI Prompt',
'aiQuant.field.executionMode': 'Execution Mode',
'aiQuant.field.positionSize': 'Position Size',
'aiQuant.field.stopLoss': 'Stop Loss %',
'aiQuant.field.takeProfit': 'Take Profit %',
'aiQuant.field.totalAnalyses': 'Total Analyses',
'aiQuant.field.totalTrades': 'Total Trades',
'aiQuant.field.totalPnl': 'Total PnL',
// AI Quant Placeholders
'aiQuant.placeholder.strategyName': 'Enter strategy name',
'aiQuant.placeholder.market': 'Select market',
'aiQuant.placeholder.symbol': 'e.g., BTC/USDT',
'aiQuant.placeholder.aiModel': 'Use system default',
'aiQuant.placeholder.aiPrompt': 'Enter your trading strategy prompt, e.g., Buy when RSI is below 30...',
// AI Quant Validation
'aiQuant.validation.strategyName': 'Please enter strategy name',
'aiQuant.validation.market': 'Please select market',
'aiQuant.validation.symbol': 'Please enter symbol',
// AI Quant Table Columns
'aiQuant.table.decision': 'Decision',
'aiQuant.table.confidence': 'Confidence',
'aiQuant.table.entryPrice': 'Entry',
'aiQuant.table.stopLoss': 'Stop Loss',
'aiQuant.table.takeProfit': 'Take Profit',
'aiQuant.table.time': 'Time',
// AI Quant Messages
'aiQuant.msg.createSuccess': 'Strategy created successfully',
'aiQuant.msg.updateSuccess': 'Strategy updated successfully',
'aiQuant.msg.deleteSuccess': 'Strategy deleted successfully',
'aiQuant.msg.startSuccess': 'Strategy started',
'aiQuant.msg.stopSuccess': 'Strategy stopped',
'aiQuant.msg.analyzeSuccess': 'Analysis completed',
// AI Quant New Fields
'aiQuant.field.initialCapital': 'Initial Capital',
'aiQuant.field.leverage': 'Leverage',
'aiQuant.field.tradeDirection': 'Trade Direction',
'aiQuant.field.trailingStop': 'Trailing Stop',
'aiQuant.field.trailingStopPct': 'Trailing Stop %',
'aiQuant.direction.long': 'Long Only',
'aiQuant.direction.short': 'Short Only',
'aiQuant.direction.both': 'Both',
'aiQuant.riskControl': 'Risk Control',
'aiQuant.aiSettings': 'AI Settings',
'aiQuant.systemDefault': 'System Default',
'aiQuant.placeholder.selectSymbol': 'Select from watchlist',
'aiQuant.hint.symbolFromWatchlist': 'Select from your watchlist, market type is auto-detected',
'aiQuant.hint.spotLeverageFixed': 'Spot market leverage is fixed at 1x',
'aiQuant.hint.stopLossEnforced': 'Enforced stop loss, AI cannot modify',
'aiQuant.hint.takeProfitEnforced': 'Enforced take profit, AI cannot modify',
'aiQuant.hint.aiPromptOnly': 'AI only determines direction based on prompt, will not modify your risk settings',
'aiQuant.aiLimitWarning': 'AI Permission Restricted',
'aiQuant.aiLimitDescription': 'AI can ONLY determine trade direction (BUY/SELL/HOLD). Leverage, position size, stop loss/take profit are fully controlled by you and CANNOT be modified by AI.',
'aiQuant.userStopLoss': 'Your Stop Loss',
'aiQuant.userTakeProfit': 'Your Take Profit',
'aiQuant.userLeverage': 'Your Leverage',
'aiQuant.validation.initialCapital': 'Please enter initial capital',
'aiQuant.table.currentPrice': 'Current Price',
// AI Quant Prompt Templates
'aiQuant.field.promptTemplate': 'Strategy Template',
'aiQuant.placeholder.selectTemplate': 'Select a preset template',
'aiQuant.template.default': '📊 Comprehensive Analysis (Recommended)',
'aiQuant.template.trend': '📈 Trend Following',
'aiQuant.template.swing': '🔄 Swing Trading',
'aiQuant.template.news': '📰 News Driven',
'aiQuant.template.custom': '✏️ Custom',
'aiQuant.hint.dataProvided': 'System auto-provides: real-time price, indicators (RSI/MACD/MA), recent news, macro data. AI analyzes these with your prompt to determine direction.',
'aiQuant.hint.liveWarning': 'Live mode will trade with REAL money! Make sure you have configured exchange API and understand the risks!'
}
export default {
+137 -1
View File
@@ -27,6 +27,7 @@ const locale = {
'menu.dashboard.indicator': '指标分析',
'menu.dashboard.community': '指标市场',
'menu.dashboard.analysis': 'AI 分析',
'menu.dashboard.aiQuant': 'AI 量化',
'menu.dashboard.tradingAssistant': '交易助手',
'menu.dashboard.portfolio': '资产监测',
'menu.dashboard.globalMarket': '全球金融',
@@ -786,6 +787,9 @@ const locale = {
'dashboard.indicator.boundary.message': '提示:指标脚本只负责“计算 + 绘图 + buy/sell 信号”;仓位、风控、加减仓、手续费/滑点属于策略执行配置。',
'dashboard.indicator.boundary.indicatorRule': "指标脚本请只输出 buy/sell(并设定 df['buy']/df['sell'])。不要在脚本内撰写仓位管理、止盈止损、加减仓。",
'dashboard.indicator.boundary.backtestRule': '规则:同一根K线若出现主信号(buy/sell→开/平仓/反手),本K线将跳过所有加仓与减仓。',
'dashboard.indicator.paramsConfig.title': '指标参数配置',
'dashboard.indicator.paramsConfig.noParams': '该指标没有可配置的参数',
'dashboard.indicator.paramsConfig.hint': '设置参数后点击确定运行指标',
'dashboard.indicator.backtest.title': '指标回测',
'dashboard.indicator.backtest.config': '回测参数',
'dashboard.indicator.backtest.startDate': '开始日期',
@@ -1205,6 +1209,8 @@ const locale = {
'trading-assistant.form.qdtCostHints': '使用策略将消耗QDT,请确保账户有足够的QDT余额',
'trading-assistant.form.indicatorDescription': '指标描述',
'trading-assistant.form.noDescription': '暂无描述',
'trading-assistant.form.indicatorParams': '指标参数',
'trading-assistant.form.indicatorParamsHint': '这些参数会传递给指标代码,不同策略可以使用不同的参数值',
'trading-assistant.form.exchange': '选择交易所',
'trading-assistant.form.apiKey': 'API Key',
'trading-assistant.form.secretKey': 'Secret Key',
@@ -2604,7 +2610,137 @@ const locale = {
// 市场概览
'fastAnalysis.marketOverview': '市场概览',
'fastAnalysis.selectTip': '选择自选列表中的标的,开始 AI 智能分析'
'fastAnalysis.selectTip': '选择自选列表中的标的,开始 AI 智能分析',
// ==================== AI 量化 ====================
'aiQuant.title': 'AI 量化',
'aiQuant.strategyList': '策略列表',
'aiQuant.create': '创建',
'aiQuant.edit': '编辑',
'aiQuant.delete': '删除',
'aiQuant.start': '启动',
'aiQuant.stop': '停止',
'aiQuant.analyze': '立即分析',
'aiQuant.noStrategy': '暂无策略,点击创建开始',
'aiQuant.selectStrategy': '请从左侧选择一个策略',
'aiQuant.createFirst': '创建第一个策略',
'aiQuant.createStrategy': '创建策略',
'aiQuant.editStrategy': '编辑策略',
'aiQuant.confirmDelete': '确定要删除此策略吗?',
'aiQuant.latestAnalysis': '最新分析结果',
'aiQuant.analysisHistory': '分析历史',
'aiQuant.decision': '决策',
'aiQuant.confidence': '置信度',
'aiQuant.currentPrice': '当前价格',
'aiQuant.entryPrice': '建议入场',
'aiQuant.stopLoss': '止损价',
'aiQuant.takeProfit': '止盈价',
'aiQuant.reason': '理由',
'aiQuant.analyzedAt': '分析时间',
'aiQuant.tradeSettings': '交易设置',
'aiQuant.minutes': '分钟',
'aiQuant.hour': '小时',
'aiQuant.hours': '小时',
// AI量化统计
'aiQuant.stats.totalStrategies': '策略总数',
'aiQuant.stats.runningStrategies': '运行中',
'aiQuant.stats.totalAnalyses': '分析次数',
'aiQuant.stats.totalPnl': '总盈亏',
// AI量化状态
'aiQuant.status.running': '运行中',
'aiQuant.status.stopped': '已停止',
'aiQuant.status.paused': '已暂停',
// AI量化执行模式
'aiQuant.executionMode.signal': '仅信号',
'aiQuant.executionMode.live': '实盘交易',
// AI量化市场类型
'aiQuant.marketType.spot': '现货',
'aiQuant.marketType.futures': '合约',
// AI量化字段
'aiQuant.field.strategyName': '策略名称',
'aiQuant.field.market': '市场',
'aiQuant.field.symbol': '交易对',
'aiQuant.field.marketType': '市场类型',
'aiQuant.field.aiModel': 'AI 模型',
'aiQuant.field.interval': '分析间隔',
'aiQuant.field.aiPrompt': 'AI 提示词',
'aiQuant.field.executionMode': '执行模式',
'aiQuant.field.positionSize': '仓位大小',
'aiQuant.field.stopLoss': '止损比例',
'aiQuant.field.takeProfit': '止盈比例',
'aiQuant.field.totalAnalyses': '分析次数',
'aiQuant.field.totalTrades': '交易次数',
'aiQuant.field.totalPnl': '总盈亏',
// AI量化占位符
'aiQuant.placeholder.strategyName': '请输入策略名称',
'aiQuant.placeholder.market': '请选择市场',
'aiQuant.placeholder.symbol': '例如: BTC/USDT',
'aiQuant.placeholder.aiModel': '默认使用系统配置',
'aiQuant.placeholder.aiPrompt': '输入您的交易策略提示词,例如:当RSI低于30时考虑买入...',
// AI量化验证消息
'aiQuant.validation.strategyName': '请输入策略名称',
'aiQuant.validation.market': '请选择市场',
'aiQuant.validation.symbol': '请输入交易对',
// AI量化表格列
'aiQuant.table.decision': '决策',
'aiQuant.table.confidence': '置信度',
'aiQuant.table.entryPrice': '入场价',
'aiQuant.table.stopLoss': '止损价',
'aiQuant.table.takeProfit': '止盈价',
'aiQuant.table.time': '时间',
// AI量化消息
'aiQuant.msg.createSuccess': '策略创建成功',
'aiQuant.msg.updateSuccess': '策略更新成功',
'aiQuant.msg.deleteSuccess': '策略删除成功',
'aiQuant.msg.startSuccess': '策略已启动',
'aiQuant.msg.stopSuccess': '策略已停止',
'aiQuant.msg.analyzeSuccess': '分析完成',
// AI量化新增字段
'aiQuant.field.initialCapital': '投入资金',
'aiQuant.field.leverage': '杠杆倍数',
'aiQuant.field.tradeDirection': '交易方向',
'aiQuant.field.trailingStop': '移动止损',
'aiQuant.field.trailingStopPct': '移动止损比例',
'aiQuant.direction.long': '做多',
'aiQuant.direction.short': '做空',
'aiQuant.direction.both': '双向',
'aiQuant.riskControl': '风控设置',
'aiQuant.aiSettings': 'AI设置',
'aiQuant.systemDefault': '系统默认',
'aiQuant.placeholder.selectSymbol': '从自选列表选择交易对',
'aiQuant.hint.symbolFromWatchlist': '从您的自选列表中选择,系统自动识别市场类型',
'aiQuant.hint.spotLeverageFixed': '现货市场杠杆固定为1x',
'aiQuant.hint.stopLossEnforced': '强制止损,AI不可修改',
'aiQuant.hint.takeProfitEnforced': '强制止盈,AI不可修改',
'aiQuant.hint.aiPromptOnly': 'AI仅根据提示词判断方向,不会修改您设置的风控参数',
'aiQuant.aiLimitWarning': 'AI权限限制',
'aiQuant.aiLimitDescription': 'AI只能判断交易方向(买入/卖出/持有),杠杆倍数、下单金额、止盈止损等风控参数由您完全控制,AI无法修改。',
'aiQuant.userStopLoss': '您的止损',
'aiQuant.userTakeProfit': '您的止盈',
'aiQuant.userLeverage': '您的杠杆',
'aiQuant.validation.initialCapital': '请输入投入资金',
'aiQuant.table.currentPrice': '当前价格',
// AI量化提示词模板
'aiQuant.field.promptTemplate': '策略模板',
'aiQuant.placeholder.selectTemplate': '选择预设策略模板',
'aiQuant.template.default': '📊 综合分析(推荐)',
'aiQuant.template.trend': '📈 趋势跟踪',
'aiQuant.template.swing': '🔄 波段交易',
'aiQuant.template.news': '📰 新闻驱动',
'aiQuant.template.custom': '✏️ 自定义',
'aiQuant.hint.dataProvided': '系统自动提供:实时价格、技术指标(RSI/MACD/均线)、最近新闻、宏观数据。AI将基于这些数据和您的提示词判断方向。',
'aiQuant.hint.liveWarning': '实盘模式将使用真实资金交易,请确保已配置交易所API并充分了解风险!'
}
export default {
@@ -1044,7 +1044,7 @@ export default {
// Use strategy_stats for pie chart data (shows all strategies, not just those with positions)
const stats = Array.isArray(this.summary.strategy_stats) ? this.summary.strategy_stats : []
const raw = Array.isArray(this.summary.strategy_pnl_chart) ? this.summary.strategy_pnl_chart : []
// Prefer strategy_stats if available, fallback to strategy_pnl_chart
let data = []
if (stats.length > 0) {
@@ -385,6 +385,58 @@
@cancel="showBacktestModal = false; backtestIndicator = null"
/>
<!-- 指标参数配置弹窗 -->
<a-modal
:visible="showParamsModal"
:title="$t('dashboard.indicator.paramsConfig.title')"
:confirmLoading="loadingParams"
@ok="confirmIndicatorParams"
@cancel="cancelIndicatorParams"
:width="500"
>
<div v-if="pendingIndicator" class="params-config-modal">
<div class="indicator-info">
<span class="indicator-name">{{ pendingIndicator.name }}</span>
</div>
<a-divider />
<div v-if="indicatorParams.length > 0" class="params-form">
<div v-for="param in indicatorParams" :key="param.name" class="param-item">
<div class="param-header">
<label class="param-label">{{ param.name }}</label>
<a-tooltip v-if="param.description" :title="param.description">
<a-icon type="question-circle" style="color: #999; margin-left: 4px;" />
</a-tooltip>
</div>
<!-- 整数类型 -->
<a-input-number
v-if="param.type === 'int'"
v-model="indicatorParamValues[param.name]"
:precision="0"
style="width: 100%;"
/>
<!-- 浮点数类型 -->
<a-input-number
v-else-if="param.type === 'float'"
v-model="indicatorParamValues[param.name]"
:precision="4"
style="width: 100%;"
/>
<!-- 布尔类型 -->
<a-switch
v-else-if="param.type === 'bool'"
v-model="indicatorParamValues[param.name]"
/>
<!-- 字符串类型 -->
<a-input
v-else
v-model="indicatorParamValues[param.name]"
/>
</div>
</div>
<a-empty v-else :description="$t('dashboard.indicator.paramsConfig.noParams')" />
</div>
</a-modal>
<!-- 回测记录抽屉 -->
<backtest-history-drawer
:visible="showBacktestHistoryDrawer"
@@ -685,6 +737,14 @@ export default {
const purchasedIndicators = ref([]) // is_buy=1
const loadingIndicators = ref(false)
//
const showParamsModal = ref(false)
const pendingIndicator = ref(null) //
const pendingSource = ref('') // (custom/purchased)
const indicatorParams = ref([]) //
const indicatorParamValues = ref({}) //
const loadingParams = ref(false)
//
const customSectionCollapsed = ref(false) //
const purchasedSectionCollapsed = ref(false) //
@@ -1296,9 +1356,13 @@ export default {
return
}
//
const userParams = indicator.userParams || {}
// Python
//
const savedCode = pythonCode
const savedUserParams = { ...userParams } //
const pythonIndicator = {
id: indicatorId, // ID bought-1
name: indicator.name,
@@ -1306,6 +1370,7 @@ export default {
code: savedCode,
description: indicator.description,
parsed: parsed, //
userParams: savedUserParams, //
// IDID
originalId: indicator.id, // ID
user_id: indicator.user_id || indicator.userId, // ID
@@ -1314,7 +1379,8 @@ export default {
// KlineChart ref 访 executePythonStrategy
// 使savedCode使
// indicator
return klineChart.value.executePythonStrategy(savedCode, data, params, {
// params params.get('name', default) 访
return klineChart.value.executePythonStrategy(savedCode, data, { ...params, ...savedUserParams }, {
id: indicator.id, // 使ID
user_id: indicator.user_id || indicator.userId,
is_encrypted: indicator.is_encrypted || indicator.isEncrypted || 0
@@ -1322,10 +1388,10 @@ export default {
}
}
const indicatorParams = { ...parsed.params }
const indicatorParamsFromParsed = { ...parsed.params, ...userParams }
activeIndicators.value.push({
...pythonIndicator,
params: indicatorParams
params: indicatorParamsFromParsed
})
// KlineChart watch activeIndicators
} catch (error) {
@@ -1334,15 +1400,63 @@ export default {
}
//
const toggleIndicator = (indicator, source) => {
const toggleIndicator = async (indicator, source) => {
const indicatorId = `${source}-${indicator.id}`
if (isIndicatorActive(indicatorId)) {
removeIndicator(indicatorId)
} else {
addPythonIndicator(indicator, source)
//
try {
loadingParams.value = true
const res = await proxy.$http.get('/api/indicator/getIndicatorParams', {
params: { indicator_id: indicator.id }
})
if (res && res.code === 1 && Array.isArray(res.data) && res.data.length > 0) {
//
indicatorParams.value = res.data
indicatorParamValues.value = {}
res.data.forEach(p => {
indicatorParamValues.value[p.name] = p.default
})
pendingIndicator.value = indicator
pendingSource.value = source
showParamsModal.value = true
} else {
//
addPythonIndicator(indicator, source)
}
} catch (err) {
console.warn('Failed to load indicator params:', err)
//
addPythonIndicator(indicator, source)
} finally {
loadingParams.value = false
}
}
}
//
const confirmIndicatorParams = () => {
if (pendingIndicator.value) {
//
const indicatorWithParams = {
...pendingIndicator.value,
userParams: { ...indicatorParamValues.value }
}
addPythonIndicator(indicatorWithParams, pendingSource.value)
}
showParamsModal.value = false
pendingIndicator.value = null
pendingSource.value = ''
}
//
const cancelIndicatorParams = () => {
showParamsModal.value = false
pendingIndicator.value = null
pendingSource.value = ''
}
//
const handleRunIndicator = (data) => {
const { code, name } = data
@@ -1831,6 +1945,14 @@ getMarketColor,
handlePriceChange,
handleChartRetry,
handleIndicatorToggle,
//
showParamsModal,
pendingIndicator,
indicatorParams,
indicatorParamValues,
loadingParams,
confirmIndicatorParams,
cancelIndicatorParams,
//
showBacktestModal,
backtestIndicator,
@@ -3210,4 +3332,45 @@ getMarketColor,
}
}
}
/* 指标参数配置弹窗 */
.params-config-modal {
.indicator-info {
text-align: center;
margin-bottom: 8px;
.indicator-name {
font-size: 16px;
font-weight: 600;
color: #1f1f1f;
}
}
.params-form {
.param-item {
margin-bottom: 16px;
.param-header {
display: flex;
align-items: center;
margin-bottom: 6px;
.param-label {
font-weight: 500;
color: #333;
}
}
}
}
}
.theme-dark .params-config-modal {
.indicator-info .indicator-name {
color: #e0e0e0;
}
.params-form .param-item .param-header .param-label {
color: #d0d0d0;
}
}
</style>
@@ -381,6 +381,51 @@
</div>
</a-form-item>
<!-- 指标参数配置 -->
<a-form-item v-if="indicatorParams.length > 0" :label="$t('trading-assistant.form.indicatorParams')">
<div class="indicator-params-form">
<a-row :gutter="16">
<a-col v-for="param in indicatorParams" :key="param.name" :xs="24" :sm="12" :md="8">
<div class="param-item">
<label class="param-label">
{{ param.name }}
<a-tooltip v-if="param.description" :title="param.description">
<a-icon type="question-circle" style="margin-left: 4px; color: #999;" />
</a-tooltip>
</label>
<!-- 整数类型 -->
<a-input-number
v-if="param.type === 'int'"
v-model="indicatorParamValues[param.name]"
:precision="0"
style="width: 100%;"
size="small" />
<!-- 浮点数类型 -->
<a-input-number
v-else-if="param.type === 'float'"
v-model="indicatorParamValues[param.name]"
:precision="4"
style="width: 100%;"
size="small" />
<!-- 布尔类型 -->
<a-switch
v-else-if="param.type === 'bool'"
v-model="indicatorParamValues[param.name]"
size="small" />
<!-- 字符串类型 -->
<a-input
v-else
v-model="indicatorParamValues[param.name]"
size="small" />
</div>
</a-col>
</a-row>
<div class="form-item-hint" style="margin-top: 8px;">
{{ $t('trading-assistant.form.indicatorParamsHint') }}
</div>
</div>
</a-form-item>
<a-divider />
<a-form-item :label="$t('trading-assistant.form.strategyName')">
@@ -1603,6 +1648,8 @@ export default {
loadingIndicators: false,
availableIndicators: [],
selectedIndicator: null,
indicatorParams: [], //
indicatorParamValues: {}, //
cryptoSymbols: CRYPTO_SYMBOLS,
// Watchlist symbols (same source as indicator-analysis page)
loadingWatchlist: false,
@@ -2359,7 +2406,17 @@ export default {
this.form.setFieldsValue({
indicator_id: finalId
})
this.handleIndicatorChange(finalId)
await this.handleIndicatorChange(finalId)
//
const savedParams = strategy.trading_config?.indicator_params
if (savedParams && typeof savedParams === 'object') {
Object.keys(savedParams).forEach(key => {
if (key in this.indicatorParamValues) {
this.indicatorParamValues[key] = savedParams[key]
}
})
}
} else {
// ID
this.form.setFieldsValue({
@@ -2795,9 +2852,30 @@ export default {
this.loadingIndicators = false
}
},
handleIndicatorChange (indicatorId) {
async handleIndicatorChange (indicatorId) {
const idStr = String(indicatorId)
this.selectedIndicator = this.availableIndicators.find(ind => String(ind.id) === idStr)
//
this.indicatorParams = []
this.indicatorParamValues = {}
if (indicatorId) {
try {
const res = await this.$http.get('/api/indicator/getIndicatorParams', {
params: { indicator_id: indicatorId }
})
// response.data访 res.code res.data
if (res && res.code === 1 && Array.isArray(res.data)) {
this.indicatorParams = res.data
//
res.data.forEach(p => {
this.indicatorParamValues[p.name] = p.default
})
}
} catch (err) {
console.warn('Failed to load indicator params:', err)
}
}
},
handleMarketTypeChange (e) {
const marketType = e.target.value
@@ -3442,7 +3520,9 @@ export default {
commission: values.commission || 0,
slippage: values.slippage || 0,
// AI
enable_ai_filter: enableAiFilter
enable_ai_filter: enableAiFilter,
//
indicator_params: this.indicatorParamValues
}
}
@@ -4988,6 +5068,25 @@ export default {
line-height: 1.6;
}
.indicator-params-form {
padding: 12px;
background-color: var(--bg-color-secondary, #f5f7fa);
border-radius: 6px;
border: 1px dashed var(--border-color, #e0e0e0);
}
.indicator-params-form .param-item {
margin-bottom: 12px;
}
.indicator-params-form .param-label {
display: block;
font-size: 13px;
color: var(--text-color, #666);
margin-bottom: 4px;
font-weight: 500;
}
.form-item-hint {
margin-top: 4px;
font-size: 12px;