fix(strategy): 编辑器代码空白根因=/strategy/files不含code; 加GET /strategy/file/{name}按需拉取; 运行回测改回跳参数页+按策略预填(New.vue读query.class,推翻上轮直接发起) [nas]

This commit is contained in:
2026-08-13 19:40:18 +08:00
parent b9d9285246
commit 82c9edb092
4 changed files with 49 additions and 71 deletions
+4
View File
@@ -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
}
+22 -71
View File
@@ -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<void> {
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<void> {
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<void> {
}
}
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
// 跳到对应的回测参数页:初始值按当前策略预填,区间等参数由人调整
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<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
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 } })
}
}
</script>
@@ -192,7 +143,7 @@ async function runBacktest(): Promise<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" :disabled="running" @click="runBacktest">{{ running ? '提交中' : '运行回测' }}</button>
<button class="term-btn sm" :disabled="loadingCode" @click="runBacktest">运行回测</button>
</div>
</div>
<div class="monaco-wrap">
+9
View File
@@ -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)。"""
+14
View File
@@ -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