feat(shadow-desk): 通路测试策略channel_test+模拟盘列表改造: 策略=每日等权轮换ETF/蓝筹(卖旧买新走全买卖通路)+T+1当日卖探针(两端应一致拒单,验证拒单通路),注册backtest/live/前端,默认universe 5只高流动ETF,4单测(坑:BrokerFacade是dataclass,子类方法被父类__init__实例属性遮蔽须实例注入); 模拟盘列表:加类型列(个股/组合)+创建时间列,标的去JSON中括号,列宽重排,模式筛选加影子 [vps]
CI/CD / test (push) Successful in 12s
CI/CD / nas-deploy (push) Successful in 28s
CI/CD / nas-verify (push) Successful in 9s

This commit is contained in:
2026-08-14 14:32:52 +08:00
parent 6220fc5943
commit 36db2ed5a6
10 changed files with 256 additions and 21 deletions
+1
View File
@@ -37,6 +37,7 @@ export interface PaperAccount {
initial_capital?: number
start_date?: string
end_date?: string
created_at?: string
last_run_date?: string | null
next_run_at?: string | null
checkpoint_date?: string | null
+5
View File
@@ -40,6 +40,11 @@
border-color: rgba(0, 229, 255, 0.3);
margin-left: 4px;
}
.chip-cta {
color: #6ee7a8;
background: rgba(110, 231, 168, 0.1);
border-color: rgba(110, 231, 168, 0.3);
}
.chip-portfolio {
color: var(--amber);
background: rgba(255, 176, 0, 0.08);
@@ -83,6 +83,7 @@ const strategyOptions = [
{ label: '牛熊动量', value: 'momentum_timing' },
{ label: '价值精选', value: 'value_selection' },
{ label: '小市值轮动', value: 'small_cap' },
{ label: '通路测试(影子vs实盘双轨)', value: 'channel_test' },
]
const BENCHMARK_OPTIONS = [
{ label: '沪深300', value: '000300.XSHG' },
+35 -19
View File
@@ -57,11 +57,22 @@ function num(v: number | null | undefined): string {
}
function symbolsShort(s: string | undefined): string {
if (!s) return '—'
const arr = typeof s === 'string' ? s.split(',') : []
return arr.length > 3 ? arr.slice(0, 3).join(',') + ` +${arr.length - 3}` : s
let arr: string[] = []
try {
arr = JSON.parse(s)
} catch {
arr = String(s).split(',').map((x) => x.trim())
}
if (!arr.length) return '—'
return arr.length > 3 ? arr.slice(0, 3).join(',') + ` +${arr.length - 3}` : arr.join(',')
}
const typeLabel: Record<string, string> = { cta: '个股', portfolio: '组合' }
function createdShort(v: string | undefined): string {
if (!v) return '—'
return v.replace('T', ' ').slice(0, 16)
}
function open(a: PaperAccount): void {
router.push(a.mode === 'live' ? `/paper/live/${a.id}` : `/paper/result/${a.id}`)
router.push(a.mode === 'replay' ? `/paper/result/${a.id}` : `/paper/live/${a.id}`)
}
function goNew(): void {
router.push('/paper/new')
@@ -170,6 +181,7 @@ async function saveEdit(): Promise<void> {
<el-select v-model="modeFilter" placeholder="全部模式" clearable style="width: 130px">
<el-option label="回放" value="replay" />
<el-option label="实走" value="live" />
<el-option label="影子" value="shadow" />
</el-select>
<el-select v-model="statusFilter" placeholder="全部状态" clearable style="width: 130px">
<el-option label="运行中" value="running" />
@@ -181,51 +193,55 @@ async function saveEdit(): Promise<void> {
<el-card class="table-card" shadow="never">
<el-table :data="filtered" size="small" empty-text="暂无模拟盘">
<el-table-column label="名称 / ID" min-width="160">
<el-table-column label="名称 / ID" min-width="150">
<template #default="{ row }">
<div class="cell-name">{{ row.name }}</div>
<div class="cell-id mono">#{{ row.id }}</div>
</template>
</el-table-column>
<el-table-column label="模式" width="80">
<el-table-column label="类型" width="76">
<template #default="{ row }">
<span class="chip" :class="row.strategy_type === 'portfolio' ? 'chip-portfolio' : 'chip-cta'">
{{ typeLabel[row.strategy_type] || row.strategy_type || '个股' }}
</span>
</template>
</el-table-column>
<el-table-column label="模式" width="92">
<template #default="{ row }">
<span class="chip chip-paper" :class="{ 'chip-shadow': row.mode === 'shadow' }">{{ modeLabel[row.mode] || row.mode }}</span>
<span v-if="row.strategy_type === 'portfolio' && row.mode === 'live'" class="chip chip-shadow" title="日终回放=每晚 20:30 全量重放结算">日终</span>
</template>
</el-table-column>
<el-table-column label="频率" width="70">
<template #default="{ row }"><span class="mono">{{ row.interval }}</span></template>
<el-table-column label="频率" width="64">
<template #default="{ row }"><span class="mono muted">{{ row.interval }}</span></template>
</el-table-column>
<el-table-column prop="instance" label="实例" min-width="160">
<template #default="{ row }"><span class="mono">{{ row.instance || '—' }}</span></template>
</el-table-column>
<el-table-column label="标的" min-width="120">
<el-table-column label="标的" min-width="130">
<template #default="{ row }"><span class="mono">{{ symbolsShort(row.symbols) }}</span></template>
</el-table-column>
<el-table-column label="收益率" width="100" align="right">
<el-table-column label="收益率" width="86" align="right">
<template #default="{ row }">
<span class="mono" :class="(row.total_return ?? 0) >= 0 ? 'up' : 'down'">{{ pct(row.total_return) }}</span>
</template>
</el-table-column>
<el-table-column label="最新净值" width="120" align="right">
<el-table-column label="最新净值" width="104" align="right">
<template #default="{ row }"><span class="mono">{{ num(row.latest_equity) }}</span></template>
</el-table-column>
<el-table-column label="起止" min-width="180">
<template #default="{ row }"><span class="mono muted">{{ row.start_date }} ~ {{ row.end_date }}</span></template>
<el-table-column label="创建时间" width="142">
<template #default="{ row }"><span class="mono muted">{{ createdShort(row.created_at) }}</span></template>
</el-table-column>
<el-table-column label="状态" width="84">
<el-table-column label="状态" width="80">
<template #default="{ row }"><span class="chip" :class="`st-${row.status}`">{{ statusLabel[row.status] || row.status }}</span></template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="176" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="open(row)">查看</el-button>
<el-button
v-if="row.mode === 'live' && row.status === 'running'"
v-if="row.mode !== 'replay' && row.status === 'running'"
link type="warning" :loading="actionLoading === row.id"
@click="onStop(row)"
>停止</el-button>
<el-button
v-if="row.mode === 'live' && row.status === 'stopped'"
v-if="row.mode !== 'replay' && row.status === 'stopped'"
link type="success" :loading="actionLoading === row.id"
@click="onResume(row)"
>恢复</el-button>
+1
View File
@@ -32,6 +32,7 @@ const PORTFOLIO_LABELS: Record<string, string> = {
momentum_timing: '牛熊动量',
value_selection: '价值精选',
small_cap: '小市值轮动',
channel_test: '通路测试(影子vs实盘双轨)',
}
onMounted(async () => {