From c1dc3f3a7126f0135a9e46ec6e998f1545a5441b Mon Sep 17 00:00:00 2001 From: TIANHE Date: Tue, 6 Jan 2026 19:53:23 +0800 Subject: [PATCH] feat: Add indicator code verification API and UI - Add POST /api/indicator/verifyCode endpoint in Python backend. - Update IndicatorEditor.vue with Verify Code button and error modal. - Add i18n support for verification UI. - Update README files. --- backend_api_python/app/routes/indicator.py | 148 +++++++++++++++++- quantdinger_vue/src/locales/lang/en-US.js | 4 + quantdinger_vue/src/locales/lang/ja-JP.js | 4 + quantdinger_vue/src/locales/lang/ko-KR.js | 4 + quantdinger_vue/src/locales/lang/zh-CN.js | 4 + quantdinger_vue/src/locales/lang/zh-TW.js | 4 + .../components/IndicatorEditor.vue | 61 ++++++++ 7 files changed, 227 insertions(+), 2 deletions(-) diff --git a/backend_api_python/app/routes/indicator.py b/backend_api_python/app/routes/indicator.py index 7414ae3..433c927 100644 --- a/backend_api_python/app/routes/indicator.py +++ b/backend_api_python/app/routes/indicator.py @@ -14,9 +14,12 @@ import json import os import re import time +import traceback from typing import Any, Dict, List from flask import Blueprint, Response, jsonify, request +import pandas as pd +import numpy as np from app.utils.db import get_db_connection from app.utils.logger import get_logger @@ -80,6 +83,38 @@ def _row_to_indicator(row: Dict[str, Any], user_id: int) -> Dict[str, Any]: } +def _generate_mock_df(length=200): + """Generate mock K-line data for verification.""" + from datetime import datetime, timedelta + + dates = [datetime.now() - timedelta(minutes=i) for i in range(length)] + dates.reverse() + + # Random walk with trend + returns = np.random.normal(0, 0.002, length) + price_path = 10000 * np.exp(np.cumsum(returns)) + + close = price_path + high = close * (1 + np.abs(np.random.normal(0, 0.001, length))) + low = close * (1 - np.abs(np.random.normal(0, 0.001, length))) + open_p = close * (1 + np.random.normal(0, 0.001, length)) # Slight deviation from close + # Ensure High is highest and Low is lowest + high = np.maximum(high, np.maximum(open_p, close)) + low = np.minimum(low, np.minimum(open_p, close)) + + volume = np.abs(np.random.normal(100, 50, length)) * 1000 + + df = pd.DataFrame({ + 'time': [int(d.timestamp() * 1000) for d in dates], + 'open': open_p, + 'high': high, + 'low': low, + 'close': close, + 'volume': volume + }) + return df + + @indicator_bp.route("/getIndicators", methods=["POST"]) def get_indicators(): """ @@ -225,6 +260,117 @@ def delete_indicator(): return jsonify({"code": 0, "msg": str(e), "data": None}), 500 +@indicator_bp.route("/verifyCode", methods=["POST"]) +def verify_code(): + """ + Verify/Dry-run indicator code with mock data. + Checks for: + - Syntax errors + - Runtime errors + - Output format (must define 'output' dict) + """ + try: + data = request.get_json() or {} + code = data.get("code") or "" + + if not code or not str(code).strip(): + return jsonify({"code": 0, "msg": "Code is empty", "data": None}), 400 + + # 1. Generate mock data + df = _generate_mock_df() + + # 2. Prepare execution environment + exec_env = { + 'df': df.copy(), + 'pd': pd, + 'np': np, + 'output': None + } + + # 3. Execute code + try: + exec(code, exec_env) + except SyntaxError as e: + return jsonify({ + "code": 0, + "msg": f"Syntax Error at line {e.lineno}: {e.msg}", + "data": {"type": "SyntaxError", "line": e.lineno, "details": str(e)} + }) + except Exception as e: + # Capture traceback for better debugging + tb = traceback.format_exc() + # Extract the line number from the exec() call in the traceback if possible + # This is tricky because the traceback includes the backend frames. + # We'll just return the exception message. + return jsonify({ + "code": 0, + "msg": f"Runtime Error: {str(e)}", + "data": {"type": type(e).__name__, "details": tb} + }) + + # 4. Check output + output = exec_env.get('output') + + if output is None: + return jsonify({ + "code": 0, + "msg": "Missing 'output' variable. Your code must define an 'output' dictionary.", + "data": {"type": "MissingOutput"} + }) + + if not isinstance(output, dict): + return jsonify({ + "code": 0, + "msg": f"'output' must be a dictionary, got {type(output).__name__}", + "data": {"type": "InvalidOutputType"} + }) + + # Check required fields + if 'plots' not in output and 'signals' not in output: + return jsonify({ + "code": 0, + "msg": "'output' dict should contain 'plots' or 'signals' list.", + "data": {"type": "InvalidOutputStructure"} + }) + + # Basic check for lengths + plots = output.get('plots', []) + signals = output.get('signals', []) + + for p in plots: + if 'data' not in p: + return jsonify({"code": 0, "msg": f"Plot '{p.get('name')}' missing 'data' field.", "data": {"type": "InvalidPlot"}}) + if len(p['data']) != len(df): + return jsonify({ + "code": 0, + "msg": f"Plot '{p.get('name')}' data length ({len(p['data'])}) does not match DataFrame length ({len(df)}).", + "data": {"type": "LengthMismatch"} + }) + + for s in signals: + if 'data' not in s: + return jsonify({"code": 0, "msg": f"Signal '{s.get('type')}' missing 'data' field.", "data": {"type": "InvalidSignal"}}) + if len(s['data']) != len(df): + return jsonify({ + "code": 0, + "msg": f"Signal '{s.get('type')}' data length ({len(s['data'])}) does not match DataFrame length ({len(df)}).", + "data": {"type": "LengthMismatch"} + }) + + return jsonify({ + "code": 1, + "msg": "Verification passed! Code executed successfully.", + "data": { + "plots_count": len(plots), + "signals_count": len(signals) + } + }) + + except Exception as e: + logger.error(f"verify_code failed: {str(e)}", exc_info=True) + return jsonify({"code": 0, "msg": f"System Error: {str(e)}", "data": None}), 500 + + @indicator_bp.route("/aiGenerate", methods=["POST"]) def ai_generate(): """ @@ -429,5 +575,3 @@ IMPORTANT: Output Python code directly, without explanations, without descriptio "X-Accel-Buffering": "no", }, ) - - diff --git a/quantdinger_vue/src/locales/lang/en-US.js b/quantdinger_vue/src/locales/lang/en-US.js index 4ae4190..2c0bca1 100644 --- a/quantdinger_vue/src/locales/lang/en-US.js +++ b/quantdinger_vue/src/locales/lang/en-US.js @@ -621,6 +621,10 @@ const locale = { 'dashboard.indicator.editor.aiPromptRequired': 'Please enter your idea', 'dashboard.indicator.editor.aiGenerateSuccess': 'Code generated successfully', 'dashboard.indicator.editor.aiGenerateError': 'Code generation failed, please try again later', + 'dashboard.indicator.editor.verifyCode': 'Verify Code', + 'dashboard.indicator.editor.verifyCodeSuccess': 'Verification Passed', + 'dashboard.indicator.editor.verifyCodeFailed': 'Verification Failed', + 'dashboard.indicator.editor.verifyCodeEmpty': 'Code cannot be empty', 'dashboard.indicator.guide.title': 'Python Indicator & Strategy Development Guide', 'dashboard.indicator.guide.intro': 'This platform supports writing custom technical indicators and trading signals using Python. The system includes built-in pandas and numpy data analysis libraries. You can use standard DataFrame operations to process K-line data and draw charts or mark buy/sell signals.', 'dashboard.indicator.guide.section1.title': '1. Runtime Environment & Predefined Variables', diff --git a/quantdinger_vue/src/locales/lang/ja-JP.js b/quantdinger_vue/src/locales/lang/ja-JP.js index 4624f29..a2378c6 100644 --- a/quantdinger_vue/src/locales/lang/ja-JP.js +++ b/quantdinger_vue/src/locales/lang/ja-JP.js @@ -617,6 +617,10 @@ const locale = { 'dashboard.indicator.editor.aiPromptRequired': 'あなたの考えを入力してください', 'dashboard.indicator.editor.aiGenerateSuccess': 'コード生成が成功しました', 'dashboard.indicator.editor.aiGenerateError': 'コード生成に失敗しました。後でもう一度お試しください。', + 'dashboard.indicator.editor.verifyCode': 'コード検証', + 'dashboard.indicator.editor.verifyCodeSuccess': '検証合格', + 'dashboard.indicator.editor.verifyCodeFailed': '検証失敗', + 'dashboard.indicator.editor.verifyCodeEmpty': 'コードは空にできません', 'dashboard.indicator.backtest.title': 'インジケーターのバックテスト', 'dashboard.indicator.backtest.config': 'バックテストパラメータ', 'dashboard.indicator.backtest.startDate': '開始日', diff --git a/quantdinger_vue/src/locales/lang/ko-KR.js b/quantdinger_vue/src/locales/lang/ko-KR.js index ccf4d0f..8cffe23 100644 --- a/quantdinger_vue/src/locales/lang/ko-KR.js +++ b/quantdinger_vue/src/locales/lang/ko-KR.js @@ -616,6 +616,10 @@ const locale = { 'dashboard.indicator.editor.aiPromptRequired': '당신의 생각을 입력해주세요', 'dashboard.indicator.editor.aiGenerateSuccess': '코드 생성 성공', 'dashboard.indicator.editor.aiGenerateError': '코드 생성에 실패했습니다. 나중에 다시 시도해 주세요.', + 'dashboard.indicator.editor.verifyCode': '코드 검증', + 'dashboard.indicator.editor.verifyCodeSuccess': '검증 통과', + 'dashboard.indicator.editor.verifyCodeFailed': '검증 실패', + 'dashboard.indicator.editor.verifyCodeEmpty': '코드는 비워둘 수 없습니다', 'dashboard.indicator.backtest.title': '지표 백테스트', 'dashboard.indicator.backtest.config': '백테스트 매개변수', 'dashboard.indicator.backtest.startDate': '시작일', diff --git a/quantdinger_vue/src/locales/lang/zh-CN.js b/quantdinger_vue/src/locales/lang/zh-CN.js index 22cd46f..1457f2b 100644 --- a/quantdinger_vue/src/locales/lang/zh-CN.js +++ b/quantdinger_vue/src/locales/lang/zh-CN.js @@ -618,6 +618,10 @@ const locale = { 'dashboard.indicator.editor.aiPromptRequired': '请输入您的想法', 'dashboard.indicator.editor.aiGenerateSuccess': '代码生成成功', 'dashboard.indicator.editor.aiGenerateError': '代码生成失败,请稍后重试', +'dashboard.indicator.editor.verifyCode': '代码检查', +'dashboard.indicator.editor.verifyCodeSuccess': '代码检查通过', +'dashboard.indicator.editor.verifyCodeFailed': '代码检查未通过', +'dashboard.indicator.editor.verifyCodeEmpty': '代码不能为空', 'dashboard.indicator.boundary.message': '提示:指标脚本只负责“计算 + 绘图 + buy/sell 信号”;仓位、风控、加减仓、手续费/滑点属于策略执行配置。', 'dashboard.indicator.boundary.indicatorRule': "指标脚本请只输出 buy/sell(并设定 df['buy']/df['sell'])。不要在脚本内撰写仓位管理、止盈止损、加减仓。", 'dashboard.indicator.boundary.backtestRule': '规则:同一根K线若出现主信号(buy/sell→开/平仓/反手),本K线将跳过所有加仓与减仓。', diff --git a/quantdinger_vue/src/locales/lang/zh-TW.js b/quantdinger_vue/src/locales/lang/zh-TW.js index 7d3c075..3cb4aab 100644 --- a/quantdinger_vue/src/locales/lang/zh-TW.js +++ b/quantdinger_vue/src/locales/lang/zh-TW.js @@ -618,6 +618,10 @@ const locale = { 'dashboard.indicator.editor.aiPromptRequired': '請輸入您的想法', 'dashboard.indicator.editor.aiGenerateSuccess': '代碼生成成功', 'dashboard.indicator.editor.aiGenerateError': '代碼生成失敗,請稍後重試', + 'dashboard.indicator.editor.verifyCode': '代碼檢查', + 'dashboard.indicator.editor.verifyCodeSuccess': '代碼檢查通過', + 'dashboard.indicator.editor.verifyCodeFailed': '代碼檢查未通過', + 'dashboard.indicator.editor.verifyCodeEmpty': '代碼不能為空', 'dashboard.indicator.boundary.message': '提示:指標腳本只負責「計算 + 繪圖 + buy/sell 信號」;倉位、風控、加減倉、手續費/滑點屬於策略執行配置。', 'dashboard.indicator.boundary.indicatorRule': "指標腳本請只輸出 buy/sell(並設定 df['buy']/df['sell'])。不要在腳本內撰寫倉位管理、止盈止損、加減倉。", 'dashboard.indicator.boundary.backtestRule': '規則:同一根K線若出現主信號(buy/sell→開/平倉/反手),本K線將跳過所有加倉與減倉。', diff --git a/quantdinger_vue/src/views/indicator-analysis/components/IndicatorEditor.vue b/quantdinger_vue/src/views/indicator-analysis/components/IndicatorEditor.vue index 4f363a3..6731204 100644 --- a/quantdinger_vue/src/views/indicator-analysis/components/IndicatorEditor.vue +++ b/quantdinger_vue/src/views/indicator-analysis/components/IndicatorEditor.vue @@ -25,6 +25,16 @@ {{ $t('dashboard.indicator.editor.code') }}
+ + + {{ $t('dashboard.indicator.editor.verifyCode') }} + {{ $t('dashboard.indicator.editor.guide') }} @@ -105,6 +115,7 @@ import 'codemirror/addon/edit/matchbrackets' import 'codemirror/addon/selection/active-line' import storage from 'store' import { ACCESS_TOKEN } from '@/store/mutation-types' +import request from '@/utils/request' export default { name: 'IndicatorEditor', @@ -128,6 +139,7 @@ export default { codeEditor: null, aiPrompt: '', aiGenerating: false, + verifying: false, isMobile: false } }, @@ -393,6 +405,55 @@ export default { window.open('https://github.com/brokermr810/QuantDinger/blob/main/docs/STRATEGY_DEV_GUIDE.md', '_blank') }, + // 验证代码 + handleVerifyCode () { + const code = this.codeEditor ? this.codeEditor.getValue() : '' + if (!code || !code.trim()) { + this.$message.warning(this.$t('dashboard.indicator.editor.verifyCodeEmpty')) + return + } + + this.verifying = true + // 使用 request 工具(axios)发送请求,它会自动处理 baseURL 和 token + request({ + url: '/api/indicator/verifyCode', + method: 'post', + data: { code: code } + }).then(res => { + if (res.code === 1) { + const data = res.data || {} + this.$message.success(`${this.$t('dashboard.indicator.editor.verifyCodeSuccess')} (${data.plots_count || 0} plots, ${data.signals_count || 0} signals)`) + } else { + // 显示详细错误 + const errorData = res.data || {} + this.$error({ + title: this.$t('dashboard.indicator.editor.verifyCodeFailed'), + width: 600, + content: (h) => { + return h('div', [ + h('p', { style: { fontWeight: 'bold', color: '#ff4d4f' } }, res.msg), + errorData.details ? h('pre', { + style: { + background: '#f5f5f5', + padding: '8px', + overflow: 'auto', + maxHeight: '300px', + marginTop: '8px', + fontSize: '12px', + fontFamily: 'monospace' + } + }, errorData.details) : null + ]) + } + }) + } + }).catch(err => { + this.$message.error('Request Failed: ' + (err.message || 'Unknown Error')) + }).finally(() => { + this.verifying = false + }) + }, + // 清理代码中的 markdown 代码块标记 cleanMarkdownCodeBlocks (code) { if (!code || typeof code !== 'string') {