From f0a5af595dd4bf3e80bcd990d59afbc122b3b00f Mon Sep 17 00:00:00 2001 From: TIANHE Date: Fri, 13 Feb 2026 02:29:12 +0800 Subject: [PATCH] fix: Fix indicator parameter configuration and Docker build issues - Fix indicator parameter values being reset after modification in indicator-analysis page - Fix Docker build nginx image pull failure (use specific version tag) - Add Docker build scripts (PowerShell and Bash versions) - Revert call_indicator feature (temporarily removed to avoid affecting existing indicator execution) --- backend_api_python/app/routes/indicator.py | 91 +++++++++++++++++++ docker-compose.yml | 2 + quantdinger_vue/Dockerfile | 3 +- quantdinger_vue/build-docker.ps1 | 27 ++++++ quantdinger_vue/build-docker.sh | 21 +++++ .../src/views/indicator-analysis/index.vue | 82 ++++++++++++++++- 6 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 quantdinger_vue/build-docker.ps1 create mode 100644 quantdinger_vue/build-docker.sh diff --git a/backend_api_python/app/routes/indicator.py b/backend_api_python/app/routes/indicator.py index b9a2a26..a75a39c 100644 --- a/backend_api_python/app/routes/indicator.py +++ b/backend_api_python/app/routes/indicator.py @@ -24,6 +24,7 @@ import numpy as np from app.utils.db import get_db_connection from app.utils.logger import get_logger from app.utils.auth import login_required +from app.services.indicator_params import IndicatorCaller import requests logger = get_logger(__name__) @@ -659,3 +660,93 @@ IMPORTANT: Output Python code directly, without explanations, without descriptio "X-Accel-Buffering": "no", }, ) + + +@indicator_bp.route("/callIndicator", methods=["POST"]) +@login_required +def call_indicator(): + """ + 调用另一个指标(供前端 Pyodide 环境使用) + + POST /api/indicator/callIndicator + Body: { + "indicatorRef": int | str, # 指标ID或名称 + "klineData": List[Dict], # K线数据 + "params": Dict, # 传递给被调用指标的参数(可选) + "currentIndicatorId": int # 当前指标ID(用于循环依赖检测,可选) + } + + Returns: + { + "code": 1, + "data": { + "df": List[Dict], # 执行后的DataFrame(转换为JSON) + "columns": List[str] # DataFrame的列名 + } + } + """ + try: + data = request.get_json() or {} + indicator_ref = data.get("indicatorRef") + kline_data = data.get("klineData", []) + params = data.get("params") or {} + current_indicator_id = data.get("currentIndicatorId") + + if not indicator_ref: + return jsonify({ + "code": 0, + "msg": "indicatorRef is required", + "data": None + }), 400 + + if not kline_data or not isinstance(kline_data, list): + return jsonify({ + "code": 0, + "msg": "klineData must be a non-empty list", + "data": None + }), 400 + + # 获取用户ID + user_id = g.user_id + + # 创建 IndicatorCaller + indicator_caller = IndicatorCaller(user_id, current_indicator_id) + + # 将前端传入的K线数据转换为DataFrame + df = pd.DataFrame(kline_data) + + # 确保必要的列存在 + required_columns = ['open', 'high', 'low', 'close', 'volume'] + for col in required_columns: + if col not in df.columns: + df[col] = 0.0 + + # 转换数据类型 + df['open'] = df['open'].astype('float64') + df['high'] = df['high'].astype('float64') + df['low'] = df['low'].astype('float64') + df['close'] = df['close'].astype('float64') + df['volume'] = df['volume'].astype('float64') + + # 调用指标 + result_df = indicator_caller.call_indicator(indicator_ref, df, params) + + # 将DataFrame转换为JSON格式(前端可以使用的格式) + result_dict = result_df.to_dict(orient='records') + + return jsonify({ + "code": 1, + "msg": "success", + "data": { + "df": result_dict, + "columns": list(result_df.columns) + } + }) + + except Exception as e: + logger.error(f"Error calling indicator: {e}", exc_info=True) + return jsonify({ + "code": 0, + "msg": str(e), + "data": None + }), 500 diff --git a/docker-compose.yml b/docker-compose.yml index 4d8036d..2a62a60 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,6 +66,8 @@ services: build: context: ./quantdinger_vue dockerfile: Dockerfile + # Force pull from official registry to avoid mirror issues + pull: true container_name: quantdinger-frontend restart: unless-stopped ports: diff --git a/quantdinger_vue/Dockerfile b/quantdinger_vue/Dockerfile index 25a385a..659385a 100644 --- a/quantdinger_vue/Dockerfile +++ b/quantdinger_vue/Dockerfile @@ -17,7 +17,8 @@ COPY . . RUN npm run build # Stage 2: Production image (using nginx) -FROM nginx:alpine +# Use specific version to avoid mirror registry issues +FROM nginx:1.25-alpine # Copy build artifacts COPY --from=builder /app/dist /usr/share/nginx/html diff --git a/quantdinger_vue/build-docker.ps1 b/quantdinger_vue/build-docker.ps1 new file mode 100644 index 0000000..7c4f4a5 --- /dev/null +++ b/quantdinger_vue/build-docker.ps1 @@ -0,0 +1,27 @@ +# PowerShell script for building Docker image with registry fallback + +Write-Host "Building frontend Docker image..." -ForegroundColor Green + +# Try to build with --pull flag to force pull from official registry +$buildResult = docker build ` + --pull ` + --platform linux/amd64 ` + -t quantdinger-frontend:latest ` + -f Dockerfile ` + . + +if ($LASTEXITCODE -ne 0) { + Write-Host "Build failed, trying with no-cache..." -ForegroundColor Yellow + docker build ` + --no-cache ` + --platform linux/amd64 ` + -t quantdinger-frontend:latest ` + -f Dockerfile ` + . +} + +if ($LASTEXITCODE -eq 0) { + Write-Host "Build successful!" -ForegroundColor Green +} else { + Write-Host "Build failed. Please check Docker registry configuration." -ForegroundColor Red +} diff --git a/quantdinger_vue/build-docker.sh b/quantdinger_vue/build-docker.sh new file mode 100644 index 0000000..d7131a6 --- /dev/null +++ b/quantdinger_vue/build-docker.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Docker build script with registry fallback + +# Try to build with --pull flag to force pull from official registry +echo "Building frontend Docker image..." +docker build \ + --pull \ + --platform linux/amd64 \ + -t quantdinger-frontend:latest \ + -f Dockerfile \ + . + +if [ $? -ne 0 ]; then + echo "Build failed, trying with no-cache..." + docker build \ + --no-cache \ + --platform linux/amd64 \ + -t quantdinger-frontend:latest \ + -f Dockerfile \ + . +fi diff --git a/quantdinger_vue/src/views/indicator-analysis/index.vue b/quantdinger_vue/src/views/indicator-analysis/index.vue index a447568..26214bb 100644 --- a/quantdinger_vue/src/views/indicator-analysis/index.vue +++ b/quantdinger_vue/src/views/indicator-analysis/index.vue @@ -392,6 +392,7 @@ :confirmLoading="loadingParams" @ok="confirmIndicatorParams" @cancel="cancelIndicatorParams" + @afterClose="handleParamsModalAfterClose" :width="500" :maskClosable="false" :keyboard="false" @@ -746,6 +747,8 @@ export default { const indicatorParams = ref([]) // 指标参数声明 const indicatorParamValues = ref({}) // 用户设置的参数值 const loadingParams = ref(false) + // 保存每个指标的参数值(key: indicatorId, value: { paramName: paramValue }) + const savedIndicatorParams = ref({}) // 折叠状态 const customSectionCollapsed = ref(false) // 我创建的指标区域是否折叠 @@ -1416,10 +1419,34 @@ export default { if (res && res.code === 1 && Array.isArray(res.data) && res.data.length > 0) { // 有参数,显示配置弹窗 indicatorParams.value = res.data - indicatorParamValues.value = {} + // 获取指标的唯一标识(用于保存参数值) + const indicatorKey = `${source}-${indicator.id}` + // 先检查是否有保存的参数值 + const savedParams = savedIndicatorParams.value[indicatorKey] + // 先清空,然后逐个设置,确保响应式正常工作 + const newParamValues = {} res.data.forEach(p => { - indicatorParamValues.value[p.name] = p.default + // 如果有保存的值,使用保存的值;否则使用默认值 + // 需要处理类型转换 + let value = savedParams && savedParams[p.name] !== undefined + ? savedParams[p.name] + : p.default + + // 根据参数类型进行类型转换 + if (p.type === 'int') { + value = parseInt(value) || 0 + } else if (p.type === 'float') { + value = parseFloat(value) || 0.0 + } else if (p.type === 'bool') { + value = value === true || value === 'true' || value === 1 || value === '1' + } else { + value = value || '' + } + + newParamValues[p.name] = value }) + // 一次性设置所有值,确保响应式更新 + indicatorParamValues.value = newParamValues pendingIndicator.value = indicator pendingSource.value = source showParamsModal.value = true @@ -1440,6 +1467,10 @@ export default { // 确认参数配置并运行指标 const confirmIndicatorParams = () => { if (pendingIndicator.value) { + // 保存参数值(用于下次打开时使用) + const indicatorKey = `${pendingSource.value}-${pendingIndicator.value.id}` + savedIndicatorParams.value[indicatorKey] = { ...indicatorParamValues.value } + // 将参数传递给指标 const indicatorWithParams = { ...pendingIndicator.value, @@ -1454,9 +1485,29 @@ export default { // 取消参数配置 const cancelIndicatorParams = () => { + // 保存参数值(在关闭前保存) + saveCurrentParams() showParamsModal.value = false - pendingIndicator.value = null - pendingSource.value = '' + // 延迟清空,确保 afterClose 能访问到数据 + setTimeout(() => { + pendingIndicator.value = null + pendingSource.value = '' + }, 100) + } + + // 保存当前参数值 + const saveCurrentParams = () => { + if (pendingIndicator.value && pendingSource.value) { + const indicatorKey = `${pendingSource.value}-${pendingIndicator.value.id}` + // 深拷贝参数值,确保保存的是当前值 + savedIndicatorParams.value[indicatorKey] = JSON.parse(JSON.stringify(indicatorParamValues.value)) + } + } + + // 弹窗关闭后的处理 + const handleParamsModalAfterClose = () => { + // 确保参数值已保存 + saveCurrentParams() } // 运行指标代码(从编辑器) @@ -1891,6 +1942,28 @@ export default { } }) + // 监听参数值变化,实时保存 + watch( + () => indicatorParamValues.value, + (newVal) => { + // 只有在弹窗打开且有 pendingIndicator 时才保存 + if (showParamsModal.value && pendingIndicator.value && pendingSource.value) { + const indicatorKey = `${pendingSource.value}-${pendingIndicator.value.id}` + // 深拷贝保存,避免引用问题 + savedIndicatorParams.value[indicatorKey] = JSON.parse(JSON.stringify(newVal)) + } + }, + { deep: true, immediate: false } + ) + + // 监听参数配置弹窗关闭 + watch(showParamsModal, (newVal) => { + if (!newVal) { + // 弹窗关闭时,确保参数值已保存 + saveCurrentParams() + } + }) + return { userId, klineChart, @@ -1955,6 +2028,7 @@ getMarketColor, loadingParams, confirmIndicatorParams, cancelIndicatorParams, + handleParamsModalAfterClose, // 回测相关 showBacktestModal, backtestIndicator,