fix(api): 控制台归因页崩修复+合成层版本树接真——两波同修(用户拍板 factor 域) [vps]
①归因页契位修复(infra 派单): 020e610 产物 factorContribution=月度长表
{factor,month,contribution_ret}(1008 条含 market/size/industry/specific
三桶行)直通撞前端 P0 短表契约 {factorId,contributionPct}——首行
contributionPct undefined → AttributionResearch.vue toFixed 整页崩
(双机实锤)。修=_aggregate_monthly_contribution 聚合适配:
- 口径=近 12 月 Σ contribution_ret×100(滚动年窗;全期 market +65%
溢出前端横条 3.2×;量级 0.1~6% 恰配)
- 三桶(market/size/industry/specific)与 12 源同板上板(研判核心结构)
- 不足 12 月(挑战者件 8 月)退化全窗 Σ;脏行跳过;降序返回
- 真件端到端验证: 1008 行→16 行与实探量级一致(market+5.85/kup+2.22)
- 前端补 typeof 兜底 filter(缺字段行滤掉防再整页崩)
②合成层版本树接真(追加派单): /pipeline/composite 真端点+mock 条目退役
- 两节点: quant12_v2a(L1 档案 resolved 权重=在位者 szneul_reb63 钉住,
releasedAt=档案 created)/quant12_v1(等权基线=#69 挑战者)
- 影子对照挂挑战者节点: 读 szneul_eq_reb63 周报 weeks_counted/
te_annual; 09-28 首跑前无周报→如实 0/4 周+TE null+进行中(不造数);
满 4 周 TE≤影子阈值达标/超阈
- 因子侧坏档 fail-soft items=[](宁空勿 500,同归因页纪律)
- 前端: 标题接 family 真名(告别 fund8 硬编码)/cand+gate 条件改
shadowCompare(告别 v3 硬编码)/TE null 显示「未算」/类型
trackingError number|null
- spec §4.7 端点节+控制台功能表两行同步(档随码走)
- 测试: tests/api +13(attribution 7+composite 6,含 jwt secret 每测试
现签防全量套件串扰); tests/api 202 全绿+tests/factor 438 全绿
+前端 vitest 17 过+vue-tsc 过
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ export interface CompositeVersion {
|
||||
releasedAt: string
|
||||
note: string
|
||||
members: CompositeMember[]
|
||||
shadowCompare?: { versus: string; week: number; totalWeeks: number; trackingError: number; verdict: '进行中' | '达标' | '超阈' }
|
||||
shadowCompare?: { versus: string; week: number; totalWeeks: number; trackingError: number | null; verdict: '进行中' | '达标' | '超阈' }
|
||||
}
|
||||
|
||||
export type LadderStage = 'backtest' | 'paper' | 'shadow' | 'live'
|
||||
@@ -120,9 +120,9 @@ export async function getMonthlyReview(): Promise<MonthlyReview> {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getCompositeVersions(): Promise<CompositeVersion[]> {
|
||||
const { data } = await apiClient.get<{ items: CompositeVersion[] }>('/pipeline/composite')
|
||||
return data.items
|
||||
export async function getCompositeVersions(): Promise<{ family: string; versions: CompositeVersion[] }> {
|
||||
const { data } = await apiClient.get<{ family: string; items: CompositeVersion[] }>('/pipeline/composite')
|
||||
return { family: data.family, versions: data.items }
|
||||
}
|
||||
|
||||
export async function getLadder(): Promise<LadderStrategy[]> {
|
||||
|
||||
@@ -121,7 +121,7 @@ export const PIPELINE_MOCK = true
|
||||
const pipelineRoutes: Route[] = [
|
||||
{ m: 'get', re: /^\/pipeline\/hypotheses$/, build: () => ({ items: P.hypothesesMock }) },
|
||||
{ m: 'get', re: /^\/pipeline\/monthly-review$/, build: () => P.monthlyReviewMock },
|
||||
{ m: 'get', re: /^\/pipeline\/composite$/, build: () => ({ items: P.compositeMock }) },
|
||||
// /pipeline/composite 已接真端点(P1, 09-27): 不再 mock, 走 sanguo_api
|
||||
{ m: 'get', re: /^\/pipeline\/factors\/[^/]+\/ic-trend$/, build: (url) => P.icTrendMock(url.split('/')[3]) },
|
||||
]
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ async function submitRuling() {
|
||||
</div>
|
||||
<div class="attr-block">
|
||||
<div class="attr-h">因子贡献分解</div>
|
||||
<div v-for="c in [...report.factorContribution].sort((a, b) => b.contributionPct - a.contributionPct)" :key="c.factorId" class="contrib">
|
||||
<div v-for="c in report.factorContribution.filter(c => typeof c.factorId === 'string' && typeof c.contributionPct === 'number').sort((a, b) => b.contributionPct - a.contributionPct)" :key="c.factorId" class="contrib">
|
||||
<span class="mono c-name">{{ c.factorId }}</span>
|
||||
<div class="c-bar-wrap"><div class="c-bar" :class="{ neg: c.contributionPct < 0 }" :style="{ width: `${Math.abs(c.contributionPct) * 3.2}%` }"></div></div>
|
||||
<span class="c-val">{{ c.contributionPct > 0 ? '+' : '' }}{{ c.contributionPct.toFixed(1) }}%</span>
|
||||
|
||||
@@ -12,21 +12,35 @@ vi.mock('@/api/pipeline', async (importOriginal) => {
|
||||
|
||||
describe('CompositeTree.vue', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it('渲染版本时间线与 v3 影子对照进度', async () => {
|
||||
vi.mocked(api.getCompositeVersions).mockResolvedValue(compositeMock)
|
||||
it('渲染版本时间线与影子对照进度(标题接 family 真名)', async () => {
|
||||
vi.mocked(api.getCompositeVersions).mockResolvedValue({
|
||||
family: 'composite_quant12', versions: compositeMock,
|
||||
})
|
||||
const w = mount(CompositeTree, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(w.text()).toContain('composite_fund8')
|
||||
expect(w.text()).toContain('composite_quant12')
|
||||
expect(w.text()).toContain('v2')
|
||||
expect(w.text()).toContain('对照 v2')
|
||||
expect(w.text()).toContain('达标')
|
||||
expect(w.text()).toContain('升级口令')
|
||||
})
|
||||
it('v2 成员含晋级双源且等权 12.5%', async () => {
|
||||
vi.mocked(api.getCompositeVersions).mockResolvedValue(compositeMock)
|
||||
vi.mocked(api.getCompositeVersions).mockResolvedValue({
|
||||
family: 'composite_quant12', versions: compositeMock,
|
||||
})
|
||||
const w = mount(CompositeTree, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(w.text()).toContain('pledge_net_chg')
|
||||
expect(w.text()).toContain('12.5%')
|
||||
})
|
||||
it('TE 未算(影子首跑前)显示「未算」不炸渲染', async () => {
|
||||
const pending = [{ ...compositeMock[2], shadowCompare: { ...compositeMock[2].shadowCompare!, trackingError: null, verdict: '进行中' as const } }]
|
||||
vi.mocked(api.getCompositeVersions).mockResolvedValue({
|
||||
family: 'composite_quant12', versions: pending,
|
||||
})
|
||||
const w = mount(CompositeTree, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(w.text()).toContain('未算')
|
||||
expect(w.text()).toContain('进行中')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,18 +4,23 @@ import { getCompositeVersions } from '@/api/pipeline'
|
||||
import type { CompositeVersion } from '@/api/pipeline'
|
||||
import PanelCard from './PanelCard.vue'
|
||||
|
||||
const family = ref('')
|
||||
const versions = ref<CompositeVersion[]>([])
|
||||
onMounted(async () => {
|
||||
versions.value = await getCompositeVersions()
|
||||
const r = await getCompositeVersions()
|
||||
family.value = r.family
|
||||
versions.value = r.versions
|
||||
})
|
||||
const pct = (wgt: number): string => `${(wgt * 100).toFixed(1)}%`
|
||||
const te = (v: CompositeVersion): string =>
|
||||
v.shadowCompare?.trackingError != null ? `${(v.shadowCompare.trackingError * 100).toFixed(1)}%` : '未算'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pl-page">
|
||||
<PanelCard title="合成层版本树 · composite_fund8" sub="策略钉住具体版本 · 变更走影子对照+口令,各策略择期升级(决议 A)">
|
||||
<PanelCard :title="`合成层版本树 · ${family}`" sub="策略钉住具体版本 · 变更走影子对照+口令,各策略择期升级(决议 A)">
|
||||
<div class="timeline">
|
||||
<div v-for="v in versions" :key="v.version" class="ver" :class="{ cand: v.version === 'v3' }">
|
||||
<div v-for="v in versions" :key="v.version" class="ver" :class="{ cand: !!v.shadowCompare }">
|
||||
<div class="ver-head">
|
||||
<span class="ver-tag">{{ v.version }}</span>
|
||||
<span class="ver-date">{{ v.releasedAt }}</span>
|
||||
@@ -28,12 +33,12 @@ const pct = (wgt: number): string => `${(wgt * 100).toFixed(1)}%`
|
||||
</div>
|
||||
<div v-if="v.shadowCompare" class="shadow">
|
||||
<span>影子对照 {{ v.shadowCompare.versus }} · 第 {{ v.shadowCompare.week }}/{{ v.shadowCompare.totalWeeks }} 周</span>
|
||||
<span>跟踪误差 {{ (v.shadowCompare.trackingError * 100).toFixed(1) }}%</span>
|
||||
<span>跟踪误差 {{ te(v) }}</span>
|
||||
<span class="verdict" :class="{ ok: v.shadowCompare.verdict === '达标' }">{{ v.shadowCompare.verdict }}</span>
|
||||
</div>
|
||||
<div v-if="v.version === 'v3'" class="gate">
|
||||
<div v-if="v.shadowCompare" class="gate">
|
||||
<el-button size="small" type="primary" plain disabled>发起升级口令</el-button>
|
||||
<span class="gate-note">P0 原型:口令闸门在 P1 接真——此处只展示流程位</span>
|
||||
<span class="gate-note">口令闸门流程位——影子对照期满由人发起升级口令</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user