feat: indicator parameterization support for kline chart
This commit is contained in:
@@ -1044,7 +1044,7 @@ export default {
|
||||
// Use strategy_stats for pie chart data (shows all strategies, not just those with positions)
|
||||
const stats = Array.isArray(this.summary.strategy_stats) ? this.summary.strategy_stats : []
|
||||
const raw = Array.isArray(this.summary.strategy_pnl_chart) ? this.summary.strategy_pnl_chart : []
|
||||
|
||||
|
||||
// Prefer strategy_stats if available, fallback to strategy_pnl_chart
|
||||
let data = []
|
||||
if (stats.length > 0) {
|
||||
|
||||
@@ -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')">
|
||||
@@ -1603,6 +1648,8 @@ export default {
|
||||
loadingIndicators: false,
|
||||
availableIndicators: [],
|
||||
selectedIndicator: null,
|
||||
indicatorParams: [], // 指标参数声明
|
||||
indicatorParamValues: {}, // 用户设置的参数值
|
||||
cryptoSymbols: CRYPTO_SYMBOLS,
|
||||
// Watchlist symbols (same source as indicator-analysis page)
|
||||
loadingWatchlist: false,
|
||||
@@ -2359,7 +2406,17 @@ export default {
|
||||
this.form.setFieldsValue({
|
||||
indicator_id: finalId
|
||||
})
|
||||
this.handleIndicatorChange(finalId)
|
||||
await this.handleIndicatorChange(finalId)
|
||||
|
||||
// 恢复已保存的指标参数值
|
||||
const savedParams = strategy.trading_config?.indicator_params
|
||||
if (savedParams && typeof savedParams === 'object') {
|
||||
Object.keys(savedParams).forEach(key => {
|
||||
if (key in this.indicatorParamValues) {
|
||||
this.indicatorParamValues[key] = savedParams[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 如果找不到,仍然设置值,但可能显示为ID
|
||||
this.form.setFieldsValue({
|
||||
@@ -2795,9 +2852,30 @@ export default {
|
||||
this.loadingIndicators = false
|
||||
}
|
||||
},
|
||||
handleIndicatorChange (indicatorId) {
|
||||
async handleIndicatorChange (indicatorId) {
|
||||
const idStr = String(indicatorId)
|
||||
this.selectedIndicator = this.availableIndicators.find(ind => String(ind.id) === idStr)
|
||||
|
||||
// 获取指标参数声明
|
||||
this.indicatorParams = []
|
||||
this.indicatorParamValues = {}
|
||||
if (indicatorId) {
|
||||
try {
|
||||
const res = await this.$http.get('/api/indicator/getIndicatorParams', {
|
||||
params: { indicator_id: indicatorId }
|
||||
})
|
||||
// 响应拦截器已返回 response.data,所以直接访问 res.code 和 res.data
|
||||
if (res && res.code === 1 && Array.isArray(res.data)) {
|
||||
this.indicatorParams = res.data
|
||||
// 初始化参数值为默认值
|
||||
res.data.forEach(p => {
|
||||
this.indicatorParamValues[p.name] = p.default
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load indicator params:', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleMarketTypeChange (e) {
|
||||
const marketType = e.target.value
|
||||
@@ -3442,7 +3520,9 @@ export default {
|
||||
commission: values.commission || 0,
|
||||
slippage: values.slippage || 0,
|
||||
// AI智能决策过滤
|
||||
enable_ai_filter: enableAiFilter
|
||||
enable_ai_filter: enableAiFilter,
|
||||
// 指标参数(外部传递)
|
||||
indicator_params: this.indicatorParamValues
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4988,6 +5068,25 @@ export default {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.indicator-params-form {
|
||||
padding: 12px;
|
||||
background-color: var(--bg-color-secondary, #f5f7fa);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.indicator-params-form .param-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.indicator-params-form .param-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--text-color, #666);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-item-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user