feat(web-ui): 取行情整合 + 市场智能识别 + 一键寻优全策略

- 取消单标的/寻优页独立的「取行情」按钮,整合进「开始回测/开始寻测」
  (SymbolPicker 经 defineExpose 暴露 loadBars,父组件串联取数+回测)
- 取消市场手动选择(沪/深/北交所下拉),改为 market.ts 按代码段智能
  识别(17 边界用例验证),代码框旁显示识别结果
- 成交价下拉精简为中文「开盘价/收盘价」,初始资金默认 100 万
- 寻优页 ParamGridPicker 切换策略自动填入预设参数网格
- 寻优页新增「一键寻优所有策略」按钮 + 全局策略排名表
  (OptimizeView 复用 store.runOptimizeAll,结果区含最佳/排名/合计网格点)
- types/api/store 新增 OptimizeAll 契约 + submitOptimizeAllTask
- vue-tsc + vite build 通过
This commit is contained in:
Justin Gu
2026-07-04 00:09:05 +08:00
parent f6ae69845d
commit ecd8af1676
10 changed files with 449 additions and 89 deletions
+23 -4
View File
@@ -50,12 +50,28 @@ function onInput(name: string, val: string) {
syncOutputs()
}
// 切换策略时清空选择
// 把预设取值列表格式化为输入框文本(逗号分隔)
function presetToText(vals: Array<number | string>): string {
return vals.join(', ')
}
// 切换策略时:若有预设网格则自动勾选并填入预设取值,否则清空选择
watch(
() => props.strategy?.name,
() => {
selected.value = new Set()
inputs.value = {}
const preset = props.strategy?.preset_grid
if (preset && Object.keys(preset).length > 0) {
const names = Object.keys(preset)
selected.value = new Set(names)
const newInputs: Record<string, string> = {}
for (const n of names) {
newInputs[n] = presetToText(preset[n])
}
inputs.value = newInputs
} else {
selected.value = new Set()
inputs.value = {}
}
syncOutputs()
},
)
@@ -71,7 +87,10 @@ const gridPoints = computed(() => {
<template>
<div class="grid-picker">
<p class="hint">勾选 1-2 个参数寻优填入取值列表逗号分隔</p>
<p class="hint">
勾选 1-2 个参数寻优填入取值列表逗号分隔
切换策略会自动填入预设参数可直接编辑
</p>
<div v-for="p in strategy?.params" :key="p.name" class="param-row">
<label class="check">
<input
+19 -16
View File
@@ -1,19 +1,24 @@
<script setup lang="ts">
// 多标的输入(组合回测用)。逐个添加 市场:代码,可删除
// 多标的输入(组合回测用)。逐个添加 6 位代码,市场自动识别
// 删除手动市场选择(沪市/深市/北交所),由 detectMarket 智能匹配。
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { detectMarket, marketLabel } from '../market'
const props = defineProps<{
modelValue: string[]
}>()
const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
const market = ref('SZ')
const code = ref('')
const detectedMarket = computed(() => (code.value && /^\d{6}$/.test(code.value)
? marketLabel(detectMarket(code.value))
: ''))
function add() {
if (!/^\d{6}$/.test(code.value)) return
const sym = `${market.value}:${code.value}`
const sym = `${detectMarket(code.value)}:${code.value}`
if (!props.modelValue.includes(sym)) {
emit('update:modelValue', [...props.modelValue, sym])
}
@@ -27,20 +32,16 @@ function remove(sym: string) {
<template>
<div class="stocks-picker">
<div class="row">
<select v-model="market">
<option value="SZ">深市</option>
<option value="SH">沪市</option>
<option value="BJ">北交所</option>
</select>
<div class="row add-row">
<input
v-model="code"
maxlength="6"
placeholder="6位代码"
placeholder="6位代码(市场自动识别)"
@keyup.enter="add"
/>
<button @click="add">添加</button>
</div>
<p v-if="detectedMarket" class="market-hint">将识别为{{ detectedMarket }}</p>
<div v-if="modelValue.length" class="stock-list">
<span v-for="s in modelValue" :key="s" class="stock-tag">
@@ -53,16 +54,18 @@ function remove(sym: string) {
</template>
<style scoped>
.row {
.add-row {
display: flex;
gap: 6px;
}
.row select {
width: auto;
}
.row input {
.add-row input {
flex: 1;
}
.market-hint {
color: var(--text-dim);
font-size: 11px;
margin-top: 4px;
}
.stock-list {
display: flex;
flex-wrap: wrap;
+52 -33
View File
@@ -1,17 +1,18 @@
<script setup lang="ts">
// 选标的 + 取行情(按日期范围)。
// 选标的 + 配置日期范围(取行情由父组件在「开始回测/开始寻优」时触发)。
// 市场按 6 位代码智能识别,不再手动选择。
// 后端 /bars 仅支持 count(上限 800,约 3.2 年),固定拉满后前端按日期过滤。
// 默认:结束日=今天(最近交易日),开始日=3年前。
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { fetchBars, formatError } from '../api'
import { detectMarket, marketLabel } from '../market'
import { useBacktestStore } from '../stores/backtest'
import type { Category } from '../types'
const store = useBacktestStore()
const market = ref('SZ')
const code = ref('000001')
const category = ref<Category>('DAY')
@@ -24,27 +25,38 @@ function isoDaysFromNow(days: number): string {
const endDate = ref(isoDaysFromNow(0))
const startDate = ref(isoDaysFromNow(-365 * 3))
const loading = ref(false)
const error = ref('')
// loading 由父组件控制(回测/寻优时驱动),组件自身只暴露 loadBars
const loading = ref(false)
const CATEGORIES: Category[] = ['DAY', 'WEEK', 'MONTH', 'MIN_5', 'MIN_15', 'MIN_30', 'MIN_60']
async function loadBars() {
// 智能识别的市场(用于提示展示)
const detectedMarket = computed(() => (code.value && /^\d{6}$/.test(code.value)
? marketLabel(detectMarket(code.value))
: ''))
/** 取行情(由父组件在点击「开始回测/开始寻优」时调用)。
* 成功返回 true,失败返回 false(并把错误写入 store.error 供父组件感知)。 */
async function loadBars(): Promise<boolean> {
// 基本校验
if (!/^\d{6}$/.test(code.value)) {
error.value = '股票代码必须是 6 位数字'
return
store.error = error.value
return false
}
if (startDate.value >= endDate.value) {
error.value = '开始日期必须早于结束日期'
return
store.error = error.value
return false
}
loading.value = true
error.value = ''
try {
const market = detectMarket(code.value)
const bars = await fetchBars(
market.value,
market,
code.value,
category.value,
startDate.value,
@@ -52,39 +64,36 @@ async function loadBars() {
)
if (bars.length < 2) {
error.value = `该日期范围内仅取到 ${bars.length} 根 K 线,不足以回测`
return
store.error = error.value
return false
}
const range = `${startDate.value} ~ ${endDate.value}`
store.setOhlcv(bars, `${market.value}:${code.value} ${category.value} ${range}`)
store.setOhlcv(bars, `${market}:${code.value} ${category.value} ${range}`)
store.clearResult()
return true
} catch (e) {
error.value = formatError(e)
store.error = error.value
return false
} finally {
loading.value = false
}
}
// 暴露给父组件(BacktestView / OptimizeView)在「开始回测/寻优」时串联调用
defineExpose({ loadBars, loading })
</script>
<template>
<div class="symbol-picker">
<div class="row">
<div class="field">
<label>市场</label>
<select v-model="market">
<option value="SZ">深市</option>
<option value="SH">沪市</option>
<option value="BJ">北交所</option>
</select>
</div>
<div class="field code-field">
<label>代码</label>
<input
v-model="code"
maxlength="6"
placeholder="6位代码"
@keyup.enter="loadBars"
/>
</div>
<div class="field code-field">
<label>代码</label>
<input
v-model="code"
maxlength="6"
placeholder="6位代码(市场自动识别)"
/>
<span v-if="detectedMarket" class="market-tag">{{ detectedMarket }}</span>
</div>
<div class="field">
@@ -105,10 +114,6 @@ async function loadBars() {
</div>
</div>
<button class="primary" :disabled="loading" @click="loadBars">
{{ loading ? '取行情中' : '取行情' }}
</button>
<p v-if="error" class="err">{{ error }}</p>
<p v-if="store.barsSource" class="ok">
已加载{{ store.barsSource }}{{ store.ohlcv.length }}
@@ -118,7 +123,21 @@ async function loadBars() {
<style scoped>
.code-field {
flex: 2;
position: relative;
}
.code-field input {
padding-right: 70px;
}
.market-tag {
position: absolute;
right: 8px;
bottom: 8px;
font-size: 11px;
color: var(--text-dim);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: 1px 6px;
border-radius: 3px;
}
.err {
color: var(--up);