Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -472,10 +472,25 @@ def send_verification_code():
|
||||
if not email or not email_service.is_valid_email(email):
|
||||
return jsonify({'code': 0, 'msg': 'Invalid email address', 'data': None}), 400
|
||||
|
||||
# Verify Turnstile
|
||||
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
|
||||
if not turnstile_ok:
|
||||
return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400
|
||||
# For change_password type with logged-in user, skip Turnstile verification
|
||||
# because user already authenticated
|
||||
skip_turnstile = False
|
||||
if code_type == 'change_password':
|
||||
# Try to get user_id from token (this route doesn't require login)
|
||||
from app.utils.auth import verify_token
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if auth_header:
|
||||
parts = auth_header.split()
|
||||
if len(parts) == 2 and parts[0].lower() == 'bearer':
|
||||
payload = verify_token(parts[1])
|
||||
if payload and payload.get('user_id'):
|
||||
skip_turnstile = True
|
||||
|
||||
# Verify Turnstile (skip for authenticated change_password requests)
|
||||
if not skip_turnstile:
|
||||
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
|
||||
if not turnstile_ok:
|
||||
return jsonify({'code': 0, 'msg': turnstile_msg, 'data': None}), 400
|
||||
|
||||
# Check rate limit
|
||||
can_send, rate_msg = security.can_send_verification_code(email, ip_address)
|
||||
|
||||
@@ -49,13 +49,17 @@ def analyze():
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
# Get current user's ID to associate analysis with user
|
||||
user_id = getattr(g, 'user_id', None)
|
||||
|
||||
service = get_fast_analysis_service()
|
||||
result = service.analyze(
|
||||
market=market,
|
||||
symbol=symbol,
|
||||
language=language,
|
||||
model=model,
|
||||
timeframe=timeframe
|
||||
timeframe=timeframe,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if result.get('error'):
|
||||
@@ -197,8 +201,11 @@ def get_all_history():
|
||||
page = int(request.args.get('page', 1))
|
||||
pagesize = min(int(request.args.get('pagesize', 20)), 50)
|
||||
|
||||
# Get current user's ID to filter history
|
||||
user_id = getattr(g, 'user_id', None)
|
||||
|
||||
memory = get_analysis_memory()
|
||||
result = memory.get_all_history(page=page, page_size=pagesize)
|
||||
result = memory.get_all_history(user_id=user_id, page=page, page_size=pagesize)
|
||||
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
@@ -229,8 +236,11 @@ def delete_history(memory_id: int):
|
||||
DELETE /api/fast-analysis/history/123
|
||||
"""
|
||||
try:
|
||||
# Get current user's ID to ensure they can only delete their own records
|
||||
user_id = getattr(g, 'user_id', None)
|
||||
|
||||
memory = get_analysis_memory()
|
||||
success = memory.delete_history(memory_id)
|
||||
success = memory.delete_history(memory_id, user_id=user_id)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
@@ -241,7 +251,7 @@ def delete_history(memory_id: int):
|
||||
else:
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'Record not found',
|
||||
'msg': 'Record not found or no permission',
|
||||
'data': None
|
||||
}), 404
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -50,6 +50,7 @@ class AnalysisMemory:
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS qd_analysis_memory (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT,
|
||||
market VARCHAR(50) NOT NULL,
|
||||
symbol VARCHAR(50) NOT NULL,
|
||||
decision VARCHAR(10) NOT NULL,
|
||||
@@ -77,18 +78,22 @@ class AnalysisMemory:
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created
|
||||
ON qd_analysis_memory(created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user
|
||||
ON qd_analysis_memory(user_id);
|
||||
""")
|
||||
db.commit()
|
||||
cur.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Memory table creation skipped: {e}")
|
||||
|
||||
def store(self, analysis_result: Dict[str, Any]) -> Optional[int]:
|
||||
def store(self, analysis_result: Dict[str, Any], user_id: int = None) -> Optional[int]:
|
||||
"""
|
||||
Store an analysis result for future reference.
|
||||
|
||||
Args:
|
||||
analysis_result: Result from FastAnalysisService.analyze()
|
||||
user_id: User ID who created this analysis
|
||||
|
||||
Returns:
|
||||
Memory ID or None if failed
|
||||
@@ -115,12 +120,12 @@ class AnalysisMemory:
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO qd_analysis_memory (
|
||||
market, symbol, decision, confidence,
|
||||
user_id, market, symbol, decision, confidence,
|
||||
price_at_analysis, entry_price, stop_loss, take_profit,
|
||||
summary, reasons, risks, scores, indicators_snapshot, raw_result
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (market, symbol, decision, confidence, price, entry, stop, take,
|
||||
""", (user_id, market, symbol, decision, confidence, price, entry, stop, take,
|
||||
summary, reasons, risks, scores, indicators, raw))
|
||||
|
||||
# 使用 lastrowid 属性获取 ID(execute 内部已经处理了 RETURNING)
|
||||
@@ -128,7 +133,7 @@ class AnalysisMemory:
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
logger.info(f"Stored analysis memory #{memory_id} for {symbol}")
|
||||
logger.info(f"Stored analysis memory #{memory_id} for {symbol} by user {user_id}")
|
||||
return memory_id
|
||||
|
||||
except Exception as e:
|
||||
@@ -192,7 +197,7 @@ class AnalysisMemory:
|
||||
Get all analysis history with pagination.
|
||||
|
||||
Args:
|
||||
user_id: Optional user ID filter (not used currently, for future)
|
||||
user_id: User ID filter (required to show only user's own history)
|
||||
page: Page number (1-indexed)
|
||||
page_size: Items per page
|
||||
|
||||
@@ -205,21 +210,27 @@ class AnalysisMemory:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
# Build WHERE clause based on user_id
|
||||
where_clause = "WHERE user_id = %s" if user_id else ""
|
||||
params_count = (user_id,) if user_id else ()
|
||||
|
||||
# Get total count
|
||||
cur.execute("SELECT COUNT(*) as cnt FROM qd_analysis_memory")
|
||||
cur.execute(f"SELECT COUNT(*) as cnt FROM qd_analysis_memory {where_clause}", params_count)
|
||||
total_row = cur.fetchone()
|
||||
total = total_row['cnt'] if total_row else 0
|
||||
|
||||
# Get paginated results
|
||||
cur.execute("""
|
||||
params = (user_id, page_size, offset) if user_id else (page_size, offset)
|
||||
cur.execute(f"""
|
||||
SELECT
|
||||
id, market, symbol, decision, confidence, price_at_analysis,
|
||||
summary, reasons, scores, indicators_snapshot, raw_result,
|
||||
created_at, validated_at, was_correct, actual_return_pct
|
||||
FROM qd_analysis_memory
|
||||
{where_clause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", (page_size, offset))
|
||||
""", params)
|
||||
|
||||
rows = cur.fetchall() or []
|
||||
cur.close()
|
||||
@@ -254,12 +265,13 @@ class AnalysisMemory:
|
||||
logger.error(f"Failed to get all history: {e}")
|
||||
return {"items": [], "total": 0, "page": page, "page_size": page_size}
|
||||
|
||||
def delete_history(self, memory_id: int) -> bool:
|
||||
def delete_history(self, memory_id: int, user_id: int = None) -> bool:
|
||||
"""
|
||||
Delete a history record by ID.
|
||||
|
||||
Args:
|
||||
memory_id: The ID of the analysis memory to delete
|
||||
user_id: User ID to ensure user can only delete their own records
|
||||
|
||||
Returns:
|
||||
True if deleted successfully, False otherwise
|
||||
@@ -267,7 +279,11 @@ class AnalysisMemory:
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s", (memory_id,))
|
||||
if user_id:
|
||||
# Only delete if it belongs to the user
|
||||
cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s AND user_id = %s", (memory_id, user_id))
|
||||
else:
|
||||
cur.execute("DELETE FROM qd_analysis_memory WHERE id = %s", (memory_id,))
|
||||
db.commit()
|
||||
affected = cur.rowcount
|
||||
cur.close()
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -442,10 +442,18 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_
|
||||
# ==================== Main Analysis ====================
|
||||
|
||||
def analyze(self, market: str, symbol: str, language: str = 'en-US',
|
||||
model: str = None, timeframe: str = "1D") -> Dict[str, Any]:
|
||||
model: str = None, timeframe: str = "1D", user_id: int = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Run fast single-call analysis.
|
||||
|
||||
Args:
|
||||
market: Market type (Crypto, USStock, etc.)
|
||||
symbol: Trading pair or stock symbol
|
||||
language: Response language (zh-CN or en-US)
|
||||
model: LLM model to use
|
||||
timeframe: Analysis timeframe (1D, 4H, etc.)
|
||||
user_id: User ID for storing analysis history
|
||||
|
||||
Returns:
|
||||
Complete analysis result with actionable recommendations.
|
||||
"""
|
||||
@@ -587,11 +595,11 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_
|
||||
})
|
||||
|
||||
# Store in memory for future retrieval and get memory_id for feedback
|
||||
memory_id = self._store_analysis_memory(result)
|
||||
memory_id = self._store_analysis_memory(result, user_id=user_id)
|
||||
if memory_id:
|
||||
result["memory_id"] = memory_id
|
||||
|
||||
logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id})")
|
||||
logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id}, user_id={user_id})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fast analysis failed: {e}", exc_info=True)
|
||||
@@ -665,12 +673,12 @@ Provide your analysis now. Remember: all prices must be within 10% of ${current_
|
||||
|
||||
return max(0, min(100, int(overall)))
|
||||
|
||||
def _store_analysis_memory(self, result: Dict) -> Optional[int]:
|
||||
def _store_analysis_memory(self, result: Dict, user_id: int = None) -> Optional[int]:
|
||||
"""Store analysis result for future learning. Returns memory_id."""
|
||||
try:
|
||||
from app.services.analysis_memory import get_analysis_memory
|
||||
memory = get_analysis_memory()
|
||||
memory_id = memory.store(result)
|
||||
memory_id = memory.store(result, user_id=user_id)
|
||||
return memory_id
|
||||
except Exception as e:
|
||||
logger.warning(f"Memory storage failed: {e}")
|
||||
|
||||
@@ -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 []
|
||||
@@ -529,10 +529,19 @@ class PendingOrderWorker:
|
||||
(float(u["size"]), float(u["entry_price"]), int(u["id"]))
|
||||
)
|
||||
for ins in to_insert:
|
||||
# Get user_id from strategy
|
||||
ins_user_id = 1
|
||||
try:
|
||||
cur.execute("SELECT user_id FROM qd_strategies_trading WHERE id = %s", (int(ins["strategy_id"]),))
|
||||
strategy_row = cur.fetchone()
|
||||
if strategy_row and strategy_row.get("user_id"):
|
||||
ins_user_id = int(strategy_row["user_id"])
|
||||
except Exception:
|
||||
pass
|
||||
cur.execute(
|
||||
"""INSERT INTO qd_strategy_positions (user_id, strategy_id, symbol, side, size, entry_price, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, NOW())""",
|
||||
(1, int(ins["strategy_id"]), str(ins["symbol"]), str(ins["side"]), float(ins["size"]), float(ins["entry_price"]))
|
||||
(ins_user_id, int(ins["strategy_id"]), str(ins["symbol"]), str(ins["side"]), float(ins["size"]), float(ins["entry_price"]))
|
||||
)
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -636,6 +636,7 @@ CREATE INDEX IF NOT EXISTS idx_reflection_market ON qd_reflection_records(market
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qd_analysis_memory (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT, -- User who created this analysis (for filtering)
|
||||
market VARCHAR(50) NOT NULL,
|
||||
symbol VARCHAR(50) NOT NULL,
|
||||
decision VARCHAR(10) NOT NULL,
|
||||
@@ -662,6 +663,20 @@ CREATE TABLE IF NOT EXISTS qd_analysis_memory (
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol ON qd_analysis_memory(market, symbol);
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created ON qd_analysis_memory(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_validated ON qd_analysis_memory(validated_at) WHERE validated_at IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id);
|
||||
|
||||
-- Migration: Add user_id column to existing qd_analysis_memory table
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id'
|
||||
) THEN
|
||||
ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT;
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id);
|
||||
RAISE NOTICE 'Added user_id column to qd_analysis_memory';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 20. Migration: Add token_version for single-client login
|
||||
|
||||
@@ -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 <name> <type> <default> <description>
|
||||
```
|
||||
|
||||
| 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
|
||||
@@ -39,6 +125,8 @@ This document records version updates, new features, bug fixes, and database mig
|
||||
- Fixed A-share and H-share data fetching with multiple fallback sources
|
||||
- Fixed watchlist price batch fetch timeout handling
|
||||
- Fixed heatmap multi-language support for commodities and forex
|
||||
- **Fixed AI analysis history not filtered by user** - All users were seeing the same history records; now each user only sees their own analysis history
|
||||
- **Fixed "Missing Turnstile token" error when changing password** - Logged-in users no longer need Turnstile verification to request password change verification code
|
||||
|
||||
### 🎨 UI/UX Improvements
|
||||
- Reorganized left menu: Indicator Market moved below Indicator Analysis, Settings moved to bottom
|
||||
@@ -92,9 +180,21 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add user_id column for user-specific history filtering
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id'
|
||||
) THEN
|
||||
ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol ON qd_analysis_memory(market, symbol);
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created ON qd_analysis_memory(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_validated ON qd_analysis_memory(validated_at) WHERE validated_at IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user ON qd_analysis_memory(user_id);
|
||||
|
||||
-- 2. Indicator Purchase Records
|
||||
CREATE TABLE IF NOT EXISTS qd_indicator_purchases (
|
||||
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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