diff --git a/frontend/src/mock/index.ts b/frontend/src/mock/index.ts index f6e62a8..377ea10 100644 --- a/frontend/src/mock/index.ts +++ b/frontend/src/mock/index.ts @@ -31,6 +31,12 @@ const routes: Route[] = [ { m: 'get', re: /^\/strategy\/files$/, build: () => D.strategyFilesMock }, { m: 'post', re: /^\/strategy\/file\/[^/]+\/check$/, build: () => ({ ok: true }) }, { m: 'post', re: /^\/strategy\/file\/[^/]+$/, build: () => ({ ok: true }) }, + // 读单文件全量(编辑器主区)——此前 dev 缺此 mock,切文件报「读取失败」 + { m: 'get', re: /^\/strategy\/file\/[^/]+$/, build: (url) => { + const name = decodeURIComponent(url.split('/').filter(Boolean)[2] || '') + return { name, dir: '', class_name: '', type: 'cta', lines: 10, modified: '', + code: `# ${name}(mock 源码)\n\nclass DemoStrategy:\n """dev mock"""\n\n def initialize(self, context):\n self.n = 3\n\n def handle_data(self, context, data):\n pass\n` } + } }, // §12.6 补:代码版本快照(列表在前,单版本 4 段路径在后) { m: 'get', re: /^\/strategy\/code-versions$/, build: (url) => D.codeVersionsMock(new URL(url, 'http://x').searchParams.get('file') || '') }, { m: 'get', re: /^\/strategy\/code-versions\/[^/]+\/[^/]+$/, build: (url) => { const seg = url.split('/').filter(Boolean); return D.codeVersionMock(decodeURIComponent(seg[2]), seg[3]) } }, diff --git a/frontend/src/views/backtest/New.vue b/frontend/src/views/backtest/New.vue index 1fc113c..46b266c 100644 --- a/frontend/src/views/backtest/New.vue +++ b/frontend/src/views/backtest/New.vue @@ -16,6 +16,7 @@ interface InstLike { interface FileLike { name: string class_name: string + type?: string } const route = useRoute() @@ -137,6 +138,21 @@ onMounted(async () => { ]) instanceOptions.value = ins classToFile.value = Object.fromEntries(fr.data.files.map((f) => [f.class_name, f.name])) + // 组合策略档案误入 CTA 回测页 → 重定向组合页(老链接 class=all_weather.py 场景) + const cls = route.query.class + const instId0 = route.query.instance + const inst = instId0 && !Array.isArray(instId0) + ? ins.find((x) => x.id === Number(instId0)) + : undefined + const probe = inst?.code_file || (typeof cls === 'string' && cls.includes('.') ? cls : '') + const probeFile = fr.data.files.find((f) => f.name === probe || f.name === `${probe}.py`) + if (probeFile?.type === 'portfolio') { + router.replace({ + path: '/backtest/portfolio', + query: { strategy: probeFile.name.replace(/\.py$/, ''), ...(instId0 ? { instance: String(instId0) } : {}) }, + }) + return + } } catch { /* 档案下拉失败不阻塞 */ } diff --git a/frontend/src/views/backtest/PortfolioBacktest.vue b/frontend/src/views/backtest/PortfolioBacktest.vue index c6439ac..ec8e3eb 100644 --- a/frontend/src/views/backtest/PortfolioBacktest.vue +++ b/frontend/src/views/backtest/PortfolioBacktest.vue @@ -389,9 +389,9 @@ const fromTaskId = computed(() => (route.query.from_task as string) || '') onMounted(async () => { if (viewTaskId.value) { loadResult(viewTaskId.value) - } else if (route.query.strategy) { - // 从代码编辑页「运行回测」跳转:按策略预填 - form.strategy = String(route.query.strategy) + } else if (route.query.strategy || route.query.class) { + // 跳转预填:strategy=文件名去.py;兼容旧链接的 class=(可能是 all_weather.py 带后缀) + form.strategy = String(route.query.strategy || route.query.class).replace(/\.py$/, '') } else if (fromTaskId.value) { // 从任务列表「点任务ID」跳转:按该任务参数完整预填 void prefillFromTask(fromTaskId.value) diff --git a/frontend/src/views/live/List.vue b/frontend/src/views/live/List.vue index e459221..45b4a36 100644 --- a/frontend/src/views/live/List.vue +++ b/frontend/src/views/live/List.vue @@ -1,6 +1,7 @@ + + + + + + + 代码快照 · {{ label }} + + {{ codeFile || '…' }} · 发起版本 v{{ codeHash.slice(0, 8) }} + + + 关闭 + + {{ err }} + + 左=发起时快照 · 右=当前代码 · 滚动联动 + + + + + + + + + diff --git a/frontend/src/views/strategy/List.vue b/frontend/src/views/strategy/List.vue index 4f7b5e8..0c0c70e 100644 --- a/frontend/src/views/strategy/List.vue +++ b/frontend/src/views/strategy/List.vue @@ -145,16 +145,36 @@ function newInstance(): void { function editInstance(i: Instance): void { router.push(`/strategy/instance/${i.id}`) } +// 以文件实际类型为准(老实例 type 字段可能不准,如组合档案标了 cta) +function fileTypeOf(i: Instance): 'portfolio' | 'cta' { + return files.value.find((f) => f.name === i.code_file)?.type || i.type +} function runBacktest(i: Instance): void { + const isPf = fileTypeOf(i) === 'portfolio' router.push({ - path: i.type === 'portfolio' ? '/backtest/portfolio' : '/backtest/new', - query: { class: i.code_file, instance: String(i.id) }, + path: isPf ? '/backtest/portfolio' : '/backtest/new', + // 组合页吃 strategy(文件名去.py);CTA 页吃 class(类名) + query: isPf + ? { strategy: i.code_file.replace(/\.py$/, ''), instance: String(i.id) } + : { + class: files.value.find((f) => f.name === i.code_file)?.class_name || i.code_file, + instance: String(i.id), + }, }) } +// 运行灯语义:有在跑账户 → 看运行(列表按档案过滤);没有 → 发起(创建页带档案) function runPaper(i: Instance, mode: 'replay' | 'live' | 'shadow'): void { + if (mode !== 'replay' && (i.running_accounts || []).some((a) => a.kind === 'paper' || a.kind === 'shadow')) { + router.push(`/paper?instance=${i.id}`) + return + } router.push({ path: '/paper/new', query: { mode, instance: String(i.id) } }) } function runLive(i: Instance): void { + if ((i.running_accounts || []).some((a) => a.kind === 'live')) { + router.push(`/live?instance=${i.id}`) + return + } router.push({ path: '/live/new', query: { instance: String(i.id) } }) } async function onSync(i: Instance): Promise { @@ -211,7 +231,6 @@ const runningRows = computed(() => 代码 → 实例档案 → 回测 / 模拟 / 实盘 · 全生命周期 - 编辑代码 + 新建策略 @@ -297,7 +316,7 @@ const runningRows = computed(() => {{ pct(latestRet(i)) }}{{ retSub(i) }} 回测 - 回放 + 回放 {{ runLabel(i, 'paper_live') }}
代码 → 实例档案 → 回测 / 模拟 / 实盘 · 全生命周期