feat(frontend): 验收UX批A: CTA回测提交统一跳任务中心; 代码编辑「运行回测」直接发起(默认参数+最近1年,CTA/组合分流); 模拟盘策略改下拉; 状态徽标实走文案+去排队兜底 [nas]
CI/CD / test (push) Successful in 12s
CI/CD / nas-deploy (push) Failing after 11s
CI/CD / nas-verify (push) Has been skipped

This commit is contained in:
2026-08-13 18:17:16 +08:00
parent 976c99de07
commit c41fd862ea
5 changed files with 101 additions and 12 deletions
+3 -2
View File
@@ -151,7 +151,8 @@ async function onSubmit(): Promise<void> {
slippage: Number(form.slippage),
})
ElMessage.success('回测已提交')
router.push(`/backtest/progress/${tid}`)
void tid
router.push('/backtest/history')
} catch {
ElMessage.error('提交失败')
} finally {
@@ -165,7 +166,7 @@ async function onSubmit(): Promise<void> {
<div class="page-head">
<div>
<h2 class="page-title">个股回测</h2>
<p class="page-subtitle">CTA 策略历史回测 · 选择策略与标的提交后进入进度跟踪</p>
<p class="page-subtitle">CTA 策略历史回测 · 提交后可在历史任务跟踪进度与结果</p>
</div>
</div>
+1 -1
View File
@@ -41,7 +41,7 @@ const stats = computed(() => ({
}))
const modeLabel: Record<string, string> = { replay: '回放', live: '实走' }
const statusLabel: Record<string, string> = { running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队' }
const statusLabel: Record<string, string> = { running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队', stopped: '已停止' }
function pct(v: number | null | undefined): string {
if (v == null || !Number.isFinite(v)) return '—'
+21 -3
View File
@@ -4,11 +4,21 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { createPaper, type PaperCreate } from '@/api/paper'
import { apiClient } from '@/api/client'
import { getStrategies, type StrategyItem } from '@/api/strategy'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const fromInstance = ref('')
const strategyOptions = ref<StrategyItem[]>([])
onMounted(async () => {
try {
strategyOptions.value = await getStrategies()
} catch {
/* 下拉加载失败不阻塞表单 */
}
})
function loadDateRange(): { start: string; end: string } {
try {
const s = localStorage.getItem('paper_date_range')
@@ -137,9 +147,17 @@ function onSymbols(v: string): void {
/>
<span class="muted form-hint">多个标的用英文逗号分隔</span>
</el-form-item>
<el-form-item label="策略">
<el-input v-model="form.strategies[0].name" style="width: 280px" />
<span class="muted form-hint">CTA 策略类名 DoubleMaStrategy</span>
<el-form-item label="策略">
<el-select
v-model="form.strategies[0].name"
filterable
allow-create
placeholder="选择或输入策略类名"
style="width: 320px"
>
<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-form-item>
<el-form-item label="撮合时点">
<el-select v-model="form.strategies[0].match_session" style="width: 280px">
+6 -2
View File
@@ -97,7 +97,7 @@ function num(v: number | undefined | null): string {
return Math.round(v).toLocaleString('zh-CN')
}
function statusLabel(s: string): string {
return ({ running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队' } as Record<string, string>)[s] || s
return ({ running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队', stopped: '已停止' } as Record<string, string>)[s] || s
}
function rowClass({ row }: { row: PaperTrade }): string {
return row.rejected ? 'rejected-row' : ''
@@ -119,7 +119,11 @@ function toLive(): void {
</p>
</div>
<div class="head-actions">
<span class="chip" :class="`st-${account?.status ?? 'pending'}`">{{ statusLabel(account?.status ?? '') }}</span>
<span v-if="account" class="chip" :class="`st-${account.status}`">{{
account.mode === 'live' && account.status === 'running'
? '实走中 · 每日 20:30 结算'
: statusLabel(account.status)
}}</span>
<el-button v-if="account?.mode === 'live'" type="primary" @click="toLive">实走监控 </el-button>
</div>
</div>
+70 -4
View File
@@ -4,6 +4,9 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import MonacoEditor from '@/components/MonacoEditor.vue'
import { apiClient } from '@/api/client'
import { getParams } from '@/api/strategy'
import { submitCta } from '@/api/backtest'
import { postPortfolioBacktest } from '@/api/portfolio'
interface StrategyFile {
name: string
@@ -76,9 +79,72 @@ async function onSave(): Promise<void> {
}
}
function runBacktest(): void {
if (active.value) {
router.push({ path: '/backtest/new', query: { class: active.value.class_name } })
const running = ref(false)
function lastDateRange(): { start: string; end: string } {
try {
const s = localStorage.getItem('bt_date_range')
if (s) return JSON.parse(s) as { start: string; end: string }
} catch {
/* ignore */
}
const end = new Date()
const start = new Date(end)
start.setFullYear(start.getFullYear() - 1)
const f = (d: Date): string => d.toISOString().slice(0, 10)
return { start: f(start), end: f(end) }
}
// 直接用当前策略发起回测:默认参数 + 最近区间,提交后跳「历史任务」看进度
async function runBacktest(): Promise<void> {
if (!active.value || running.value) return
if (dirty.value) {
ElMessage.warning('有未保存修改,请先保存再运行回测')
return
}
running.value = true
try {
const dr = lastDateRange()
if (active.value.type === 'portfolio') {
await postPortfolioBacktest({
pool: 'hs300_subset',
strategy: active.value.name.replace(/\.py$/, ''),
max_pool: 30,
start_date: dr.start,
end_date: dr.end,
initial_cash: 1_000_000,
benchmark: '000300.XSHG',
commission_rate: 0.0003,
stamp_duty_rate: 0.001,
min_commission: 5,
slippage: 0.001,
})
} else {
const p = await getParams(active.value.class_name)
const params: Record<string, unknown> = {}
p.parameters.forEach((k) => {
const v = p.defaults[k]
params[k] = v != null ? v : ''
})
await submitCta({
symbol: '600000',
strategy: active.value.class_name,
params,
start: dr.start,
end: dr.end,
benchmark: 'hs300',
interval: 'd',
commission_rate: 0.0003,
stamp_duty_rate: 0.001,
slippage: 0.001,
})
}
ElMessage.success('回测已提交,可在「历史任务」查看进度')
router.push('/backtest/history')
} catch {
ElMessage.error('回测提交失败')
} finally {
running.value = false
}
}
</script>
@@ -125,7 +191,7 @@ function runBacktest(): void {
<span class="modified mono">{{ active?.modified }}</span>
<button class="term-btn sm" :disabled="checking" @click="onCheck">{{ checking ? '校验中' : '语法检查' }}</button>
<button class="term-btn sm primary" :disabled="!dirty || saving" @click="onSave">{{ saving ? '保存中' : '保存' }}</button>
<button class="term-btn sm" @click="runBacktest">运行回测</button>
<button class="term-btn sm" :disabled="running" @click="runBacktest">{{ running ? '提交中' : '运行回测' }}</button>
</div>
</div>
<div class="monaco-wrap">