feat(frontend): 验收UX批A: CTA回测提交统一跳任务中心; 代码编辑「运行回测」直接发起(默认参数+最近1年,CTA/组合分流); 模拟盘策略改下拉; 状态徽标实走文案+去排队兜底 [nas]
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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 '—'
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user