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]
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface PortfolioBacktestReq {
|
||||
slippage?: number
|
||||
// K线周期(组合回放暂仅日线 d)
|
||||
interval?: string
|
||||
// §12.6 可选关联档案(带了则结果回写档案四格)
|
||||
instance_id?: number | null
|
||||
}
|
||||
|
||||
export interface EquityPoint {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 统计 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 统计 -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user