fix(strategy): 编辑器代码空白根因=/strategy/files不含code; 加GET /strategy/file/{name}按需拉取; 运行回测改回跳参数页+按策略预填(New.vue读query.class,推翻上轮直接发起) [nas]
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)。"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user