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]
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user