feat(strategy): 代码版本快照(§12.6补,方案B发起时快照·QuantConnect轻量版)——用户拍板:解决「实例参数有快照但代码没有,历史运行无法回溯当时跑的哪版代码」——①sanguo_api/code_versions.py:发起时全文落盘data/strategy_code_versions/{file}.{hash8}.py(md5内容寻址天然去重,原子替换防并发坏);发起四入口(paper/live/CTA/组合回测)全快照,账户加code_hash列(ALTER迁移),回测经spec→run_meta.code_hash落档案②查看:GET /strategy/code-versions列表+单版本全文;代码编辑页「版本历史」抽屉(MonacoDiff左右对比当前,主题同款)③「代码已变更」角标:策略库档案行(enriched code_changed)+全景每run标v哈希·一致/已变更④双轨对账加「代码版本一致」第五指标(对账FAIL先查这行,两边代码不同价差必然大);路径穿越防护(版本号只认8位hex)+6测试;927绿+build绿;「用当时代码重跑」留P2 [vps]
CI/CD / test (push) Successful in 13s
CI/CD / nas-deploy (push) Successful in 1m1s
CI/CD / nas-verify (push) Successful in 19s

This commit is contained in:
2026-08-15 23:38:07 +08:00
parent 1103ae85d5
commit 6b07389c85
20 changed files with 549 additions and 18 deletions
+7
View File
@@ -99,6 +99,13 @@ function sideText(s: string): string {
</el-tag>
<el-tag v-else size="small" type="info">净值序列不全</el-tag>
</div>
<div class="stat">
<span class="stat-label">代码版本一致§12.6 快照</span>
<span class="stat-value mono">{{ p.code_match == null ? '—' : '双端快照' }}</span>
<el-tag v-if="p.code_match === true" size="small" type="success">一致</el-tag>
<el-tag v-else-if="p.code_match === false" size="small" type="danger">不一致先查这个</el-tag>
<el-tag v-else size="small" type="info">无快照</el-tag>
</div>
</div>
<div class="pair-nav mono muted" v-if="p.report.nav.note">
+68
View File
@@ -3,7 +3,9 @@ import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import MonacoEditor from '@/components/MonacoEditor.vue'
import MonacoDiff from '@/components/MonacoDiff.vue'
import { apiClient } from '@/api/client'
import { getCodeVersions, getCodeVersion, type CodeVersion } from '@/api/strategy'
interface StrategyFile {
name: string
@@ -132,6 +134,35 @@ function runBacktest(): void {
router.push({ path: '/backtest/new', query: { class: active.value.class_name } })
}
}
// ===== §12.6 补:版本历史(发起时快照)=====
const versions = ref<CodeVersion[]>([])
const versionsOpen = ref(false)
const diffCode = ref<string | null>(null)
const versionsLoading = ref(false)
async function openVersions(): Promise<void> {
if (!active.value) return
versionsOpen.value = true
versionsLoading.value = true
diffCode.value = null
try {
versions.value = await getCodeVersions(active.value.name)
} catch {
ElMessage.error('版本列表加载失败')
} finally {
versionsLoading.value = false
}
}
async function showDiff(h8: string): Promise<void> {
if (!active.value) return
try {
diffCode.value = await getCodeVersion(active.value.name, h8)
} catch {
ElMessage.error('读取版本失败')
}
}
</script>
<template>
@@ -184,6 +215,7 @@ function runBacktest(): void {
<div class="bar-right">
<span class="modified mono">{{ active?.modified }}</span>
<button class="term-btn sm" :disabled="checking" @click="onCheck">{{ checking ? '校验中' : '语法检查' }}</button>
<button class="term-btn sm" :disabled="loadingCode" @click="openVersions">版本历史</button>
<button class="term-btn sm primary" :disabled="!dirty || saving" @click="onSave">{{ saving ? '保存中' : '保存' }}</button>
<button class="term-btn sm" :disabled="loadingCode" @click="runBacktest">运行回测</button>
</div>
@@ -193,6 +225,32 @@ function runBacktest(): void {
</div>
</section>
</div>
<!-- §12.6 版本历史发起时快照列表 + 与当前代码 diff -->
<el-drawer :model-value="versionsOpen" size="860px" :with-header="false" append-to-body destroy-on-close @close="versionsOpen = false">
<div v-loading="versionsLoading" class="ver-drawer">
<div class="ver-head">
<div>
<div style="font-size:15px;font-weight:700;color:var(--text)">版本历史 · {{ activeName }}</div>
<div class="muted" style="font-size:11.5px;margin-top:2px">发起回测/模拟/实盘时的代码快照内容寻址去重对比查看与当前代码差异</div>
</div>
<button class="term-btn sm" @click="versionsOpen = false">关闭</button>
</div>
<div v-if="!versions.length && !versionsLoading" class="muted" style="padding:20px 0;text-align:center;font-size:12.5px">
还没有快照从策略档案发起过回测/模拟/实盘后自动生成
</div>
<div v-for="v in versions" :key="v.code_version" class="ver-row">
<span class="mono" style="color:var(--brand)">v{{ v.code_version }}</span>
<span class="muted mono" style="font-size:11px">{{ v.saved_at }}</span>
<span class="muted mono" style="font-size:11px">{{ (v.size / 1024).toFixed(1) }} KB</span>
<button class="term-btn sm" style="margin-left:auto" @click="showDiff(v.code_version)">对比当前</button>
</div>
<div v-if="diffCode != null" class="ver-diff">
<div class="muted mono" style="font-size:11px;margin-bottom:6px">=快照版本 · =当前代码</div>
<MonacoDiff :original="diffCode" :modified="code" />
</div>
</div>
</el-drawer>
</div>
</template>
@@ -324,4 +382,14 @@ function runBacktest(): void {
min-height: 0;
background: #05080d;
}
/* 版本历史抽屉 */
.ver-drawer { display: flex; flex-direction: column; gap: 10px; padding: 4px 2px; }
.ver-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.ver-row {
display: flex; align-items: center; gap: 12px; padding: 8px 10px;
border: 1px solid var(--border-2); border-radius: var(--r-sm);
}
.ver-row:hover { background: var(--bg-hover); }
.ver-diff { height: 460px; border: 1px solid var(--border-2); border-radius: var(--r-sm); overflow: hidden; }
</style>
@@ -80,6 +80,9 @@ const stLabel: Record<string, string> = { running: '运行中', done: '已完成
<span class="chip" :class="r.kind === 'live' ? 'chip-cta' : 'chip-portfolio'">{{ kindLabel[r.kind] }}</span>
<span class="mono" style="font-size:11px">{{ r.label || `#${r.aid}` }}</span>
<span class="muted mono" style="font-size:10px">{{ stLabel[r.status || ''] || r.status }}</span>
<span v-if="r.code_version" class="mono" style="font-size:10px" :style="{ color: r.code_changed ? 'var(--amber)' : 'var(--text-3)' }" :title="r.code_changed ? '发起后代码已修改' : '与当前代码一致'">
v{{ r.code_version }}{{ r.code_changed ? ' · 已变更' : '' }}
</span>
<span class="mono ret" :class="retClass(r.ret)" style="font-weight:700">{{ pct(r.ret) }}</span>
</div>
<div v-if="!data.runs.length" class="muted" style="font-size:12px;padding:8px 0">该档案还没有账户运行</div>
+1
View File
@@ -289,6 +289,7 @@ const runningRows = computed(() =>
<div class="iname" @click="editInstance(i)">
{{ i.name }}
<span v-if="i.drift" class="chip drift">参数已漂移</span>
<span v-if="i.code_changed" class="chip drift" title="发起后策略代码被修改过,在跑账户仍是旧代码">代码已变更</span>
<span class="sub mono">#{{ i.id }} · {{ i.interval }} · 更新 {{ i.updated_at }}</span>
</div>
<div class="params mono muted">{{ paramSummary(i.params) }}</div>