fix(strategy): 实例做实审计补漏6项——①回测表单提交透传instance_id(CTA/组合两页原只预填不传,四格回写链路断头;组合页补档案预填max_pool/benchmark/pool)②模拟盘/实盘列表支持?instance=N按档案过滤(策略库行加模拟历史/实盘历史按钮,过滤中提示条可退出)③live创建补D1快照(setting用档案参数,API直调不绕过)④实盘账户也标参数漂移角标(D2只亮不同步;account_params_drifted兼容live setting字段)⑤paper页PORTFOLIO_LABELS换STRATEGY_LABELS共享⑥同步生效时机实证标注(实走每日结算重读DB次晚生效;影子进程常驻内存需重启,实证live_step/runner.py均从DB读);921绿+build绿 [vps]
CI/CD / test (push) Successful in 14s
CI/CD / nas-deploy (push) Successful in 45s
CI/CD / nas-verify (push) Successful in 10s

This commit is contained in:
2026-08-15 22:54:02 +08:00
parent 256820d36f
commit 2580b5c080
12 changed files with 79 additions and 18 deletions
+2
View File
@@ -11,6 +11,8 @@ export interface CtaSubmit {
slippage?: number
commission_rate?: number
stamp_duty_rate?: number
// §12.6 可选关联档案(带了则结果回写档案四格)
instance_id?: number | null
}
export interface TaskStatus {
+2
View File
@@ -15,6 +15,8 @@ export interface PortfolioBacktestReq {
slippage?: number
// K线周期(组合回放暂仅日线 d)
interval?: string
// §12.6 可选关联档案(带了则结果回写档案四格)
instance_id?: number | null
}
export interface EquityPoint {
+1
View File
@@ -167,6 +167,7 @@ async function onSubmit(): Promise<void> {
commission_rate: Number(form.commission_rate),
stamp_duty_rate: Number(form.stamp_duty_rate),
slippage: Number(form.slippage),
instance_id: route.query.instance && !Array.isArray(route.query.instance) ? Number(route.query.instance) : null,
})
ElMessage.success('回测已提交')
void tid
@@ -6,6 +6,7 @@ import type { EChartsCoreOption } from 'echarts'
import { useChart } from '@/composables/useChart'
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
import { getTaskParams } from '@/api/backtest'
import { apiClient } from '@/api/client'
import { disableFutureDate } from '@/utils/dates'
import { INTERVAL_OPTIONS } from '@/constants/intervals'
import {
@@ -370,6 +371,7 @@ async function onSubmit(): Promise<void> {
min_commission: Number(form.min_commission),
slippage: Number(form.slippage),
interval: form.interval,
instance_id: route.query.instance && !Array.isArray(route.query.instance) ? Number(route.query.instance) : null,
})
ElMessage.success('回测已提交,后台运行中')
router.push('/backtest/history') // 跳任务中心(历史任务页)看进度/结果
@@ -391,8 +393,27 @@ onMounted(() => {
// 从任务列表「点任务ID」跳转:按该任务参数完整预填
void prefillFromTask(fromTaskId.value)
}
// §12.6 从策略库档案发起:按档案参数预填(max_pool/benchmark 等)
const instQ = route.query.instance
if (instQ && !Array.isArray(instQ)) void prefillFromInstance(Number(instQ))
})
async function prefillFromInstance(instId: number): Promise<void> {
try {
const { data } = await apiClient.get<{
instance: { name?: string; code_file: string; params: Record<string, unknown>; symbol_or_pool?: string }
}>(`/strategy/instances/${instId}`)
const inst = data.instance
if (inst.code_file) form.strategy = inst.code_file.replace(/\.py$/, '')
const p = inst.params || {}
if (typeof p.max_pool === 'number') form.max_pool = p.max_pool
if (typeof p.benchmark === 'string') form.benchmark = p.benchmark
if (inst.symbol_or_pool && inst.symbol_or_pool !== '') form.pool = inst.symbol_or_pool
} catch {
/* 预填失败走默认 */
}
}
async function prefillFromTask(tid: string): Promise<void> {
try {
const tp = await getTaskParams(tid)
+11 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listLives, startLive, stopLive, deleteLive, updateLive,
@@ -8,9 +8,14 @@ import {
} from '@/api/live'
const router = useRouter()
const route = useRoute()
const accounts = ref<LiveAccount[]>([])
const loading = ref(false)
const kw = ref('')
// §12.6 按档案过滤(策略库「历史」跳转 ?instance=N
const instFilter = route.query.instance && !Array.isArray(route.query.instance)
? ref<number | null>(Number(route.query.instance))
: ref<number | null>(null)
const statusFilter = ref('')
const actionLoading = ref<number | null>(null)
let timer: ReturnType<typeof setInterval> | null = null
@@ -42,6 +47,7 @@ async function refreshSilent(): Promise<void> {
const filtered = computed(() =>
accounts.value.filter((a) => {
if (instFilter.value != null && (a as { instance_id?: number | null }).instance_id !== instFilter.value) return false
if (kw.value) {
const hay = `${a.name} ${a.id} ${a.vt_symbol} ${a.strategy_name}`.toLowerCase()
if (!hay.includes(kw.value.toLowerCase())) return false
@@ -159,7 +165,10 @@ async function onStop(a: LiveAccount): Promise<void> {
<h2 class="page-title">实盘模拟 <span class="count-badge">{{ stats.total }}</span></h2>
<p class="page-subtitle">miniQMT 直连 · supervisor 轮询 · 15s 自动刷新</p>
</div>
<el-button type="primary" @click="goNew">+ 新建实盘</el-button>
<div style="display:flex;gap:8px;align-items:center">
<el-button v-if="instFilter != null" size="small" @click="router.push('/live')">档案 #{{ instFilter }} 过滤中 · 查看</el-button>
<el-button type="primary" @click="goNew">+ 新建实盘</el-button>
</div>
</div>
<!-- 统计 -->
+12 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listPapers, stopPaper, resumePaper, deletePaper, updatePaper,
@@ -8,9 +8,14 @@ import {
} from '@/api/paper'
const router = useRouter()
const route = useRoute()
const accounts = ref<PaperAccount[]>([])
const loading = ref(false)
const kw = ref('')
// §12.6 ?instance=N
const instFilter = route.query.instance && !Array.isArray(route.query.instance)
? ref<number | null>(Number(route.query.instance))
: ref<number | null>(null)
const modeFilter = ref('')
const statusFilter = ref('')
const actionLoading = ref(0)
@@ -30,6 +35,8 @@ async function refresh(): Promise<void> {
const filtered = computed(() =>
accounts.value.filter((a) => {
// §12.6 ?instance=N
if (instFilter.value != null && (a as { instance_id?: number | null }).instance_id !== instFilter.value) return false
if (kw.value && !String(a.name + a.id + (a.symbols ?? '')).toLowerCase().includes(kw.value.toLowerCase())) return false
if (modeFilter.value && a.mode !== modeFilter.value) return false
if (statusFilter.value && a.status !== statusFilter.value) return false
@@ -168,7 +175,10 @@ async function saveEdit(): Promise<void> {
<h2 class="page-title">模拟交易 <span class="count-badge">{{ stats.total }}</span></h2>
<p class="page-subtitle">回放 / 实走 · 策略组合模拟盘</p>
</div>
<el-button type="primary" @click="goNew">+ 新建模拟盘</el-button>
<div style="display:flex;gap:8px;align-items:center">
<el-button v-if="instFilter != null" size="small" @click="router.push('/paper')">档案 #{{ instFilter }} 过滤中 · 查看</el-button>
<el-button type="primary" @click="goNew">+ 新建模拟盘</el-button>
</div>
</div>
<!-- 统计 -->
+2 -7
View File
@@ -6,6 +6,7 @@ import { createPaper, type PaperCreate } from '@/api/paper'
import { apiClient } from '@/api/client'
import { getStrategies, getInstances, type StrategyItem, type Instance } from '@/api/strategy'
import { INTERVAL_OPTIONS } from '@/constants/intervals'
import { STRATEGY_LABELS } from '@/constants/strategy'
import { disableFutureDate } from '@/utils/dates'
const route = useRoute()
@@ -31,13 +32,7 @@ const BENCH_OPTIONS = [
{ label: '中证1000', value: '000852.XSHG' },
{ label: '中证2000', value: '932000.XSHG' },
]
const PORTFOLIO_LABELS: Record<string, string> = {
all_weather: '全天候轮动',
momentum_timing: '牛熊动量',
value_selection: '价值精选',
small_cap: '小市值轮动',
channel_test: '通路测试(影子vs实盘双轨)',
}
const PORTFOLIO_LABELS = STRATEGY_LABELS
onMounted(async () => {
try {
+3 -1
View File
@@ -160,7 +160,7 @@ function runLive(i: Instance): void {
async function onSync(i: Instance): Promise<void> {
try {
await ElMessageBox.confirm(
`把档案「${i.name}」当前参数同步到全部运行中的模拟账户实走+影子一起,明晚结算生效)?实盘不受影响`,
`把档案「${i.name}」当前参数同步到全部运行中的模拟账户实走明晚结算生效;影子账户需重启影子进程生效。实盘不受影响(停了重发)`,
'参数同步', { type: 'warning', confirmButtonText: '同步', cancelButtonText: '取消' },
)
} catch {
@@ -306,6 +306,8 @@ const runningRows = computed(() =>
</div>
<div class="ops">
<button v-if="i.drift" class="term-btn sm warn" :disabled="syncing" @click="onSync(i)">同步</button>
<button class="term-btn sm" @click="router.push(`/paper?instance=${i.id}`)">模拟历史</button>
<button class="term-btn sm" @click="router.push(`/live?instance=${i.id}`)">实盘历史</button>
<button class="term-btn sm" @click="overviewId = i.id">全景</button>
<button class="term-btn sm" @click="editInstance(i)">编辑</button>
<button class="term-btn sm danger" @click="onDelInst(i)">删除</button>