feat(front+api): #79+#94+#95验收三连——#79:策略库卡片收益格移除(用户'回测·最新10.44%没啥用',回测数字口径易误导,收益看各列表/全景;latestRet/retInfoOf孤儿函数随删,pct/retClass巡检仍在);#94:实盘列表标的列接poolLabel(all→全市场)+实例列建instanceNames映射显实例名(原绑row.instance但后端list_lives从未填恒'—',对齐模拟盘#71口径,兜底实例#id);#95:标的池默认值统一改all全市场——前端三表单(paper/live/PortfolioBacktest poolForm)+后端四处默认(routes_live setdefault/routes_portfolio Field/routes_paper/orchestrator runner)同步对齐防口径分裂,标签'全市场(慢,非MVP)'简化'全市场';936绿+build绿 [vps]
CI/CD / test (push) Successful in 11s
CI/CD / nas-deploy (push) Successful in 25s
CI/CD / nas-verify (push) Successful in 10s

This commit is contained in:
2026-08-16 22:29:29 +08:00
parent 17c51c44c7
commit 6ecdaab959
9 changed files with 31 additions and 32 deletions
@@ -55,7 +55,7 @@ const dr = loadDateRange()
// §12.6 选档案(跳转带入或页面下拉),提交带 instance_id 回写四格
const selectedInstanceId = ref<number | null>(null)
const form = reactive({
pool: 'hs300_subset',
pool: 'all',
strategy: 'all_weather',
max_pool: 30,
start: dr.start,
@@ -81,7 +81,7 @@ watch(
const poolOptions = [
{ label: 'HS300 子集(小范围验证)', value: 'hs300_subset' },
{ label: '全市场(慢,非 MVP)', value: 'all' },
{ label: '全市场', value: 'all' },
]
const strategyOptions = [
{ label: '全天候轮动', value: 'all_weather' },
+20 -3
View File
@@ -2,6 +2,8 @@
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import CodeSnapshotDrawer from '@/views/strategy/CodeSnapshotDrawer.vue'
import { poolLabel } from '@/constants/strategy'
import { getInstances } from '@/api/strategy'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listLives, startLive, stopLive, deleteLive, updateLive,
@@ -73,6 +75,21 @@ function num(v: number | null | undefined): string {
return Math.round(v).toLocaleString('zh-CN')
}
// #94 instance_id list_lives instance
const instanceNames = ref<Record<number, string>>({})
onMounted(async () => {
try {
const ins = await getInstances()
instanceNames.value = Object.fromEntries(ins.map((i) => [i.id, i.name]))
} catch {
/* 映射失败退化为 实例 #id */
}
})
function instanceNameOf(instanceId: number | null | undefined): string {
if (!instanceId) return '—'
return instanceNames.value[instanceId] || `实例 #${instanceId}`
}
function goMonitor(a: LiveAccount): void {
router.push(`/live/monitor/${a.id}`)
}
@@ -253,7 +270,7 @@ async function onStop(a: LiveAccount): Promise<void> {
</template>
</el-table-column>
<el-table-column label="标的" min-width="110">
<template #default="{ row }"><span class="mono">{{ row.vt_symbol }}</span></template>
<template #default="{ row }"><span class="mono">{{ poolLabel(row.vt_symbol) }}</span></template>
</el-table-column>
<el-table-column label="策略实例" min-width="160">
<template #default="{ row }">
@@ -261,8 +278,8 @@ async function onStop(a: LiveAccount): Promise<void> {
<div class="cell-id mono muted">{{ row.strategy_class }}</div>
</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 label="实例" min-width="160">
<template #default="{ row }"><span class="mono">{{ instanceNameOf(row.instance_id) }}</span></template>
</el-table-column>
<el-table-column label="频率" width="70">
<template #default="{ row }"><span class="mono">{{ row.interval }}</span></template>
+2 -2
View File
@@ -19,10 +19,10 @@ const isPortfolio = computed(() => strategyType.value === 'portfolio')
const portfolioOptions = ref<string[]>([])
const portfolioStrategy = ref('all_weather')
const poolForm = ref({ pool: 'hs300_subset', max_pool: 30, benchmark: '000300.XSHG' })
const poolForm = ref({ pool: 'all', max_pool: 30, benchmark: '000300.XSHG' })
const POOL_OPTIONS = [
{ label: 'HS300 子集(小范围验证)', value: 'hs300_subset' },
{ label: '全市场(慢,非 MVP)', value: 'all' },
{ label: '全市场', value: 'all' },
]
const BENCH_OPTIONS = [
{ label: '沪深300', value: '000300.XSHG' },
+2 -2
View File
@@ -27,10 +27,10 @@ const instancesOfCta = computed(() => {
const file = classToFile.value[form.value.strategies[0]?.name]
return file ? instanceOptions.value.filter((i) => i.code_file === file) : instanceOptions.value
})
const poolForm = reactive({ pool: 'hs300_subset', max_pool: 30, benchmark: '000300.XSHG' })
const poolForm = reactive({ pool: 'all', max_pool: 30, benchmark: '000300.XSHG' })
const POOL_OPTIONS = [
{ label: 'HS300 子集(小范围验证)', value: 'hs300_subset' },
{ label: '全市场(慢,非 MVP)', value: 'all' },
{ label: '全市场', value: 'all' },
]
const BENCH_OPTIONS = [
{ label: '沪深300', value: '000300.XSHG' },
+1 -19
View File
@@ -115,23 +115,6 @@ const lampOf = (s: string): string => {
}
const pct = (v: number | null | undefined): string => (v == null ? '—' : (v * 100).toFixed(2) + '%')
const retClass = (v: number | null | undefined): string => (v == null ? '' : v >= 0 ? 'up' : 'down')
// #75 ret / run_returns
interface RetInfo { v: number | null; label: string }
function retInfoOf(i: Instance): RetInfo {
const runs = (i.running_accounts || []).filter((a) => a.ret != null)
if (runs.length) {
const last = runs[runs.length - 1]
const label = last.kind === 'live' ? '实盘 · 最新' : last.kind === 'shadow' ? '影子 · 最新' : '模拟实走 · 最新'
return { v: last.ret, label }
}
const rr = (i as Instance & { run_returns?: Record<string, number> }).run_returns || {}
if (rr.backtest != null) return { v: rr.backtest, label: '回测 · 最新' }
if (rr.replay != null) return { v: rr.replay, label: '回放 · 最新' }
return { v: null, label: '未运行' }
}
function latestRet(i: Instance): number | null {
return retInfoOf(i).v
}
const paramSummary = (p: Record<string, unknown>): string =>
Object.entries(p).map(([k, v]) => `${k}=${v}`).join(' · ')
@@ -327,8 +310,7 @@ const runningRows = computed(() =>
<span class="sub mono">#{{ i.id }} · {{ i.interval }} · 更新 {{ i.updated_at }}</span>
</div>
<div class="params mono muted">{{ paramSummary(i.params) }}</div>
<!-- #91: 标的行移除#86a 定稿实例不承载标的(发起时定),老实例存量值显示只会误导 -->
<div class="ret mono" :class="retClass(latestRet(i))">{{ pct(latestRet(i)) }}<span class="sub">{{ retInfoOf(i).label }}</span></div>
<!-- #91: 标的行移除;#79: 收益格移除回测数字口径易误导,收益看各列表/全景(口径干净) -->
<div class="runs">
<button class="run-btn" @click="runGo(i, 'backtest')"><span class="lamp" :class="lampOf(i.status.backtest)"></span>回测{{ badge(i, 'backtest') }}</button>
<button v-if="fileTypeOf(i) !== 'portfolio'" class="run-btn" @click="runGo(i, 'replay')"><span class="lamp" :class="lampOf(i.status.replay)"></span>回放{{ badge(i, 'replay') }}</button>
+1 -1
View File
@@ -138,7 +138,7 @@ def create_live(req: LiveCreateRequest):
# 组合实盘:vt_symbol 占位为池名;setting 存组合参数(supervisor 转发 env)
if not payload.get("strategy_class"):
raise HTTPException(400, "组合实盘需选择策略(strategy_class)")
payload.setdefault("pool", "hs300_subset")
payload.setdefault("pool", "all")
payload.setdefault("max_pool", 30)
payload.setdefault("benchmark", "000300.XSHG")
payload["vt_symbol"] = payload["pool"]
+1 -1
View File
@@ -61,7 +61,7 @@ class PaperCreateRequest(BaseModel):
strategy_type: str = "cta"
# 撮合引擎(影子柜台 P1):eod_replay=日终回放(NAS 20:30) / shadow=影子柜台(VPS 盘中实时)
engine: str = "eod_replay"
pool: str = "hs300_subset"
pool: str = "all"
max_pool: int = 30
benchmark: str = "000300.XSHG"
# §12.6 实例做实:账户绑档案;空=发起即建档(自动创建实例再发起)
+1 -1
View File
@@ -29,7 +29,7 @@ async def verify_token(authorization: str | None = Header(None)):
class PortfolioBacktestRequest(BaseModel):
pool: str = Field(default="hs300_subset", description="标的池(占位,MVP 用默认)")
pool: str = Field(default="all", description="标的池(all=全市场)")
start_date: str = Field(default="2024-01-01", description="YYYY-MM-DD")
end_date: str = Field(default="2024-02-29", description="YYYY-MM-DD")
initial_cash: float = Field(default=1_000_000.0, description="初始资金(元)")
+1 -1
View File
@@ -140,7 +140,7 @@ class Orchestrator:
async def submit_portfolio(self, start: str, end: str, cash: float,
benchmark: str, strategy: str = "all_weather",
max_pool: int = 30,
pool: str = "hs300_subset",
pool: str = "all",
provider_config=None,
commission_rate: float = 0.0003,
stamp_duty_rate: float = 0.001,