mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 15:44:15 +08:00
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:
@@ -7,6 +7,7 @@ import type {
|
|||||||
BacktestResult,
|
BacktestResult,
|
||||||
Bar,
|
Bar,
|
||||||
Category,
|
Category,
|
||||||
|
OptimizeAllBacktestRequest,
|
||||||
OptimizeBacktestRequest,
|
OptimizeBacktestRequest,
|
||||||
PortfolioBacktestRequest,
|
PortfolioBacktestRequest,
|
||||||
TaskListResponse,
|
TaskListResponse,
|
||||||
@@ -160,6 +161,19 @@ export async function submitOptimizeTask(
|
|||||||
return (await resp.json()) as TaskSubmitResponse
|
return (await resp.json()) as TaskSubmitResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提交「一键寻优所有策略」后台任务,返回 task_id。 */
|
||||||
|
export async function submitOptimizeAllTask(
|
||||||
|
req: OptimizeAllBacktestRequest,
|
||||||
|
): Promise<TaskSubmitResponse> {
|
||||||
|
const resp = await fetch(`${BASE}/backtest/optimize-all/run/async`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(req),
|
||||||
|
})
|
||||||
|
if (!resp.ok) await throwError(resp)
|
||||||
|
return (await resp.json()) as TaskSubmitResponse
|
||||||
|
}
|
||||||
|
|
||||||
/** 查询后台任务状态(轮询用)。 */
|
/** 查询后台任务状态(轮询用)。 */
|
||||||
export async function fetchTask(taskId: string): Promise<TaskState> {
|
export async function fetchTask(taskId: string): Promise<TaskState> {
|
||||||
const resp = await fetch(`${BASE}/backtest/tasks/${taskId}`)
|
const resp = await fetch(`${BASE}/backtest/tasks/${taskId}`)
|
||||||
|
|||||||
@@ -50,12 +50,28 @@ function onInput(name: string, val: string) {
|
|||||||
syncOutputs()
|
syncOutputs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换策略时清空选择
|
// 把预设取值列表格式化为输入框文本(逗号分隔)
|
||||||
|
function presetToText(vals: Array<number | string>): string {
|
||||||
|
return vals.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换策略时:若有预设网格则自动勾选并填入预设取值,否则清空选择
|
||||||
watch(
|
watch(
|
||||||
() => props.strategy?.name,
|
() => props.strategy?.name,
|
||||||
() => {
|
() => {
|
||||||
selected.value = new Set()
|
const preset = props.strategy?.preset_grid
|
||||||
inputs.value = {}
|
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()
|
syncOutputs()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -71,7 +87,10 @@ const gridPoints = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid-picker">
|
<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">
|
<div v-for="p in strategy?.params" :key="p.name" class="param-row">
|
||||||
<label class="check">
|
<label class="check">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 多标的输入(组合回测用)。逐个添加 市场:代码,可删除。
|
// 多标的输入(组合回测用)。逐个添加 6 位代码,市场自动识别。
|
||||||
|
// 删除手动市场选择(沪市/深市/北交所),由 detectMarket 智能匹配。
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { detectMarket, marketLabel } from '../market'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string[]
|
modelValue: string[]
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
|
||||||
|
|
||||||
const market = ref('SZ')
|
|
||||||
const code = ref('')
|
const code = ref('')
|
||||||
|
const detectedMarket = computed(() => (code.value && /^\d{6}$/.test(code.value)
|
||||||
|
? marketLabel(detectMarket(code.value))
|
||||||
|
: ''))
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
if (!/^\d{6}$/.test(code.value)) return
|
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)) {
|
if (!props.modelValue.includes(sym)) {
|
||||||
emit('update:modelValue', [...props.modelValue, sym])
|
emit('update:modelValue', [...props.modelValue, sym])
|
||||||
}
|
}
|
||||||
@@ -27,20 +32,16 @@ function remove(sym: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="stocks-picker">
|
<div class="stocks-picker">
|
||||||
<div class="row">
|
<div class="row add-row">
|
||||||
<select v-model="market">
|
|
||||||
<option value="SZ">深市</option>
|
|
||||||
<option value="SH">沪市</option>
|
|
||||||
<option value="BJ">北交所</option>
|
|
||||||
</select>
|
|
||||||
<input
|
<input
|
||||||
v-model="code"
|
v-model="code"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
placeholder="6位代码"
|
placeholder="6位代码(市场自动识别)"
|
||||||
@keyup.enter="add"
|
@keyup.enter="add"
|
||||||
/>
|
/>
|
||||||
<button @click="add">添加</button>
|
<button @click="add">添加</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="detectedMarket" class="market-hint">将识别为:{{ detectedMarket }}</p>
|
||||||
|
|
||||||
<div v-if="modelValue.length" class="stock-list">
|
<div v-if="modelValue.length" class="stock-list">
|
||||||
<span v-for="s in modelValue" :key="s" class="stock-tag">
|
<span v-for="s in modelValue" :key="s" class="stock-tag">
|
||||||
@@ -53,16 +54,18 @@ function remove(sym: string) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.row {
|
.add-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.row select {
|
.add-row input {
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.row input {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.market-hint {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
.stock-list {
|
.stock-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 选标的 + 取行情(按日期范围)。
|
// 选标的 + 配置日期范围(取行情由父组件在「开始回测/开始寻优」时触发)。
|
||||||
|
// 市场按 6 位代码智能识别,不再手动选择。
|
||||||
// 后端 /bars 仅支持 count(上限 800,约 3.2 年),固定拉满后前端按日期过滤。
|
// 后端 /bars 仅支持 count(上限 800,约 3.2 年),固定拉满后前端按日期过滤。
|
||||||
// 默认:结束日=今天(最近交易日),开始日=3年前。
|
// 默认:结束日=今天(最近交易日),开始日=3年前。
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchBars, formatError } from '../api'
|
import { fetchBars, formatError } from '../api'
|
||||||
|
import { detectMarket, marketLabel } from '../market'
|
||||||
import { useBacktestStore } from '../stores/backtest'
|
import { useBacktestStore } from '../stores/backtest'
|
||||||
import type { Category } from '../types'
|
import type { Category } from '../types'
|
||||||
|
|
||||||
const store = useBacktestStore()
|
const store = useBacktestStore()
|
||||||
|
|
||||||
const market = ref('SZ')
|
|
||||||
const code = ref('000001')
|
const code = ref('000001')
|
||||||
const category = ref<Category>('DAY')
|
const category = ref<Category>('DAY')
|
||||||
|
|
||||||
@@ -24,27 +25,38 @@ function isoDaysFromNow(days: number): string {
|
|||||||
const endDate = ref(isoDaysFromNow(0))
|
const endDate = ref(isoDaysFromNow(0))
|
||||||
const startDate = ref(isoDaysFromNow(-365 * 3))
|
const startDate = ref(isoDaysFromNow(-365 * 3))
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
// loading 由父组件控制(回测/寻优时驱动),组件自身只暴露 loadBars
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
const CATEGORIES: Category[] = ['DAY', 'WEEK', 'MONTH', 'MIN_5', 'MIN_15', 'MIN_30', 'MIN_60']
|
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)) {
|
if (!/^\d{6}$/.test(code.value)) {
|
||||||
error.value = '股票代码必须是 6 位数字'
|
error.value = '股票代码必须是 6 位数字'
|
||||||
return
|
store.error = error.value
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
if (startDate.value >= endDate.value) {
|
if (startDate.value >= endDate.value) {
|
||||||
error.value = '开始日期必须早于结束日期'
|
error.value = '开始日期必须早于结束日期'
|
||||||
return
|
store.error = error.value
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
|
const market = detectMarket(code.value)
|
||||||
const bars = await fetchBars(
|
const bars = await fetchBars(
|
||||||
market.value,
|
market,
|
||||||
code.value,
|
code.value,
|
||||||
category.value,
|
category.value,
|
||||||
startDate.value,
|
startDate.value,
|
||||||
@@ -52,39 +64,36 @@ async function loadBars() {
|
|||||||
)
|
)
|
||||||
if (bars.length < 2) {
|
if (bars.length < 2) {
|
||||||
error.value = `该日期范围内仅取到 ${bars.length} 根 K 线,不足以回测`
|
error.value = `该日期范围内仅取到 ${bars.length} 根 K 线,不足以回测`
|
||||||
return
|
store.error = error.value
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
const range = `${startDate.value} ~ ${endDate.value}`
|
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()
|
store.clearResult()
|
||||||
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = formatError(e)
|
error.value = formatError(e)
|
||||||
|
store.error = error.value
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暴露给父组件(BacktestView / OptimizeView)在「开始回测/寻优」时串联调用
|
||||||
|
defineExpose({ loadBars, loading })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="symbol-picker">
|
<div class="symbol-picker">
|
||||||
<div class="row">
|
<div class="field code-field">
|
||||||
<div class="field">
|
<label>代码</label>
|
||||||
<label>市场</label>
|
<input
|
||||||
<select v-model="market">
|
v-model="code"
|
||||||
<option value="SZ">深市</option>
|
maxlength="6"
|
||||||
<option value="SH">沪市</option>
|
placeholder="6位代码(市场自动识别)"
|
||||||
<option value="BJ">北交所</option>
|
/>
|
||||||
</select>
|
<span v-if="detectedMarket" class="market-tag">{{ detectedMarket }}</span>
|
||||||
</div>
|
|
||||||
<div class="field code-field">
|
|
||||||
<label>代码</label>
|
|
||||||
<input
|
|
||||||
v-model="code"
|
|
||||||
maxlength="6"
|
|
||||||
placeholder="6位代码"
|
|
||||||
@keyup.enter="loadBars"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -105,10 +114,6 @@ async function loadBars() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="primary" :disabled="loading" @click="loadBars">
|
|
||||||
{{ loading ? '取行情中…' : '取行情' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p v-if="error" class="err">{{ error }}</p>
|
<p v-if="error" class="err">{{ error }}</p>
|
||||||
<p v-if="store.barsSource" class="ok">
|
<p v-if="store.barsSource" class="ok">
|
||||||
已加载:{{ store.barsSource }}({{ store.ohlcv.length }} 根)
|
已加载:{{ store.barsSource }}({{ store.ohlcv.length }} 根)
|
||||||
@@ -118,7 +123,21 @@ async function loadBars() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.code-field {
|
.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 {
|
.err {
|
||||||
color: var(--up);
|
color: var(--up);
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// A股代码 → 市场智能识别。
|
||||||
|
// 用户只输入 6 位代码,按代码段规则自动匹配 沪市(SH)/深市(SZ)/北交所(BJ),
|
||||||
|
// 拼成后端要求的 "市场:代码" 格式(如 SZ:000001)。
|
||||||
|
|
||||||
|
export type Market = 'SH' | 'SZ' | 'BJ'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 6 位股票代码智能判断所属市场。
|
||||||
|
*
|
||||||
|
* 规则(按优先级,先匹配到的为准):
|
||||||
|
* - 北交所(BJ):43/83/87/92/93/920(小盘/三板)或 4xx/8xx 开头
|
||||||
|
* - 沪市(SH) :6/9 开头(主板 60/68 科创、B 股 900)或 5 开头(基金 50/51/56/58)
|
||||||
|
* - 其余归深市(SZ):000/001/002/003/300/301 创业板、200 B股 等
|
||||||
|
*
|
||||||
|
* @param code 6 位股票代码(纯数字)
|
||||||
|
* @returns 市场代码 SH/SZ/BJ;无法判断时默认深市(覆盖面最广)
|
||||||
|
*/
|
||||||
|
export function detectMarket(code: string): Market {
|
||||||
|
const c = code.trim()
|
||||||
|
if (!/^\d{6}$/.test(c)) return 'SZ'
|
||||||
|
|
||||||
|
// 北交所:43/83/87/92(含920段)/93 + 4xx/8xx(三板/小盘)
|
||||||
|
if (/^(43|83|87|92|93|4|8)/.test(c)) return 'BJ'
|
||||||
|
|
||||||
|
// 沪市:6xx(主板/科创板 60/68)、9xx(B股)、5xx(沪市基金 50/51/56/58/50ETF 等)
|
||||||
|
if (/^[695]/.test(c)) return 'SH'
|
||||||
|
|
||||||
|
// 其余归深市:000/001/002/003/300/301/200 等
|
||||||
|
return 'SZ'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 6 位代码转成后端要求的 "市场:代码" 格式。
|
||||||
|
* @param code 6 位股票代码
|
||||||
|
*/
|
||||||
|
export function toSymbol(code: string): string {
|
||||||
|
return `${detectMarket(code)}:${code.trim()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 市场中文显示名。 */
|
||||||
|
export function marketLabel(market: Market): string {
|
||||||
|
switch (market) {
|
||||||
|
case 'SH':
|
||||||
|
return '沪市'
|
||||||
|
case 'BJ':
|
||||||
|
return '北交所'
|
||||||
|
default:
|
||||||
|
return '深市'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
formatError,
|
formatError,
|
||||||
runBacktest,
|
runBacktest,
|
||||||
submitPortfolioTask,
|
submitPortfolioTask,
|
||||||
|
submitOptimizeAllTask,
|
||||||
submitOptimizeTask,
|
submitOptimizeTask,
|
||||||
fetchTask,
|
fetchTask,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
@@ -18,6 +19,8 @@ import type {
|
|||||||
Bar,
|
Bar,
|
||||||
PortfolioBacktestRequest,
|
PortfolioBacktestRequest,
|
||||||
PortfolioResult,
|
PortfolioResult,
|
||||||
|
OptimizeAllBacktestRequest,
|
||||||
|
OptimizeAllResult,
|
||||||
OptimizeBacktestRequest,
|
OptimizeBacktestRequest,
|
||||||
OptimizeResult,
|
OptimizeResult,
|
||||||
StrategySchema,
|
StrategySchema,
|
||||||
@@ -147,6 +150,40 @@ export const useBacktestStore = defineStore('backtest', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 一键寻优所有策略(Phase 6) ─────────────────────────────────────────
|
||||||
|
const optimizeAllResult = ref<OptimizeAllResult | null>(null)
|
||||||
|
const optimizeAllRunning = ref(false)
|
||||||
|
|
||||||
|
/** 提交「一键寻优所有策略」后台任务并轮询直到完成。 */
|
||||||
|
async function runOptimizeAll(req: OptimizeAllBacktestRequest) {
|
||||||
|
optimizeAllRunning.value = true
|
||||||
|
error.value = ''
|
||||||
|
optimizeAllResult.value = null
|
||||||
|
try {
|
||||||
|
const { task_id } = await submitOptimizeAllTask(req)
|
||||||
|
const start = Date.now()
|
||||||
|
// 一键寻优全策略网格点更多,放宽超时到 300s
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const state = await fetchTask(task_id)
|
||||||
|
if (state.status === 'done' && state.result) {
|
||||||
|
optimizeAllResult.value = state.result as OptimizeAllResult
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (state.status === 'failed') {
|
||||||
|
throw new Error(state.error || '一键寻优失败')
|
||||||
|
}
|
||||||
|
if (Date.now() - start > 300_000) throw new Error('一键寻优超时(300s)')
|
||||||
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = formatError(e)
|
||||||
|
optimizeAllResult.value = null
|
||||||
|
} finally {
|
||||||
|
optimizeAllRunning.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// state
|
// state
|
||||||
strategies,
|
strategies,
|
||||||
@@ -160,6 +197,8 @@ export const useBacktestStore = defineStore('backtest', () => {
|
|||||||
portfolioRunning,
|
portfolioRunning,
|
||||||
optimizeResult,
|
optimizeResult,
|
||||||
optimizeRunning,
|
optimizeRunning,
|
||||||
|
optimizeAllResult,
|
||||||
|
optimizeAllRunning,
|
||||||
// getters
|
// getters
|
||||||
hasBars,
|
hasBars,
|
||||||
// actions
|
// actions
|
||||||
@@ -170,5 +209,6 @@ export const useBacktestStore = defineStore('backtest', () => {
|
|||||||
runPortfolio,
|
runPortfolio,
|
||||||
clearPortfolio,
|
clearPortfolio,
|
||||||
runOptimize,
|
runOptimize,
|
||||||
|
runOptimizeAll,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+38
-2
@@ -22,6 +22,7 @@ export interface StrategySchema {
|
|||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
params: ParamSchema[]
|
params: ParamSchema[]
|
||||||
|
preset_grid?: Record<string, Array<number | string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StrategiesResponse {
|
export interface StrategiesResponse {
|
||||||
@@ -48,7 +49,7 @@ export interface DataFrameResponse {
|
|||||||
|
|
||||||
// ── 回测请求(POST /api/v1/backtest/run) ─────────────────────────────────────
|
// ── 回测请求(POST /api/v1/backtest/run) ─────────────────────────────────────
|
||||||
|
|
||||||
export type ExecutionMode = 'next_open' | 'next_close' | 'this_close' | 'worst' | 'best'
|
export type ExecutionMode = 'next_open' | 'next_close'
|
||||||
export type Category = 'DAY' | 'WEEK' | 'MONTH' | 'MIN_5' | 'MIN_15' | 'MIN_30' | 'MIN_60'
|
export type Category = 'DAY' | 'WEEK' | 'MONTH' | 'MIN_5' | 'MIN_15' | 'MIN_30' | 'MIN_60'
|
||||||
|
|
||||||
export interface BacktestRequest {
|
export interface BacktestRequest {
|
||||||
@@ -130,7 +131,7 @@ export type TaskStatus = 'pending' | 'running' | 'done' | 'failed'
|
|||||||
export interface TaskState {
|
export interface TaskState {
|
||||||
task_id: string
|
task_id: string
|
||||||
status: TaskStatus
|
status: TaskStatus
|
||||||
result: BacktestResult | PortfolioResult | OptimizeResult | null
|
result: BacktestResult | PortfolioResult | OptimizeResult | OptimizeAllResult | null
|
||||||
error: string | null
|
error: string | null
|
||||||
description: string
|
description: string
|
||||||
elapsed: number
|
elapsed: number
|
||||||
@@ -221,6 +222,41 @@ export interface OptimizeResult {
|
|||||||
heatmap: OptimizeHeatmap | null
|
heatmap: OptimizeHeatmap | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 一键寻优所有策略(Phase 6) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface OptimizeAllBacktestRequest {
|
||||||
|
cash?: number
|
||||||
|
commission?: number
|
||||||
|
slippage?: number
|
||||||
|
execution?: ExecutionMode
|
||||||
|
ohlcv?: Bar[]
|
||||||
|
symbol?: string
|
||||||
|
category?: Category
|
||||||
|
count?: number
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptimizeAllRankEntry {
|
||||||
|
strategy: string
|
||||||
|
strategy_label: string
|
||||||
|
params: Record<string, number | string>
|
||||||
|
total_return: number | null
|
||||||
|
sharpe: number | null
|
||||||
|
max_drawdown: number | null
|
||||||
|
total_trades: number
|
||||||
|
win_rate: number | null
|
||||||
|
profit_factor: number | null
|
||||||
|
grid_points: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptimizeAllResult {
|
||||||
|
ranking: OptimizeAllRankEntry[]
|
||||||
|
best: OptimizeAllRankEntry | null
|
||||||
|
per_strategy: Record<string, OptimizeAllRankEntry>
|
||||||
|
total_grid_points: number
|
||||||
|
}
|
||||||
|
|
||||||
// ── 错误响应(后端 ApiErrorResponse) ─────────────────────────────────────────
|
// ── 错误响应(后端 ApiErrorResponse) ─────────────────────────────────────────
|
||||||
|
|
||||||
export interface ApiError {
|
export interface ApiError {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 回测主页面:左配置面板 / 右报告面板。
|
// 回测主页面:左配置面板 / 右报告面板。
|
||||||
// 编排:取行情 → 选策略+参数 → 回测 → 展示 K线+净值+指标+成交。
|
// 编排:点击「开始回测」→ 自动取行情 → 回测 → 展示 K线+净值+指标+成交。
|
||||||
|
// 取行情已整合进「开始回测」(不再有单独的取行情按钮)。
|
||||||
|
|
||||||
import { nextTick, onMounted, ref } from 'vue'
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@@ -17,15 +18,22 @@ import { useBacktestStore } from '../stores/backtest'
|
|||||||
const store = useBacktestStore()
|
const store = useBacktestStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
// SymbolPicker 实例引用,用于触发取行情
|
||||||
|
const symbolPicker = ref<InstanceType<typeof SymbolPicker> | null>(null)
|
||||||
|
|
||||||
// 表单状态(v-model 给子组件)
|
// 表单状态(v-model 给子组件)
|
||||||
const strategy = ref('ma_cross')
|
const strategy = ref('ma_cross')
|
||||||
const params = ref<Record<string, number | string | boolean>>({})
|
const params = ref<Record<string, number | string | boolean>>({})
|
||||||
const cash = ref(100000)
|
const cash = ref(1000000)
|
||||||
const commission = ref(0.0003)
|
const commission = ref(0.0003)
|
||||||
const slippage = ref(0)
|
const slippage = ref(0)
|
||||||
const execution = ref<ExecutionMode>('next_open')
|
const execution = ref<ExecutionMode>('next_open')
|
||||||
|
|
||||||
const EXECUTIONS: ExecutionMode[] = ['next_open', 'next_close', 'this_close', 'worst', 'best']
|
// 成交价模式(精简为 开盘价/收盘价)
|
||||||
|
const EXECUTIONS: { value: ExecutionMode; label: string }[] = [
|
||||||
|
{ value: 'next_open', label: '开盘价' },
|
||||||
|
{ value: 'next_close', label: '收盘价' },
|
||||||
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.loadStrategies().catch((e) => {
|
await store.loadStrategies().catch((e) => {
|
||||||
@@ -50,7 +58,13 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 取行情 + 回测 串联(点击「开始回测」触发)
|
||||||
async function onRun() {
|
async function onRun() {
|
||||||
|
store.error = ''
|
||||||
|
// 1. 先取行情(SymbolPicker.loadBars 会校验并填充 store.ohlcv)
|
||||||
|
const ok = await symbolPicker.value?.loadBars()
|
||||||
|
if (!ok) return // 校验/取数失败,错误已在 store.error
|
||||||
|
// 2. 再回测
|
||||||
await store.run({
|
await store.run({
|
||||||
strategy: strategy.value,
|
strategy: strategy.value,
|
||||||
params: params.value,
|
params: params.value,
|
||||||
@@ -68,7 +82,7 @@ async function onRun() {
|
|||||||
<aside class="config-panel">
|
<aside class="config-panel">
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h3>行情数据</h3>
|
<h3>行情数据</h3>
|
||||||
<SymbolPicker />
|
<SymbolPicker ref="symbolPicker" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
@@ -99,21 +113,20 @@ async function onRun() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>成交模式</label>
|
<label>成交价</label>
|
||||||
<select v-model="execution">
|
<select v-model="execution">
|
||||||
<option v-for="e in EXECUTIONS" :key="e" :value="e">{{ e }}</option>
|
<option v-for="e in EXECUTIONS" :key="e.value" :value="e.value">{{ e.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary run-btn"
|
class="primary run-btn"
|
||||||
:disabled="store.running || !store.hasBars"
|
:disabled="store.running"
|
||||||
@click="onRun"
|
@click="onRun"
|
||||||
>
|
>
|
||||||
{{ store.running ? '回测中…' : '开始回测' }}
|
{{ store.running ? '取行情+回测中…' : '开始回测' }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="!store.hasBars" class="hint">请先取行情数据</p>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- 右栏:报告 -->
|
<!-- 右栏:报告 -->
|
||||||
@@ -121,7 +134,7 @@ async function onRun() {
|
|||||||
<div v-if="store.error" class="error-banner">⚠ {{ store.error }}</div>
|
<div v-if="store.error" class="error-banner">⚠ {{ store.error }}</div>
|
||||||
|
|
||||||
<div v-if="!store.result && !store.running && !store.error" class="placeholder">
|
<div v-if="!store.result && !store.running && !store.error" class="placeholder">
|
||||||
<p>选择标的、取行情、配置策略后点击「开始回测」</p>
|
<p>输入代码、配置策略后点击「开始回测」(自动取行情)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="store.result" class="report-content">
|
<div v-if="store.result" class="report-content">
|
||||||
@@ -190,12 +203,6 @@ async function onRun() {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.hint {
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 右栏报告面板 */
|
/* 右栏报告面板 */
|
||||||
.report-panel {
|
.report-panel {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 参数网格寻优主页面:左配置(选标的 + 策略 + 寻优参数)/ 右报告(排名表 + 热力图)。
|
// 参数寻优主页面:左配置(选标的 + 策略 + 寻优参数)/ 右报告(排名表 + 热力图)。
|
||||||
|
// 取行情已整合进「开始寻优」。另有「一键寻优所有策略」:用各策略预设网格逐策略寻优再全局排名。
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -14,11 +15,18 @@ import { useBacktestStore } from '../stores/backtest'
|
|||||||
const store = useBacktestStore()
|
const store = useBacktestStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// SymbolPicker 实例引用,用于触发取行情
|
||||||
|
const symbolPicker = ref<InstanceType<typeof SymbolPicker> | null>(null)
|
||||||
|
|
||||||
const strategy = ref('ma_cross')
|
const strategy = ref('ma_cross')
|
||||||
const paramGrid = ref<Record<string, Array<number | string>>>({})
|
const paramGrid = ref<Record<string, Array<number | string>>>({})
|
||||||
const cash = ref(100000)
|
const cash = ref(1000000)
|
||||||
const execution = ref<ExecutionMode>('next_open')
|
const execution = ref<ExecutionMode>('next_open')
|
||||||
const EXECUTIONS: ExecutionMode[] = ['next_open', 'next_close', 'this_close', 'worst', 'best']
|
// 成交价模式(精简为 开盘价/收盘价)
|
||||||
|
const EXECUTIONS: { value: ExecutionMode; label: string }[] = [
|
||||||
|
{ value: 'next_open', label: '开盘价' },
|
||||||
|
{ value: 'next_close', label: '收盘价' },
|
||||||
|
]
|
||||||
|
|
||||||
const selectedStrategy = computed(
|
const selectedStrategy = computed(
|
||||||
() => store.strategies.find((s) => s.name === strategy.value) ?? null,
|
() => store.strategies.find((s) => s.name === strategy.value) ?? null,
|
||||||
@@ -36,11 +44,13 @@ const gridPoints = computed(() => {
|
|||||||
return sizes.reduce((a, b) => a * b, 1)
|
return sizes.reduce((a, b) => a * b, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 取行情(点击「开始寻优」时触发)→ 寻优
|
||||||
async function onRun() {
|
async function onRun() {
|
||||||
if (!store.hasBars) {
|
store.error = ''
|
||||||
store.error = '请先取行情数据'
|
// 1. 先取行情
|
||||||
return
|
const ok = await symbolPicker.value?.loadBars()
|
||||||
}
|
if (!ok) return
|
||||||
|
// 2. 校验寻优参数
|
||||||
if (Object.keys(paramGrid.value).length === 0) {
|
if (Object.keys(paramGrid.value).length === 0) {
|
||||||
store.error = '请勾选至少 1 个参数并填入取值'
|
store.error = '请勾选至少 1 个参数并填入取值'
|
||||||
return
|
return
|
||||||
@@ -49,6 +59,7 @@ async function onRun() {
|
|||||||
store.error = `网格点数 ${gridPoints.value} 超过上限 200`
|
store.error = `网格点数 ${gridPoints.value} 超过上限 200`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 3. 寻优
|
||||||
await store.runOptimize({
|
await store.runOptimize({
|
||||||
strategy: strategy.value,
|
strategy: strategy.value,
|
||||||
param_grid: paramGrid.value,
|
param_grid: paramGrid.value,
|
||||||
@@ -58,6 +69,20 @@ async function onRun() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 一键寻优所有策略:取行情 → 全策略预设网格寻优 → 全局排名
|
||||||
|
async function onRunAll() {
|
||||||
|
store.error = ''
|
||||||
|
// 1. 先取行情
|
||||||
|
const ok = await symbolPicker.value?.loadBars()
|
||||||
|
if (!ok) return
|
||||||
|
// 2. 一键寻优
|
||||||
|
await store.runOptimizeAll({
|
||||||
|
cash: cash.value,
|
||||||
|
execution: execution.value,
|
||||||
|
ohlcv: store.ohlcv,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 点击排名表「查看」→ 跳转单标的页用该参数回测
|
// 点击排名表「查看」→ 跳转单标的页用该参数回测
|
||||||
function onViewParams(params: Record<string, number | string>) {
|
function onViewParams(params: Record<string, number | string>) {
|
||||||
// 通过 query 传递参数,单标的页接收后自动填充
|
// 通过 query 传递参数,单标的页接收后自动填充
|
||||||
@@ -66,6 +91,21 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
query: { strategy: strategy.value, params: JSON.stringify(params) },
|
query: { strategy: strategy.value, params: JSON.stringify(params) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 一键寻优结果点击「查看」→ 跳转单标的页用该策略 + 参数回测
|
||||||
|
function onViewAll(strategyName: string, params: Record<string, number | string>) {
|
||||||
|
router.push({
|
||||||
|
path: '/',
|
||||||
|
query: { strategy: strategyName, params: JSON.stringify(params) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pct(v: number | null | undefined): string {
|
||||||
|
return v !== null && v !== undefined && Number.isFinite(v) ? `${(v * 100).toFixed(2)}%` : '-'
|
||||||
|
}
|
||||||
|
function num(v: number | null | undefined, d = 2): string {
|
||||||
|
return v !== null && v !== undefined && Number.isFinite(v) ? v.toFixed(d) : '-'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -73,7 +113,7 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
<aside class="config-panel">
|
<aside class="config-panel">
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h3>行情数据</h3>
|
<h3>行情数据</h3>
|
||||||
<SymbolPicker />
|
<SymbolPicker ref="symbolPicker" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
@@ -99,19 +139,26 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
<input v-model.number="cash" type="number" min="1000" step="10000" />
|
<input v-model.number="cash" type="number" min="1000" step="10000" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>成交模式</label>
|
<label>成交价</label>
|
||||||
<select v-model="execution">
|
<select v-model="execution">
|
||||||
<option v-for="e in EXECUTIONS" :key="e" :value="e">{{ e }}</option>
|
<option v-for="e in EXECUTIONS" :key="e.value" :value="e.value">{{ e.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary run-btn"
|
class="primary run-btn"
|
||||||
:disabled="store.optimizeRunning || !store.hasBars"
|
:disabled="store.optimizeRunning || store.optimizeAllRunning"
|
||||||
@click="onRun"
|
@click="onRun"
|
||||||
>
|
>
|
||||||
{{ store.optimizeRunning ? '寻优中…' : '开始寻优' }}
|
{{ store.optimizeRunning ? '取行情+寻优中…' : '开始寻优' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="run-btn"
|
||||||
|
:disabled="store.optimizeRunning || store.optimizeAllRunning"
|
||||||
|
@click="onRunAll"
|
||||||
|
>
|
||||||
|
{{ store.optimizeAllRunning ? '一键寻优所有策略中…' : '一键寻优所有策略' }}
|
||||||
</button>
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -119,12 +166,13 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
<div v-if="store.error" class="error-banner">⚠ {{ store.error }}</div>
|
<div v-if="store.error" class="error-banner">⚠ {{ store.error }}</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!store.optimizeResult && !store.optimizeRunning && !store.error"
|
v-if="!store.optimizeResult && !store.optimizeAllResult && !store.optimizeRunning && !store.optimizeAllRunning && !store.error"
|
||||||
class="placeholder"
|
class="placeholder"
|
||||||
>
|
>
|
||||||
<p>选标的 → 取行情 → 选策略 → 勾选寻优参数 → 开始寻优</p>
|
<p>选标的 → 选策略 → 勾选寻优参数 → 开始寻优;或点「一键寻优所有策略」</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 单策略寻优结果 -->
|
||||||
<div v-if="store.optimizeResult" class="report-content">
|
<div v-if="store.optimizeResult" class="report-content">
|
||||||
<section class="report-section">
|
<section class="report-section">
|
||||||
<h3>最优结果</h3>
|
<h3>最优结果</h3>
|
||||||
@@ -154,6 +202,71 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 一键寻优所有策略结果 -->
|
||||||
|
<div v-if="store.optimizeAllResult" class="report-content">
|
||||||
|
<section class="report-section">
|
||||||
|
<h3>全局最佳</h3>
|
||||||
|
<div v-if="store.optimizeAllResult.best" class="best-summary">
|
||||||
|
<span class="best-params">
|
||||||
|
{{ store.optimizeAllResult.best.strategy_label }}
|
||||||
|
{{ JSON.stringify(store.optimizeAllResult.best.params) }}
|
||||||
|
</span>
|
||||||
|
<span class="best-return pos">
|
||||||
|
{{ (store.optimizeAllResult.best.total_return! * 100).toFixed(2) }}%
|
||||||
|
</span>
|
||||||
|
<span class="best-meta">
|
||||||
|
夏普 {{ store.optimizeAllResult.best.sharpe?.toFixed(2) }} · 回撤
|
||||||
|
{{ (store.optimizeAllResult.best.max_drawdown! * 100).toFixed(2) }}% · 胜率
|
||||||
|
{{ (store.optimizeAllResult.best.win_rate! * 100).toFixed(1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="meta-line">
|
||||||
|
共 {{ store.optimizeAllResult.ranking.length }} 个策略有效 ·
|
||||||
|
合计 {{ store.optimizeAllResult.total_grid_points }} 网格点
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="report-section">
|
||||||
|
<h3>策略排名(按总收益降序)</h3>
|
||||||
|
<table class="opt-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>策略</th>
|
||||||
|
<th>参数</th>
|
||||||
|
<th class="num">总收益</th>
|
||||||
|
<th class="num">夏普</th>
|
||||||
|
<th class="num">最大回撤</th>
|
||||||
|
<th class="num">交易数</th>
|
||||||
|
<th class="num">胜率</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(r, i) in store.optimizeAllResult.ranking"
|
||||||
|
:key="r.strategy"
|
||||||
|
:class="{ best: i === 0 }"
|
||||||
|
>
|
||||||
|
<td class="rank">{{ i + 1 }}</td>
|
||||||
|
<td>{{ r.strategy_label }}</td>
|
||||||
|
<td class="params">{{ JSON.stringify(r.params) }}</td>
|
||||||
|
<td class="num" :class="r.total_return !== null && r.total_return > 0 ? 'pos' : 'neg'">
|
||||||
|
{{ pct(r.total_return) }}
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ num(r.sharpe) }}</td>
|
||||||
|
<td class="num neg">{{ pct(r.max_drawdown) }}</td>
|
||||||
|
<td class="num">{{ r.total_trades }}</td>
|
||||||
|
<td class="num">{{ pct(r.win_rate) }}</td>
|
||||||
|
<td>
|
||||||
|
<button class="view-btn" @click="onViewAll(r.strategy, r.params)">查看</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -188,6 +301,10 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.run-btn:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.report-panel {
|
.report-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -227,6 +344,7 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.best-params {
|
.best-params {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
@@ -242,7 +360,57 @@ function onViewParams(params: Record<string, number | string>) {
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.meta-line {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
.pos {
|
.pos {
|
||||||
color: var(--up);
|
color: var(--up);
|
||||||
}
|
}
|
||||||
|
.neg {
|
||||||
|
color: var(--down);
|
||||||
|
}
|
||||||
|
.opt-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.opt-table th,
|
||||||
|
.opt-table td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.opt-table th {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
text-align: right;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
.params {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.rank {
|
||||||
|
color: var(--text-dim);
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
.best {
|
||||||
|
background: rgba(74, 158, 255, 0.08);
|
||||||
|
}
|
||||||
|
.best .rank {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.view-btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,11 +16,15 @@ const store = useBacktestStore()
|
|||||||
const stocks = ref<string[]>(['SZ:000001', 'SH:600519'])
|
const stocks = ref<string[]>(['SZ:000001', 'SH:600519'])
|
||||||
const strategy = ref('ma_cross')
|
const strategy = ref('ma_cross')
|
||||||
const params = ref<Record<string, number | string | boolean>>({})
|
const params = ref<Record<string, number | string | boolean>>({})
|
||||||
const cash = ref(200000)
|
const cash = ref(1000000)
|
||||||
const category = ref<Category>('DAY')
|
const category = ref<Category>('DAY')
|
||||||
const execution = ref<ExecutionMode>('next_open')
|
const execution = ref<ExecutionMode>('next_open')
|
||||||
|
|
||||||
const EXECUTIONS: ExecutionMode[] = ['next_open', 'next_close', 'this_close', 'worst', 'best']
|
// 成交价模式(精简为 开盘价/收盘价)
|
||||||
|
const EXECUTIONS: { value: ExecutionMode; label: string }[] = [
|
||||||
|
{ value: 'next_open', label: '开盘价' },
|
||||||
|
{ value: 'next_close', label: '收盘价' },
|
||||||
|
]
|
||||||
const CATEGORIES: Category[] = ['DAY', 'WEEK', 'MONTH', 'MIN_5', 'MIN_15', 'MIN_30', 'MIN_60']
|
const CATEGORIES: Category[] = ['DAY', 'WEEK', 'MONTH', 'MIN_5', 'MIN_15', 'MIN_30', 'MIN_60']
|
||||||
|
|
||||||
// 日期默认(复用单标的逻辑)
|
// 日期默认(复用单标的逻辑)
|
||||||
@@ -98,9 +102,9 @@ async function onRun() {
|
|||||||
<input v-model.number="cash" type="number" min="1000" step="10000" />
|
<input v-model.number="cash" type="number" min="1000" step="10000" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>成交模式</label>
|
<label>成交价</label>
|
||||||
<select v-model="execution">
|
<select v-model="execution">
|
||||||
<option v-for="e in EXECUTIONS" :key="e" :value="e">{{ e }}</option>
|
<option v-for="e in EXECUTIONS" :key="e.value" :value="e.value">{{ e.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user