refactor(frontend): 实例选择改级联——策略右边实例下拉(用户设计反馈「实例凌驾策略之上」)——四表单(个股回测/组合回测/模拟盘/实盘)统一:策略下拉旁并排「实例档案(可选·带出参数)」下拉,选中策略后只列该策略的档案(类名→code_file映射,组合按文件名);换策略档案不属自动清空防绑错;移除原顶部大下拉;CTA级联映射缺失(mock中文名/自输类名)退化为全量;布局一行「策略/档案」层级心明(策略→实例从属);build绿+实例测试绿 [vps]
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getStrategies, getParams, getInstances, type StrategyItem } from '@/api/strategy'
|
||||
@@ -112,12 +112,31 @@ function onPickInstance(id: number | null): void {
|
||||
if (id != null) void applyInstance(id)
|
||||
}
|
||||
|
||||
// 级联:类名→文件名映射(实例按 code_file 归属策略)
|
||||
const classToFile = ref<Record<string, string>>({})
|
||||
const instancesOfStrategy = computed(() => {
|
||||
// 类名不在映射(mock 中文名/自输类名) → 不过滤退化为全量;有映射 → 严格级联
|
||||
const file = classToFile.value[form.strategy]
|
||||
return file ? instanceOptions.value.filter((i) => i.code_file === file) : instanceOptions.value
|
||||
})
|
||||
// 换策略后档案不属于它 → 清空选择(避免绑错档案)
|
||||
watch(() => form.strategy, () => {
|
||||
if (selectedInstanceId.value != null && !instancesOfStrategy.value.some((i) => i.id === selectedInstanceId.value)) {
|
||||
selectedInstanceId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
strategies.value = await getStrategies()
|
||||
try {
|
||||
instanceOptions.value = await getInstances()
|
||||
const [ins, fr] = await Promise.all([
|
||||
getInstances(),
|
||||
apiClient.get<{ files: FileLike[] }>('/strategy/files'),
|
||||
])
|
||||
instanceOptions.value = ins
|
||||
classToFile.value = Object.fromEntries(fr.data.files.map((f) => [f.class_name, f.name]))
|
||||
} catch {
|
||||
/* 档案下拉失败不阻塞 */
|
||||
}
|
||||
@@ -211,24 +230,22 @@ async function onSubmit(): Promise<void> {
|
||||
<el-card class="blk" shadow="never" v-loading="loading">
|
||||
<template #header><span class="section-title">策略与参数</span></template>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="实例档案">
|
||||
<el-form-item label="策略 / 档案">
|
||||
<el-select v-model="form.strategy" placeholder="选择策略" style="width: 300px">
|
||||
<el-option v-for="s in strategies" :key="s.name" :label="s.name" :value="s.name" />
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="选档案带出参数并回写结果(可选);不选=自由回测"
|
||||
style="width: 420px"
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instanceOptions" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.code_file || '—'}·${i.interval})`"
|
||||
v-for="i in instancesOfStrategy" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">已绑档案 #{{ selectedInstanceId }},完成后回写档案</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="策略">
|
||||
<el-select v-model="form.strategy" placeholder="选择策略" style="width: 320px">
|
||||
<el-option v-for="s in strategies" :key="s.name" :label="s.name" :value="s.name" />
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">完成后回写档案 #{{ selectedInstanceId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="paramsList.length" label="参数">
|
||||
<div class="param-grid">
|
||||
|
||||
@@ -411,6 +411,15 @@ onMounted(async () => {
|
||||
|
||||
// §12.6 选档案:跳转带入或页面下拉选择,提交带 instance_id 回写四格
|
||||
const instanceOptions = ref<{ id: number; name: string; code_file: string; interval: string }[]>([])
|
||||
// 级联:实例按 code_file 归属策略(form.strategy=文件名去 .py)
|
||||
const instancesOfStrategy = computed(() =>
|
||||
instanceOptions.value.filter((i) => i.code_file.replace(/\.py$/, '') === form.strategy),
|
||||
)
|
||||
watch(() => form.strategy, () => {
|
||||
if (selectedInstanceId.value != null && !instancesOfStrategy.value.some((i) => i.id === selectedInstanceId.value)) {
|
||||
selectedInstanceId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
function onPickInstance(id: number | null): void {
|
||||
selectedInstanceId.value = id
|
||||
@@ -480,30 +489,28 @@ function fmtNum(v: number | null | undefined, digits = 2): string {
|
||||
<el-card class="blk" shadow="never">
|
||||
<template #header><span class="section-title">策略与选股</span></template>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="实例档案">
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="选档案带出参数并回写结果(可选);不选=自由回测"
|
||||
style="width: 420px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instanceOptions" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.code_file || '—'}·${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">已绑档案 #{{ selectedInstanceId }},完成后回写档案</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标的池">
|
||||
<el-select v-model="form.pool" style="width: 320px">
|
||||
<el-option v-for="opt in poolOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<span class="muted form-hint">MVP 默认 HS300 子集(20-30 只),快速验证链路</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="策略">
|
||||
<el-select v-model="form.strategy" style="width: 320px">
|
||||
<el-form-item label="策略 / 档案">
|
||||
<el-select v-model="form.strategy" style="width: 300px">
|
||||
<el-option v-for="opt in strategyOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instancesOfStrategy" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">完成后回写档案 #{{ selectedInstanceId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="选股池上限">
|
||||
<el-input-number v-model="form.max_pool" :min="0" :step="10" :controls="false" style="width: 220px" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createLive, type LiveCreateRequest } from '@/api/live'
|
||||
@@ -34,13 +34,23 @@ const PORTFOLIO_LABELS = STRATEGY_LABELS
|
||||
// §12.6 选实例发起:选中即预填+绑定;不选=自由配置(后端发起即建档)
|
||||
const instanceOptions = ref<Instance[]>([])
|
||||
const selectedInstanceId = ref<number | null>(null)
|
||||
// 级联:类名→文件名映射(实例按 code_file 归属策略)
|
||||
const classToFile = ref<Record<string, string>>({})
|
||||
const instancesOfCta = computed(() => {
|
||||
const file = classToFile.value[form.value.strategy_class]
|
||||
return file ? instanceOptions.value.filter((i) => i.code_file === file) : instanceOptions.value
|
||||
})
|
||||
const instancesOfPortfolio = computed(() =>
|
||||
instanceOptions.value.filter((i) => i.code_file.replace(/\.py$/, '') === portfolioStrategy.value),
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await apiClient.get<{ files: { name: string; type: string }[] }>('/strategy/files')
|
||||
const { data } = await apiClient.get<{ files: { name: string; type: string; class_name: string }[] }>('/strategy/files')
|
||||
portfolioOptions.value = data.files
|
||||
.filter((f) => f.type === 'portfolio')
|
||||
.map((f) => f.name.replace(/\.py$/, ''))
|
||||
classToFile.value = Object.fromEntries(data.files.map((f) => [f.class_name, f.name]))
|
||||
} catch {
|
||||
|
||||
/* 下拉加载失败不阻塞表单 */
|
||||
@@ -71,6 +81,12 @@ const form = ref<LiveCreateRequest>({
|
||||
init_wait_sec: 60,
|
||||
mini_path: 'C:\\国金QMT交易端模拟\\userdata_mini',
|
||||
})
|
||||
// 换策略后档案不属于它 → 清空(避免绑错档案)。置 form 声明后(watch 源立即求值)
|
||||
watch([() => form.value.strategy_class, portfolioStrategy], () => {
|
||||
if (selectedInstanceId.value == null) return
|
||||
const pool = isPortfolio.value ? instancesOfPortfolio.value : instancesOfCta.value
|
||||
if (!pool.some((i) => i.id === selectedInstanceId.value)) selectedInstanceId.value = null
|
||||
})
|
||||
|
||||
const strategyClassOptions = [
|
||||
{ value: 'AShareDoubleMaStrategy', label: 'AShareDoubleMaStrategy(双均线 A 股策略)' },
|
||||
@@ -180,21 +196,6 @@ async function onSubmit(): Promise<void> {
|
||||
</div>
|
||||
|
||||
<el-form label-width="140px" style="margin-top:16px">
|
||||
<el-form-item label="实例档案">
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="选档案发起(推荐);不选=自由配置,提交后自动建档"
|
||||
style="width: 420px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instanceOptions" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.code_file || i.symbol_or_pool}·${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">已绑档案 #{{ selectedInstanceId }},提交用档案参数(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">策略库点「实盘」会带档案跳进来</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="实例名" required>
|
||||
<el-input v-model="form.name" placeholder="live-600000" style="width: 320px" />
|
||||
<span class="muted form-hint">页面显示用</span>
|
||||
@@ -219,8 +220,8 @@ async function onSubmit(): Promise<void> {
|
||||
<el-card class="blk" shadow="never">
|
||||
<template #header><span class="section-title">标的与策略</span></template>
|
||||
<el-form :model="form" label-width="140px">
|
||||
<el-form-item label="策略类">
|
||||
<el-select v-model="form.strategy_class" style="width: 360px">
|
||||
<el-form-item label="策略 / 档案">
|
||||
<el-select v-model="form.strategy_class" style="width: 300px">
|
||||
<el-option
|
||||
v-for="opt in strategyClassOptions"
|
||||
:key="opt.value"
|
||||
@@ -228,7 +229,19 @@ async function onSubmit(): Promise<void> {
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="muted form-hint">实盘需 A 股适配(定寸/禁空/分钟合成),目前仅 DoubleMa 完成适配,其余策略陆续扩展</span>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instancesOfCta" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span class="muted form-hint">实盘需 A 股适配,目前仅 DoubleMa 完成适配</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="策略实例名" required>
|
||||
<el-input v-model="form.strategy_name" placeholder="AShareDoubleMa_600000" style="width: 320px" />
|
||||
@@ -296,14 +309,26 @@ async function onSubmit(): Promise<void> {
|
||||
<template #header><span class="section-title">组合策略与选股</span></template>
|
||||
<el-form :model="poolForm" label-width="140px">
|
||||
<el-form-item label="组合策略">
|
||||
<el-select v-model="portfolioStrategy" style="width: 320px">
|
||||
<el-select v-model="portfolioStrategy" style="width: 280px">
|
||||
<el-option
|
||||
v-for="p in portfolioOptions" :key="p"
|
||||
:label="PORTFOLIO_LABELS[p] ? `${PORTFOLIO_LABELS[p]}(${p})` : p"
|
||||
:value="p"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="muted form-hint">supervisor 拉起 runner_live 子进程(bullet_trade LiveEngine)</span>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instancesOfPortfolio" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">supervisor 拉起 runner_live 子进程(bullet_trade LiveEngine)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标的池">
|
||||
<el-select v-model="poolForm.pool" style="width: 320px">
|
||||
|
||||
@@ -21,6 +21,12 @@ const strategyType = ref<'cta' | 'portfolio'>('cta')
|
||||
// §12.6 选实例发起:选中即预填+绑定;不选=自由配置(后端发起即建档)
|
||||
const instanceOptions = ref<Instance[]>([])
|
||||
const selectedInstanceId = ref<number | null>(null)
|
||||
// 级联:类名→文件名映射(实例按 code_file 归属策略)
|
||||
const classToFile = ref<Record<string, string>>({})
|
||||
const instancesOfCta = computed(() => {
|
||||
const file = classToFile.value[form.value.strategies[0]?.name]
|
||||
return file ? instanceOptions.value.filter((i) => i.code_file === file) : instanceOptions.value
|
||||
})
|
||||
const poolForm = reactive({ pool: 'hs300_subset', max_pool: 30, benchmark: '000300.XSHG' })
|
||||
const POOL_OPTIONS = [
|
||||
{ label: 'HS300 子集(小范围验证)', value: 'hs300_subset' },
|
||||
@@ -38,12 +44,13 @@ onMounted(async () => {
|
||||
try {
|
||||
const [strats, files] = await Promise.all([
|
||||
getStrategies(),
|
||||
apiClient.get<{ files: { name: string; type: string }[] }>('/strategy/files'),
|
||||
apiClient.get<{ files: { name: string; type: string; class_name: string }[] }>('/strategy/files'),
|
||||
])
|
||||
strategyOptions.value = strats
|
||||
portfolioOptions.value = files.data.files
|
||||
.filter((f) => f.type === 'portfolio')
|
||||
.map((f) => f.name.replace(/\.py$/, ''))
|
||||
classToFile.value = Object.fromEntries(files.data.files.map((f) => [f.class_name, f.name]))
|
||||
} catch {
|
||||
/* 下拉加载失败不阻塞表单 */
|
||||
}
|
||||
@@ -132,6 +139,15 @@ function onPickInstance(id: number | null): void {
|
||||
|
||||
const isPortfolio = computed(() => strategyType.value === 'portfolio')
|
||||
const portfolioStrategy = ref('all_weather')
|
||||
const instancesOfPortfolio = computed(() =>
|
||||
instanceOptions.value.filter((i) => i.code_file.replace(/\.py$/, '') === portfolioStrategy.value),
|
||||
)
|
||||
// 换策略后档案不属于它 → 清空(避免绑错档案)
|
||||
watch([() => form.value.strategies[0]?.name, portfolioStrategy], () => {
|
||||
if (selectedInstanceId.value == null) return
|
||||
const pool = strategyType.value === 'portfolio' ? instancesOfPortfolio.value : instancesOfCta.value
|
||||
if (!pool.some((i) => i.id === selectedInstanceId.value)) selectedInstanceId.value = null
|
||||
})
|
||||
|
||||
function setMode(m: string): void {
|
||||
// 组合类型不支持回放(历史回放走「组合回测」页)
|
||||
@@ -237,32 +253,27 @@ function onSymbols(v: string): void {
|
||||
|
||||
<el-card class="blk" shadow="never">
|
||||
<template #header><span class="section-title">{{ isPortfolio ? '组合策略与选股' : '标的与策略' }}</span></template>
|
||||
<el-form label-width="120px" style="margin-bottom: 4px">
|
||||
<el-form-item label="实例档案">
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="选档案发起(推荐);不选=自由配置,提交后自动建档"
|
||||
style="width: 420px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instanceOptions" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.code_file || i.symbol_or_pool}·${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">已绑档案 #{{ selectedInstanceId }},提交用档案参数(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">策略库点「实走/影子」会带档案跳进来</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-form v-if="isPortfolio" :model="poolForm" label-width="120px">
|
||||
<el-form-item label="组合策略">
|
||||
<el-select v-model="portfolioStrategy" style="width: 320px">
|
||||
<el-select v-model="portfolioStrategy" style="width: 280px">
|
||||
<el-option
|
||||
v-for="p in portfolioOptions" :key="p"
|
||||
:label="PORTFOLIO_LABELS[p] ? `${PORTFOLIO_LABELS[p]}(${p})` : p"
|
||||
:value="p"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instancesOfPortfolio" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标的池">
|
||||
<el-select v-model="poolForm.pool" style="width: 320px">
|
||||
@@ -313,11 +324,23 @@ function onSymbols(v: string): void {
|
||||
filterable
|
||||
allow-create
|
||||
placeholder="选择或输入策略类名"
|
||||
style="width: 320px"
|
||||
style="width: 280px"
|
||||
>
|
||||
<el-option v-for="s in strategyOptions" :key="s.name" :label="s.name" :value="s.name" />
|
||||
</el-select>
|
||||
<span class="muted form-hint">CTA 策略(个股),如 DoubleMaStrategy</span>
|
||||
<el-select
|
||||
:model-value="selectedInstanceId" clearable filterable
|
||||
placeholder="实例档案(可选·带出参数)"
|
||||
style="width: 260px; margin-left: 8px"
|
||||
@update:model-value="onPickInstance"
|
||||
>
|
||||
<el-option
|
||||
v-for="i in instancesOfCta" :key="i.id" :value="i.id"
|
||||
:label="`${i.name}(${i.interval})`"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="selectedInstanceId != null" class="muted form-hint">绑定档案 #{{ selectedInstanceId }}(发起时快照)</span>
|
||||
<span v-else class="muted form-hint">CTA 策略(个股),如 DoubleMaStrategy</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="撮合时点">
|
||||
<el-select v-model="form.strategies[0].match_session" style="width: 280px">
|
||||
|
||||
Reference in New Issue
Block a user