Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
<!-- 版本号 -->
|
||||
<div class="footer-section version">
|
||||
V2.1.1
|
||||
V2.1.2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -132,13 +132,10 @@
|
||||
<span class="kpi-label">{{ $t('dashboard.runningStrategies') || '运行中策略' }}</span>
|
||||
</div>
|
||||
<div class="kpi-value">
|
||||
<span class="amount">{{ summary.indicator_strategy_count + summary.ai_strategy_count }}</span>
|
||||
<span class="amount">{{ summary.indicator_strategy_count }}</span>
|
||||
<span class="unit">{{ $t('dashboard.unit.strategies') }}</span>
|
||||
</div>
|
||||
<div class="kpi-sub">
|
||||
<span class="highlight">{{ summary.ai_strategy_count }}</span>
|
||||
<span class="label"> AI</span>
|
||||
<span class="divider">·</span>
|
||||
<span class="highlight">{{ summary.indicator_strategy_count }}</span>
|
||||
<span class="label"> {{ $t('dashboard.label.indicator') }}</span>
|
||||
</div>
|
||||
@@ -1044,14 +1041,30 @@ export default {
|
||||
if (!chartDom) return
|
||||
this.pieChart = echarts.init(chartDom)
|
||||
|
||||
// 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 : []
|
||||
const data = raw
|
||||
.map(it => {
|
||||
const name = (it && it.name) ? String(it.name) : '-'
|
||||
const val = Number(it && it.value ? it.value : 0)
|
||||
return { name, value: Math.abs(val), signedValue: val }
|
||||
})
|
||||
.filter(it => it.value > 0)
|
||||
|
||||
// Prefer strategy_stats if available, fallback to strategy_pnl_chart
|
||||
let data = []
|
||||
if (stats.length > 0) {
|
||||
data = stats.map(it => {
|
||||
const name = (it && it.strategy_name) ? String(it.strategy_name) : '-'
|
||||
const val = Number(it && it.total_pnl ? it.total_pnl : 0)
|
||||
const trades = Number(it && it.total_trades ? it.total_trades : 0)
|
||||
// Use trades count as value if no PnL, so at least we show the distribution
|
||||
const displayVal = val !== 0 ? Math.abs(val) : trades
|
||||
return { name, value: displayVal, signedValue: val, trades }
|
||||
}).filter(it => it.value > 0)
|
||||
} else {
|
||||
data = raw
|
||||
.map(it => {
|
||||
const name = (it && it.name) ? String(it.name) : '-'
|
||||
const val = Number(it && it.value ? it.value : 0)
|
||||
return { name, value: Math.abs(val), signedValue: val }
|
||||
})
|
||||
.filter(it => it.value > 0)
|
||||
}
|
||||
|
||||
const isDark = this.isDarkTheme
|
||||
const textColor = isDark ? '#9ca3af' : '#6b7280'
|
||||
|
||||
@@ -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, // 保存用户参数
|
||||
// 保存原始数据库ID和用户ID,用于解密
|
||||
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')">
|
||||
@@ -1605,6 +1650,8 @@ export default {
|
||||
loadingIndicators: false,
|
||||
availableIndicators: [],
|
||||
selectedIndicator: null,
|
||||
indicatorParams: [], // 指标参数声明
|
||||
indicatorParamValues: {}, // 用户设置的参数值
|
||||
cryptoSymbols: CRYPTO_SYMBOLS,
|
||||
// Watchlist symbols (same source as indicator-analysis page)
|
||||
loadingWatchlist: false,
|
||||
@@ -2361,7 +2408,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({
|
||||
@@ -2797,9 +2854,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
|
||||
@@ -3444,7 +3522,9 @@ export default {
|
||||
commission: values.commission || 0,
|
||||
slippage: values.slippage || 0,
|
||||
// AI智能决策过滤
|
||||
enable_ai_filter: enableAiFilter
|
||||
enable_ai_filter: enableAiFilter,
|
||||
// 指标参数(外部传递)
|
||||
indicator_params: this.indicatorParamValues
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4990,6 +5070,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;
|
||||
|
||||
Reference in New Issue
Block a user