From 82c9edb092c67f40faa094a1494ac9cb3c738bfc Mon Sep 17 00:00:00 2001 From: claude_dev Date: Thu, 13 Aug 2026 19:40:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(strategy):=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=A9=BA=E7=99=BD=E6=A0=B9=E5=9B=A0=3D/strat?= =?UTF-8?q?egy/files=E4=B8=8D=E5=90=ABcode;=20=E5=8A=A0GET=20/strategy/fil?= =?UTF-8?q?e/{name}=E6=8C=89=E9=9C=80=E6=8B=89=E5=8F=96;=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E5=9B=9E=E6=B5=8B=E6=94=B9=E5=9B=9E=E8=B7=B3=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=A1=B5+=E6=8C=89=E7=AD=96=E7=95=A5=E9=A2=84?= =?UTF-8?q?=E5=A1=AB(New.vue=E8=AF=BBquery.class,=E6=8E=A8=E7=BF=BB?= =?UTF-8?q?=E4=B8=8A=E8=BD=AE=E7=9B=B4=E6=8E=A5=E5=8F=91=E8=B5=B7)=20[nas]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/views/backtest/New.vue | 4 ++ frontend/src/views/strategy/Code.vue | 93 +++++++--------------------- sanguo_api/routes_strategy.py | 9 +++ tests/api/test_routes_strategy.py | 14 +++++ 4 files changed, 49 insertions(+), 71 deletions(-) diff --git a/frontend/src/views/backtest/New.vue b/frontend/src/views/backtest/New.vue index 0898317..b36c9a7 100644 --- a/frontend/src/views/backtest/New.vue +++ b/frontend/src/views/backtest/New.vue @@ -106,6 +106,10 @@ onMounted(async () => { /* 预填失败,走默认 */ } } + // 从代码编辑页「运行回测」跳转:按策略类名预填 + if (!form.strategy && route.query.class) { + form.strategy = String(route.query.class) + } if (!form.strategy && strategies.value.length) { form.strategy = strategies.value[0].name } diff --git a/frontend/src/views/strategy/Code.vue b/frontend/src/views/strategy/Code.vue index 445968c..7cd522f 100644 --- a/frontend/src/views/strategy/Code.vue +++ b/frontend/src/views/strategy/Code.vue @@ -4,9 +4,6 @@ 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 @@ -15,7 +12,6 @@ interface StrategyFile { type: string lines: number modified: string - code: string } const router = useRouter() @@ -45,11 +41,22 @@ onMounted(async () => { } }) -function select(f: StrategyFile): void { +const loadingCode = ref(false) + +async function select(f: StrategyFile): Promise { activeName.value = f.name - code.value = f.code dirty.value = false syntaxOk.value = null + loadingCode.value = true + try { + const { data } = await apiClient.get<{ code: string }>(`/strategy/file/${f.name}`) + code.value = data.code + } catch { + code.value = '' + ElMessage.error(`读取 ${f.name} 失败`) + } finally { + loadingCode.value = false + } } watch(code, () => { @@ -70,7 +77,6 @@ async function onSave(): Promise { saving.value = true try { await apiClient.post(`/strategy/file/${activeName.value}`, { code: code.value }) - active.value.code = code.value dirty.value = false ElMessage.success('已保存 · 下次回测自动加载最新代码') } catch { @@ -80,72 +86,17 @@ async function onSave(): Promise { } } -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 +// 跳到对应的回测参数页:初始值按当前策略预填,区间等参数由人调整 +function runBacktest(): void { + if (!active.value) return if (dirty.value) { - ElMessage.warning('有未保存修改,请先保存再运行回测') + 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 + if (active.value.type === 'portfolio') { + router.push({ path: '/backtest/portfolio', query: { strategy: active.value.name.replace(/\.py$/, '') } }) + } else { + router.push({ path: '/backtest/new', query: { class: active.value.class_name } }) } } @@ -192,7 +143,7 @@ async function runBacktest(): Promise { {{ active?.modified }} - +
diff --git a/sanguo_api/routes_strategy.py b/sanguo_api/routes_strategy.py index 8590cd6..21175fa 100644 --- a/sanguo_api/routes_strategy.py +++ b/sanguo_api/routes_strategy.py @@ -34,6 +34,15 @@ def get_files(): return list_strategy_files() +@router.get("/strategy/file/{name}") +def get_file(name: str): + """读单个策略文件全量(含 code)。""" + try: + return read_strategy_file(name) + except ValueError: + raise HTTPException(status_code=404, detail="策略文件不存在") + + @router.post("/strategy/file/{name}") def post_file(name: str, req: FileWriteRequest): """保存策略代码(py_compile 校验,失败 400)。""" diff --git a/tests/api/test_routes_strategy.py b/tests/api/test_routes_strategy.py index abbda10..4eb1508 100644 --- a/tests/api/test_routes_strategy.py +++ b/tests/api/test_routes_strategy.py @@ -53,3 +53,17 @@ def test_file_write_rejects_syntax_error(monkeypatch, tmp_path): # 故意写语法错误的代码 → 400(py_compile 门禁) r = c.post("/api/v1/strategy/file/__probe_bad.py", json={"code": "def (:"}) assert r.status_code == 400 + + +def test_file_read_returns_code(monkeypatch, tmp_path): + c = _client(monkeypatch, tmp_path) + files = c.get("/api/v1/strategy/files").json()["files"] + if not files: + return + name = files[0]["name"] + r = c.get(f"/api/v1/strategy/file/{name}") + assert r.status_code == 200, r.text + body = r.json() + assert isinstance(body["code"], str) and body["code"] # 编辑器空白 bug 的回归测试 + assert body["name"] == name + assert c.get("/api/v1/strategy/file/__no_such__.py").status_code == 404