From c41fd862ea4c2b6e4bf6d523416a0f7e8a6fe4fc Mon Sep 17 00:00:00 2001
From: claude_dev
Date: Thu, 13 Aug 2026 18:17:16 +0800
Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E9=AA=8C=E6=94=B6UX=E6=89=B9?=
=?UTF-8?q?A:=20CTA=E5=9B=9E=E6=B5=8B=E6=8F=90=E4=BA=A4=E7=BB=9F=E4=B8=80?=
=?UTF-8?q?=E8=B7=B3=E4=BB=BB=E5=8A=A1=E4=B8=AD=E5=BF=83;=20=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81=E7=BC=96=E8=BE=91=E3=80=8C=E8=BF=90=E8=A1=8C=E5=9B=9E?=
=?UTF-8?q?=E6=B5=8B=E3=80=8D=E7=9B=B4=E6=8E=A5=E5=8F=91=E8=B5=B7(?=
=?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=8F=82=E6=95=B0+=E6=9C=80=E8=BF=911?=
=?UTF-8?q?=E5=B9=B4,CTA/=E7=BB=84=E5=90=88=E5=88=86=E6=B5=81);=20?=
=?UTF-8?q?=E6=A8=A1=E6=8B=9F=E7=9B=98=E7=AD=96=E7=95=A5=E6=94=B9=E4=B8=8B?=
=?UTF-8?q?=E6=8B=89;=20=E7=8A=B6=E6=80=81=E5=BE=BD=E6=A0=87=E5=AE=9E?=
=?UTF-8?q?=E8=B5=B0=E6=96=87=E6=A1=88+=E5=8E=BB=E6=8E=92=E9=98=9F?=
=?UTF-8?q?=E5=85=9C=E5=BA=95=20[nas]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/src/views/backtest/New.vue | 5 +-
frontend/src/views/paper/List.vue | 2 +-
frontend/src/views/paper/New.vue | 24 +++++++--
frontend/src/views/paper/Result.vue | 8 ++-
frontend/src/views/strategy/Code.vue | 74 ++++++++++++++++++++++++++--
5 files changed, 101 insertions(+), 12 deletions(-)
diff --git a/frontend/src/views/backtest/New.vue b/frontend/src/views/backtest/New.vue
index 3b60079..0898317 100644
--- a/frontend/src/views/backtest/New.vue
+++ b/frontend/src/views/backtest/New.vue
@@ -151,7 +151,8 @@ async function onSubmit(): Promise {
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 {
个股回测
-
CTA 策略历史回测 · 选择策略与标的,提交后进入进度跟踪
+
CTA 策略历史回测 · 提交后可在「历史任务」跟踪进度与结果
diff --git a/frontend/src/views/paper/List.vue b/frontend/src/views/paper/List.vue
index 55215c4..c227961 100644
--- a/frontend/src/views/paper/List.vue
+++ b/frontend/src/views/paper/List.vue
@@ -41,7 +41,7 @@ const stats = computed(() => ({
}))
const modeLabel: Record = { replay: '回放', live: '实走' }
-const statusLabel: Record = { running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队' }
+const statusLabel: Record = { running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队', stopped: '已停止' }
function pct(v: number | null | undefined): string {
if (v == null || !Number.isFinite(v)) return '—'
diff --git a/frontend/src/views/paper/New.vue b/frontend/src/views/paper/New.vue
index 82a123a..bb72255 100644
--- a/frontend/src/views/paper/New.vue
+++ b/frontend/src/views/paper/New.vue
@@ -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([])
+
+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 {
/>
多个标的用英文逗号分隔
-
-
- CTA 策略类名,如 DoubleMaStrategy
+
+
+
+
+ CTA 策略(个股),如 DoubleMaStrategy
diff --git a/frontend/src/views/paper/Result.vue b/frontend/src/views/paper/Result.vue
index 9e6c2a3..e021bf4 100644
--- a/frontend/src/views/paper/Result.vue
+++ b/frontend/src/views/paper/Result.vue
@@ -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)[s] || s
+ return ({ running: '运行中', done: '完成', failed: '失败', created: '已创建', pending: '排队', stopped: '已停止' } as Record)[s] || s
}
function rowClass({ row }: { row: PaperTrade }): string {
return row.rejected ? 'rejected-row' : ''
@@ -119,7 +119,11 @@ function toLive(): void {
- {{ statusLabel(account?.status ?? '') }}
+ {{
+ account.mode === 'live' && account.status === 'running'
+ ? '实走中 · 每日 20:30 结算'
+ : statusLabel(account.status)
+ }}
实走监控 →
diff --git a/frontend/src/views/strategy/Code.vue b/frontend/src/views/strategy/Code.vue
index 4cf4884..8f951a0 100644
--- a/frontend/src/views/strategy/Code.vue
+++ b/frontend/src/views/strategy/Code.vue
@@ -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 {
}
}
-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 {
+ 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 = {}
+ 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
}
}
@@ -125,7 +191,7 @@ function runBacktest(): void {
{{ active?.modified }}
-
+