From 8eb38bc6f847a8e0ee12f554ec65d683b44169c5 Mon Sep 17 00:00:00 2001 From: TIANHE Date: Sun, 1 Feb 2026 15:02:14 +0800 Subject: [PATCH] feat: indicator parameterization support for kline chart --- backend_api_python/app/routes/indicator.py | 42 +++ backend_api_python/app/services/backtest.py | 16 + .../app/services/indicator_params.py | 295 ++++++++++++++++++ .../app/services/trading_executor.py | 18 ++ docs/CHANGELOG.md | 86 +++++ .../templates/strategy/dual_ma_with_params.py | 55 ++++ .../strategy/multi_indicator_composite.py | 108 +++++++ quantdinger_vue/src/layouts/BasicLayout.vue | 2 +- quantdinger_vue/src/locales/lang/en-US.js | 138 +++++++- quantdinger_vue/src/locales/lang/zh-CN.js | 138 +++++++- quantdinger_vue/src/views/dashboard/index.vue | 2 +- .../src/views/indicator-analysis/index.vue | 173 +++++++++- .../src/views/trading-assistant/index.vue | 105 ++++++- 13 files changed, 1166 insertions(+), 12 deletions(-) create mode 100644 backend_api_python/app/services/indicator_params.py create mode 100644 quantdinger_vue/public/templates/strategy/dual_ma_with_params.py create mode 100644 quantdinger_vue/public/templates/strategy/multi_indicator_composite.py diff --git a/backend_api_python/app/routes/indicator.py b/backend_api_python/app/routes/indicator.py index c3a5117..b9a2a26 100644 --- a/backend_api_python/app/routes/indicator.py +++ b/backend_api_python/app/routes/indicator.py @@ -306,6 +306,48 @@ def delete_indicator(): return jsonify({"code": 0, "msg": str(e), "data": None}), 500 +@indicator_bp.route("/getIndicatorParams", methods=["GET"]) +@login_required +def get_indicator_params(): + """ + 获取指标的参数声明 + + 用于前端在策略创建时显示可配置的参数表单。 + + Query params: + indicator_id: 指标ID + + Returns: + params: [ + { + "name": "ma_fast", + "type": "int", + "default": 5, + "description": "短期均线周期" + }, + ... + ] + """ + try: + from app.services.indicator_params import get_indicator_params as get_params + + indicator_id = request.args.get("indicator_id") + if not indicator_id: + return jsonify({"code": 0, "msg": "indicator_id is required", "data": None}), 400 + + try: + indicator_id = int(indicator_id) + except ValueError: + return jsonify({"code": 0, "msg": "indicator_id must be an integer", "data": None}), 400 + + params = get_params(indicator_id) + return jsonify({"code": 1, "msg": "success", "data": params}) + + except Exception as e: + logger.error(f"get_indicator_params failed: {str(e)}", exc_info=True) + return jsonify({"code": 0, "msg": str(e), "data": None}), 500 + + @indicator_bp.route("/verifyCode", methods=["POST"]) @login_required def verify_code(): diff --git a/backend_api_python/app/services/backtest.py b/backend_api_python/app/services/backtest.py index 88b084f..7022708 100644 --- a/backend_api_python/app/services/backtest.py +++ b/backend_api_python/app/services/backtest.py @@ -11,6 +11,7 @@ import numpy as np from app.data_sources import DataSourceFactory from app.utils.logger import get_logger +from app.services.indicator_params import IndicatorParamsParser, IndicatorCaller logger = get_logger(__name__) @@ -1114,6 +1115,21 @@ class BacktestService: local_vars['commission'] = backtest_params.get('commission', 0.0002) local_vars['trade_direction'] = backtest_params.get('trade_direction', 'both') + # === 指标参数支持 === + # 从 backtest_params 获取用户设置的指标参数 + user_indicator_params = (backtest_params or {}).get('indicator_params', {}) + # 解析指标代码中声明的参数 + declared_params = IndicatorParamsParser.parse_params(code) + # 合并参数(用户值优先,否则使用默认值) + merged_params = IndicatorParamsParser.merge_params(declared_params, user_indicator_params) + local_vars['params'] = merged_params + + # === 指标调用器支持 === + user_id = (backtest_params or {}).get('user_id', 1) + indicator_id = (backtest_params or {}).get('indicator_id') + indicator_caller = IndicatorCaller(user_id, indicator_id) + local_vars['call_indicator'] = indicator_caller.call_indicator + # Add technical indicator functions local_vars.update(self._get_indicator_functions()) diff --git a/backend_api_python/app/services/indicator_params.py b/backend_api_python/app/services/indicator_params.py new file mode 100644 index 0000000..2f74641 --- /dev/null +++ b/backend_api_python/app/services/indicator_params.py @@ -0,0 +1,295 @@ +""" +Indicator Parameters Parser and Helper Functions + +支持两个核心功能: +1. 指标参数外部传递 - 解析指标代码中的 @param 声明 +2. 指标调用其他指标 - 提供 call_indicator() 函数 + +参数声明格式: +# @param param_name type default_value 描述 +# @param ma_fast int 5 短期均线周期 +# @param ma_slow int 20 长期均线周期 +# @param threshold float 0.5 阈值 + +支持的类型:int, float, bool, str +""" + +import re +import json +from typing import Dict, Any, List, Optional, Tuple +from app.utils.logger import get_logger +from app.utils.db import get_db_connection + +logger = get_logger(__name__) + + +class IndicatorParamsParser: + """解析指标代码中的参数声明""" + + # 参数声明正则:# @param name type default description + PARAM_PATTERN = re.compile( + r'#\s*@param\s+(\w+)\s+(int|float|bool|str|string)\s+(\S+)\s*(.*)', + re.IGNORECASE + ) + + @classmethod + def parse_params(cls, indicator_code: str) -> List[Dict[str, Any]]: + """ + 解析指标代码中的参数声明 + + Returns: + List of param definitions: + [ + { + "name": "ma_fast", + "type": "int", + "default": 5, + "description": "短期均线周期" + }, + ... + ] + """ + params = [] + if not indicator_code: + return params + + for line in indicator_code.split('\n'): + line = line.strip() + match = cls.PARAM_PATTERN.match(line) + if match: + name = match.group(1) + param_type = match.group(2).lower() + default_str = match.group(3) + description = match.group(4).strip() if match.group(4) else '' + + # 转换默认值类型 + default = cls._convert_value(default_str, param_type) + + # 规范化类型名 + if param_type == 'string': + param_type = 'str' + + params.append({ + "name": name, + "type": param_type, + "default": default, + "description": description + }) + + return params + + @classmethod + def _convert_value(cls, value_str: str, param_type: str) -> Any: + """转换字符串值为对应类型""" + try: + param_type = param_type.lower() + if param_type == 'int': + return int(value_str) + elif param_type == 'float': + return float(value_str) + elif param_type == 'bool': + return value_str.lower() in ('true', '1', 'yes', 'on') + else: # str/string + return value_str + except (ValueError, TypeError): + return value_str + + @classmethod + def merge_params(cls, declared_params: List[Dict], user_params: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并声明的参数和用户提供的参数 + + Args: + declared_params: 从代码中解析的参数声明 + user_params: 用户提供的参数值 + + Returns: + 合并后的参数字典(使用用户值或默认值) + """ + result = {} + for param in declared_params: + name = param['name'] + param_type = param['type'] + default = param['default'] + + if name in user_params: + # 用户提供了值,转换为正确类型 + result[name] = cls._convert_value(str(user_params[name]), param_type) + else: + # 使用默认值 + result[name] = default + + return result + + +class IndicatorCaller: + """ + 指标调用器 - 允许一个指标调用另一个指标 + + 使用方式(在指标代码中): + # 按ID调用 + rsi_df = call_indicator(5, df) + + # 按名称调用(自己的指标) + macd_df = call_indicator('My MACD', df) + """ + + # 最大调用深度,防止循环依赖 + MAX_CALL_DEPTH = 5 + + def __init__(self, user_id: int, current_indicator_id: int = None): + self.user_id = user_id + self.current_indicator_id = current_indicator_id + self._call_stack = [] # 调用栈,用于检测循环依赖 + + def call_indicator( + self, + indicator_ref: Any, # int (ID) 或 str (名称) + df: 'pd.DataFrame', + params: Dict[str, Any] = None, + _depth: int = 0 + ) -> Optional['pd.DataFrame']: + """ + 调用另一个指标并返回结果 + + Args: + indicator_ref: 指标ID或名称 + df: 输入的K线数据 + params: 传递给被调用指标的参数 + _depth: 内部使用,跟踪调用深度 + + Returns: + 执行后的DataFrame,包含被调用指标计算的列 + """ + import pandas as pd + import numpy as np + + # 检查调用深度 + if _depth >= self.MAX_CALL_DEPTH: + logger.error(f"Indicator call depth exceeded {self.MAX_CALL_DEPTH}") + return df.copy() + + # 获取指标代码 + indicator_code, indicator_id = self._get_indicator_code(indicator_ref) + if not indicator_code: + logger.warning(f"Indicator not found: {indicator_ref}") + return df.copy() + + # 检查循环依赖 + if indicator_id in self._call_stack: + logger.error(f"Circular dependency detected: {self._call_stack} -> {indicator_id}") + return df.copy() + + self._call_stack.append(indicator_id) + + try: + # 解析并合并参数 + declared_params = IndicatorParamsParser.parse_params(indicator_code) + merged_params = IndicatorParamsParser.merge_params(declared_params, params or {}) + + # 准备执行环境 + df_copy = df.copy() + local_vars = { + 'df': df_copy, + 'open': df_copy['open'].astype('float64') if 'open' in df_copy.columns else pd.Series(dtype='float64'), + 'high': df_copy['high'].astype('float64') if 'high' in df_copy.columns else pd.Series(dtype='float64'), + 'low': df_copy['low'].astype('float64') if 'low' in df_copy.columns else pd.Series(dtype='float64'), + 'close': df_copy['close'].astype('float64') if 'close' in df_copy.columns else pd.Series(dtype='float64'), + 'volume': df_copy['volume'].astype('float64') if 'volume' in df_copy.columns else pd.Series(dtype='float64'), + 'signals': pd.Series(0, index=df_copy.index, dtype='float64'), + 'np': np, + 'pd': pd, + 'params': merged_params, + # 递归调用支持 + 'call_indicator': lambda ref, d, p=None: self.call_indicator(ref, d, p, _depth + 1) + } + + # 安全执行 + import builtins + def safe_import(name, *args, **kwargs): + allowed_modules = ['numpy', 'pandas', 'math', 'json', 'time'] + if name in allowed_modules or name.split('.')[0] in allowed_modules: + return builtins.__import__(name, *args, **kwargs) + raise ImportError(f"Module not allowed: {name}") + + safe_builtins = {k: getattr(builtins, k) for k in dir(builtins) + if not k.startswith('_') and k not in [ + 'eval', 'exec', 'compile', 'open', 'input', + 'help', 'exit', 'quit', '__import__', + 'copyright', 'credits', 'license' + ]} + safe_builtins['__import__'] = safe_import + + exec_env = local_vars.copy() + exec_env['__builtins__'] = safe_builtins + + pre_import = "import numpy as np\nimport pandas as pd\n" + exec(pre_import, exec_env) + exec(indicator_code, exec_env) + + return exec_env.get('df', df_copy) + + except Exception as e: + logger.error(f"Error calling indicator {indicator_ref}: {e}") + return df.copy() + finally: + self._call_stack.pop() + + def _get_indicator_code(self, indicator_ref: Any) -> Tuple[Optional[str], Optional[int]]: + """获取指标代码""" + try: + with get_db_connection() as db: + cursor = db.cursor() + + if isinstance(indicator_ref, int): + # 按ID查询 + cursor.execute(""" + SELECT id, code FROM qd_indicator_codes + WHERE id = %s AND (user_id = %s OR publish_to_community = 1) + """, (indicator_ref, self.user_id)) + else: + # 按名称查询(优先自己的指标) + cursor.execute(""" + SELECT id, code FROM qd_indicator_codes + WHERE name = %s AND user_id = %s + UNION + SELECT id, code FROM qd_indicator_codes + WHERE name = %s AND publish_to_community = 1 + LIMIT 1 + """, (str(indicator_ref), self.user_id, str(indicator_ref))) + + row = cursor.fetchone() + cursor.close() + + if row: + return row['code'], row['id'] + return None, None + + except Exception as e: + logger.error(f"Error fetching indicator code: {e}") + return None, None + + +def get_indicator_params(indicator_id: int) -> List[Dict[str, Any]]: + """ + 获取指标的参数声明(供API调用) + + Args: + indicator_id: 指标ID + + Returns: + 参数声明列表 + """ + try: + with get_db_connection() as db: + cursor = db.cursor() + cursor.execute("SELECT code FROM qd_indicator_codes WHERE id = %s", (indicator_id,)) + row = cursor.fetchone() + cursor.close() + + if row and row['code']: + return IndicatorParamsParser.parse_params(row['code']) + return [] + except Exception as e: + logger.error(f"Error getting indicator params: {e}") + return [] diff --git a/backend_api_python/app/services/trading_executor.py b/backend_api_python/app/services/trading_executor.py index fdf47dc..77b9f03 100644 --- a/backend_api_python/app/services/trading_executor.py +++ b/backend_api_python/app/services/trading_executor.py @@ -21,6 +21,7 @@ from app.utils.logger import get_logger from app.utils.db import get_db_connection from app.data_sources import DataSourceFactory from app.services.kline import KlineService +from app.services.indicator_params import IndicatorParamsParser, IndicatorCaller logger = get_logger(__name__) @@ -1789,6 +1790,21 @@ class TradingExecutor: # Also provide a backtest-modal compatible nested config object: cfg.risk/cfg.scale/cfg.position. tc = dict(trading_config or {}) cfg = self._build_cfg_from_trading_config(tc) + + # === 指标参数支持 === + # 从 trading_config 获取用户设置的指标参数 + user_indicator_params = tc.get('indicator_params', {}) + # 解析指标代码中声明的参数 + declared_params = IndicatorParamsParser.parse_params(indicator_code) + # 合并参数(用户值优先,否则使用默认值) + merged_params = IndicatorParamsParser.merge_params(declared_params, user_indicator_params) + + # === 指标调用器支持 === + # 获取用户ID和指标ID(用于 call_indicator 权限检查) + user_id = tc.get('user_id', 1) + indicator_id = tc.get('indicator_id') + indicator_caller = IndicatorCaller(user_id, indicator_id) + local_vars = { 'df': df, 'open': df['open'].astype('float64'), @@ -1802,6 +1818,8 @@ class TradingExecutor: 'trading_config': tc, 'config': tc, # alias 'cfg': cfg, # normalized nested config + 'params': merged_params, # 指标参数 (新增) + 'call_indicator': indicator_caller.call_indicator, # 调用其他指标 (新增) 'leverage': float(trading_config.get('leverage', 1)), 'initial_capital': float(trading_config.get('initial_capital', 1000)), 'commission': 0.001, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 878b245..46121dd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,92 @@ This document records version updates, new features, bug fixes, and database mig --- +## V2.1.2 (2026-02-01) + +### 🚀 New Features + +#### Indicator Parameter Support +- **External Parameter Passing** - Indicators can now declare parameters using `# @param` syntax that can be configured per-strategy + - Supported types: `int`, `float`, `bool`, `str` + - Parameters are displayed in the strategy creation form after selecting an indicator + - Different strategies using the same indicator can have different parameter values +- **Cross-Indicator Calling** - Indicators can now call other indicators using `call_indicator(id_or_name, df)` function + - Supports calling by indicator ID (number) or name (string) + - Maximum call depth of 5 to prevent circular dependencies + - Only allows calling own indicators or published community indicators + +#### Parameter Declaration Syntax +``` +# @param +``` + +| Field | Description | Example | +|-------|-------------|---------| +| name | Parameter name (variable name) | `ma_fast` | +| type | Data type: `int`, `float`, `bool`, `str` | `int` | +| default | Default value | `5` | +| description | Description (shown in UI tooltip) | `Short-term MA period` | + +#### Example: Dual Moving Average with Parameters +```python +# @param sma_short int 14 Short-term MA period +# @param sma_long int 28 Long-term MA period + +# Get parameters +sma_short_period = params.get('sma_short', 14) +sma_long_period = params.get('sma_long', 28) + +my_indicator_name = "Dual MA Strategy" +my_indicator_description = f"SMA{sma_short_period}/{sma_long_period} crossover" + +df = df.copy() +sma_short = df["close"].rolling(sma_short_period).mean() +sma_long = df["close"].rolling(sma_long_period).mean() + +# Golden cross / Death cross +buy = (sma_short > sma_long) & (sma_short.shift(1) <= sma_long.shift(1)) +sell = (sma_short < sma_long) & (sma_short.shift(1) >= sma_long.shift(1)) + +df["buy"] = buy.fillna(False).astype(bool) +df["sell"] = sell.fillna(False).astype(bool) + +# Chart markers +buy_marks = [df["low"].iloc[i] * 0.995 if df["buy"].iloc[i] else None for i in range(len(df))] +sell_marks = [df["high"].iloc[i] * 1.005 if df["sell"].iloc[i] else None for i in range(len(df))] + +output = { + "name": my_indicator_name, + "plots": [ + {"name": f"SMA{sma_short_period}", "data": sma_short.tolist(), "color": "#FF9800", "overlay": True}, + {"name": f"SMA{sma_long_period}", "data": sma_long.tolist(), "color": "#3F51B5", "overlay": True} + ], + "signals": [ + {"type": "buy", "text": "B", "data": buy_marks, "color": "#00E676"}, + {"type": "sell", "text": "S", "data": sell_marks, "color": "#FF5252"} + ] +} +``` + +#### Example: Using call_indicator() +```python +# Call another indicator by name or ID +# rsi_df = call_indicator('RSI', df) # By name +# rsi_df = call_indicator(5, df) # By ID +# rsi_df = call_indicator('RSI', df, {'period': 14}) # With params + +# Note: The called indicator must be created first +# and accessible (own indicator or published community indicator) +``` + +### 🐛 Bug Fixes + +#### Dashboard Fixes +- **Fixed current positions showing records from other users** - Position synchronization now correctly associates positions with the strategy owner's user_id +- **Fixed strategy distribution pie chart always showing "No Data"** - Chart now uses `strategy_stats` data which includes all strategies with trading activity +- **Removed AI strategy count from running strategies card** - Dashboard now only shows indicator strategy count since AI strategies category has been removed + +--- + ## V2.1.1 (2026-01-31) ### 🚀 New Features diff --git a/quantdinger_vue/public/templates/strategy/dual_ma_with_params.py b/quantdinger_vue/public/templates/strategy/dual_ma_with_params.py new file mode 100644 index 0000000..69bc9ea --- /dev/null +++ b/quantdinger_vue/public/templates/strategy/dual_ma_with_params.py @@ -0,0 +1,55 @@ +# ============================================================ +# 双均线策略 (支持外部参数配置) +# Dual Moving Average Strategy with External Parameters +# ============================================================ +# +# 使用方法: +# 1. 在交易助手中选择此指标 +# 2. 根据不同币种配置不同参数 +# - BTC/USDT: sma_short=5, sma_long=10 +# - ETH/USDT: sma_short=5, sma_long=20 +# +# ============================================================ + +# === 参数声明 (会在前端表单中显示) === +# @param sma_short int 14 短期均线周期 +# @param sma_long int 28 长期均线周期 + +# === 获取参数 (带默认值作为后备) === +sma_short_period = params.get('sma_short', 14) +sma_long_period = params.get('sma_long', 28) + +# === 指标信息 === +my_indicator_name = "双均线策略" +my_indicator_description = f"短期{sma_short_period}/长期{sma_long_period}均线交叉策略" + +# === 计算均线 === +df = df.copy() +sma_short = df["close"].rolling(sma_short_period).mean() +sma_long = df["close"].rolling(sma_long_period).mean() + +# === 生成买卖信号 === +# 金叉:短期均线上穿长期均线 +buy = (sma_short > sma_long) & (sma_short.shift(1) <= sma_long.shift(1)) +# 死叉:短期均线下穿长期均线 +sell = (sma_short < sma_long) & (sma_short.shift(1) >= sma_long.shift(1)) + +df["buy"] = buy.fillna(False).astype(bool) +df["sell"] = sell.fillna(False).astype(bool) + +# === 买卖标记点 (用于K线图显示) === +buy_marks = [df["low"].iloc[i] * 0.995 if df["buy"].iloc[i] else None for i in range(len(df))] +sell_marks = [df["high"].iloc[i] * 1.005 if df["sell"].iloc[i] else None for i in range(len(df))] + +# === 图表输出配置 === +output = { + "name": my_indicator_name, + "plots": [ + {"name": f"SMA{sma_short_period}", "data": sma_short.tolist(), "color": "#FF9800", "overlay": True}, + {"name": f"SMA{sma_long_period}", "data": sma_long.tolist(), "color": "#3F51B5", "overlay": True} + ], + "signals": [ + {"type": "buy", "text": "B", "data": buy_marks, "color": "#00E676"}, + {"type": "sell", "text": "S", "data": sell_marks, "color": "#FF5252"} + ] +} diff --git a/quantdinger_vue/public/templates/strategy/multi_indicator_composite.py b/quantdinger_vue/public/templates/strategy/multi_indicator_composite.py new file mode 100644 index 0000000..2daa538 --- /dev/null +++ b/quantdinger_vue/public/templates/strategy/multi_indicator_composite.py @@ -0,0 +1,108 @@ +# ============================================================ +# 多指标组合策略 (均线+RSI+MACD) +# Multi-Indicator Composite Strategy +# ============================================================ +# +# 使用方法: +# 1. 可配置均线周期、RSI阈值等参数 +# 2. 买入条件: RSI超卖 + MACD金叉 + 成交量放大 +# 3. 卖出条件: RSI超买 或 MACD死叉 +# +# ============================================================ + +# === 参数声明 === +# @param sma_short int 10 短期均线周期 +# @param sma_long int 30 长期均线周期 +# @param rsi_period int 14 RSI周期 +# @param rsi_oversold int 30 RSI超卖阈值 +# @param rsi_overbought int 70 RSI超买阈值 +# @param use_macd bool True 是否使用MACD过滤 +# @param use_volume bool False 是否使用成交量过滤 +# @param volume_mult float 1.5 成交量放大倍数 + +# === 获取参数 === +sma_short_period = params.get('sma_short', 10) +sma_long_period = params.get('sma_long', 30) +rsi_period = params.get('rsi_period', 14) +rsi_oversold = params.get('rsi_oversold', 30) +rsi_overbought = params.get('rsi_overbought', 70) +use_macd = params.get('use_macd', True) +use_volume = params.get('use_volume', False) +volume_mult = params.get('volume_mult', 1.5) + +# === 指标信息 === +my_indicator_name = "多指标组合策略" +my_indicator_description = f"SMA{sma_short_period}/{sma_long_period} + RSI{rsi_period}" + +df = df.copy() + +# === 计算均线 === +sma_short = df["close"].rolling(sma_short_period).mean() +sma_long = df["close"].rolling(sma_long_period).mean() + +# === 计算RSI === +delta = df["close"].diff() +gain = delta.where(delta > 0, 0).rolling(window=rsi_period).mean() +loss = (-delta.where(delta < 0, 0)).rolling(window=rsi_period).mean() +rs = gain / loss +rsi = 100 - (100 / (1 + rs)) + +# === 计算MACD === +exp1 = df["close"].ewm(span=12, adjust=False).mean() +exp2 = df["close"].ewm(span=26, adjust=False).mean() +macd = exp1 - exp2 +macd_signal = macd.ewm(span=9, adjust=False).mean() +macd_hist = macd - macd_signal + +# === 计算成交量均线 === +volume_ma = df["volume"].rolling(20).mean() + +# === 生成信号条件 === +# 均线金叉 +ma_golden = (sma_short > sma_long) & (sma_short.shift(1) <= sma_long.shift(1)) +# 均线死叉 +ma_death = (sma_short < sma_long) & (sma_short.shift(1) >= sma_long.shift(1)) +# RSI超卖 +rsi_buy = rsi < rsi_oversold +# RSI超买 +rsi_sell = rsi > rsi_overbought +# MACD金叉 +macd_golden = (macd > macd_signal) & (macd.shift(1) <= macd_signal.shift(1)) +# MACD死叉 +macd_death = (macd < macd_signal) & (macd.shift(1) >= macd_signal.shift(1)) +# 成交量放大 +volume_up = df["volume"] > volume_ma * volume_mult + +# === 综合买卖信号 === +buy = ma_golden | rsi_buy # 均线金叉 或 RSI超卖 + +if use_macd: + buy = buy & (macd > macd_signal) # 需要MACD向上 + +if use_volume: + buy = buy & volume_up # 需要成交量放大 + +sell = ma_death | rsi_sell # 均线死叉 或 RSI超买 + +if use_macd: + sell = sell | macd_death # MACD死叉也卖出 + +df["buy"] = buy.fillna(False).astype(bool) +df["sell"] = sell.fillna(False).astype(bool) + +# === 买卖标记点 === +buy_marks = [df["low"].iloc[i] * 0.995 if df["buy"].iloc[i] else None for i in range(len(df))] +sell_marks = [df["high"].iloc[i] * 1.005 if df["sell"].iloc[i] else None for i in range(len(df))] + +# === 图表输出配置 === +output = { + "name": my_indicator_name, + "plots": [ + {"name": f"SMA{sma_short_period}", "data": sma_short.tolist(), "color": "#FF9800", "overlay": True}, + {"name": f"SMA{sma_long_period}", "data": sma_long.tolist(), "color": "#3F51B5", "overlay": True} + ], + "signals": [ + {"type": "buy", "text": "B", "data": buy_marks, "color": "#00E676"}, + {"type": "sell", "text": "S", "data": sell_marks, "color": "#FF5252"} + ] +} diff --git a/quantdinger_vue/src/layouts/BasicLayout.vue b/quantdinger_vue/src/layouts/BasicLayout.vue index 80b18b1..93754af 100644 --- a/quantdinger_vue/src/layouts/BasicLayout.vue +++ b/quantdinger_vue/src/layouts/BasicLayout.vue @@ -119,7 +119,7 @@ diff --git a/quantdinger_vue/src/locales/lang/en-US.js b/quantdinger_vue/src/locales/lang/en-US.js index e6afa67..2c9d8c1 100644 --- a/quantdinger_vue/src/locales/lang/en-US.js +++ b/quantdinger_vue/src/locales/lang/en-US.js @@ -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 df['open_long']/df['close_long']/df['open_short']/df['close_short']. They should be boolean (True/False), not numeric. Use condition.fillna(False).astype(bool) 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 { diff --git a/quantdinger_vue/src/locales/lang/zh-CN.js b/quantdinger_vue/src/locales/lang/zh-CN.js index de18f2a..aaddd3b 100644 --- a/quantdinger_vue/src/locales/lang/zh-CN.js +++ b/quantdinger_vue/src/locales/lang/zh-CN.js @@ -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 { diff --git a/quantdinger_vue/src/views/dashboard/index.vue b/quantdinger_vue/src/views/dashboard/index.vue index 0fd5d16..a6d4596 100644 --- a/quantdinger_vue/src/views/dashboard/index.vue +++ b/quantdinger_vue/src/views/dashboard/index.vue @@ -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) { diff --git a/quantdinger_vue/src/views/indicator-analysis/index.vue b/quantdinger_vue/src/views/indicator-analysis/index.vue index e26a02f..143156e 100644 --- a/quantdinger_vue/src/views/indicator-analysis/index.vue +++ b/quantdinger_vue/src/views/indicator-analysis/index.vue @@ -385,6 +385,58 @@ @cancel="showBacktestModal = false; backtestIndicator = null" /> + + +
+
+ {{ pendingIndicator.name }} +
+ +
+
+
+ + + + +
+ + + + + + + + +
+
+ +
+
+ { + 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; + } +} diff --git a/quantdinger_vue/src/views/trading-assistant/index.vue b/quantdinger_vue/src/views/trading-assistant/index.vue index 399bc2f..340dc27 100644 --- a/quantdinger_vue/src/views/trading-assistant/index.vue +++ b/quantdinger_vue/src/views/trading-assistant/index.vue @@ -381,6 +381,51 @@ + + +
+ + +
+ + + + + + + + + +
+
+
+
+ {{ $t('trading-assistant.form.indicatorParamsHint') }} +
+
+
+ @@ -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;