feat: 预测性成本智能·宏观敏感性因素联动(IMA 2026.7完整版)

- 内置3宏观因素: 原油价格/美元汇率/CPI通胀率
- 敏感性引擎: 按KPI类别推断弹性(成本类油价0.15/利润类0.12/营收类0.08), 方向+因素涨KPI涨
- 负值KPI(亏损)方向反转修复: 油价涨→净利更亏
- API: GET /predict/kpi-forecast/sensitivity?pct=10 → KPI×因素矩阵(±pct调整后预测)
- 前端: 敏感性幅度选择(±5/10/20%) + 敏感性矩阵表(同向/反向+↑↓调整值)
- 诚实标注: 模型弹性(规则推断,非历史回归), 后续可用宏观历史数据回归校准
- 验证: Chrome实测页面+API矩阵, pytest 46 passed
This commit is contained in:
Hermes CI Fix
2026-08-25 00:44:45 +08:00
parent 1c8b01d682
commit 8ec846c6df
4 changed files with 217 additions and 3 deletions
+1
View File
@@ -223,6 +223,7 @@ export const predictApi = {
// KPI趋势预测(预测性成本智能)
kpiForecast: (params?: any) => api.get('/predict/kpi-forecast', { params }),
kpiForecastFinance: (params?: any) => api.get('/predict/kpi-forecast/finance', { params }),
kpiForecastSensitivity: (params?: any) => api.get('/predict/kpi-forecast/sensitivity', { params }),
}
export const deviationPushApi = {
+72 -2
View File
@@ -15,7 +15,13 @@
<el-option label="6期" :value="6" />
<el-option label="12期" :value="12" />
</el-select>
<el-button type="primary" :loading="loading" style="margin-left: 16px;" @click="loadData">刷新预测</el-button>
<span class="ctrl-label" style="margin-left: 16px;">敏感性幅度</span>
<el-select v-model="sensPct" style="width: 90px" @change="loadSensitivity">
<el-option label="±5%" :value="5" />
<el-option label="±10%" :value="10" />
<el-option label="±20%" :value="20" />
</el-select>
<el-button type="primary" :loading="loading" style="margin-left: 16px;" @click="loadAll">刷新预测</el-button>
</div>
<div class="ctrl-right">
<span class="mapping-hint">基于 kpi_values 历史数据 · 线性回归/移动平均 · 置信度诚实标注</span>
@@ -92,6 +98,39 @@
</el-col>
</el-row>
<!-- 宏观敏感性联动IMA 2026.7 -->
<el-card shadow="never" style="margin-top: 12px;">
<template #header>
<div class="card-header">
<span>🌐 宏观敏感性因素联动</span>
<el-tag size="small" type="warning">模型弹性规则推断非历史回归</el-tag>
<span class="empty-hint">因素变动 {{ sensPct }}% 对预测值的影响</span>
</div>
</template>
<el-table :data="sensList" size="small" border stripe max-height="360">
<el-table-column prop="kpi.name" label="KPI" min-width="110" show-overflow-tooltip />
<el-table-column label="类别" width="70" align="center">
<template #default="{ row }">{{ catLabel(row.category) }}</template>
</el-table-column>
<el-table-column label="基准预测" width="100" align="right">
<template #default="{ row }">{{ fmtVal(row.next_target) }}</template>
</el-table-column>
<el-table-column v-for="f in sensFactors" :key="f.key" :label="f.name" min-width="150" align="center">
<template #default="{ row }">
<template v-if="factorOf(row, f.key)">
<span :style="{ color: factorOf(row, f.key).direction === '+' ? '#F56C6C' : '#67C23A' }">
{{ factorOf(row, f.key).direction === '+' ? '同向' : '反向' }}
</span>
<span class="sens-val">{{ fmtVal(factorOf(row, f.key).adj_up) }} {{ fmtVal(factorOf(row, f.key).adj_down) }}</span>
</template>
</template>
</el-table-column>
</el-table>
<div class="note-line" style="margin-top:8px;">
弹性系数按KPI类别推断成本类对油价最敏感0.15利润类0.12营收类0.08MVP规则模型后续可用宏观历史数据回归校准
</div>
</el-card>
<!-- 预测说明 -->
<el-card shadow="never" style="margin-top: 12px;">
<div class="note-line">
@@ -114,6 +153,18 @@ const selected = ref<any>(null)
const chartRef = ref<HTMLElement | null>(null)
let chart: any = null
// ── 宏观敏感性联动 ──
const sensPct = ref(10)
const sensList = ref<any[]>([])
const sensFactors = ref<any[]>([])
function catLabel(c: string) {
return { profit: '利润', revenue: '营收', cost: '成本', cash: '现金流' }[c] || c
}
function factorOf(row: any, key: string) {
return (row.factors || []).find((f: any) => f.factor_key === key)
}
function lastHistory(row: any) {
const h = row.history || []
return h.length ? h[h.length - 1].value : null
@@ -164,6 +215,20 @@ async function loadData() {
}
}
async function loadSensitivity() {
try {
const r: any = await predictApi.kpiForecastSensitivity({ pct: sensPct.value, periods: periods.value, model: model.value })
sensList.value = r.data || []
sensFactors.value = r.factors || []
} catch (e: any) {
console.error('敏感性加载失败', e)
}
}
async function loadAll() {
await Promise.all([loadData(), loadSensitivity()])
}
function onSelect(row: any) {
selected.value = row
renderChart()
@@ -207,7 +272,7 @@ function resizeChart() {
}
onMounted(() => {
loadData()
loadAll()
window.addEventListener('resize', resizeChart)
})
onBeforeUnmount(() => {
@@ -267,6 +332,11 @@ onBeforeUnmount(() => {
font-size: 12px;
color: #C0C4CC;
}
.sens-val {
font-size: 12px;
color: #606266;
margin-left: 4px;
}
.trend-chart {
height: 520px;
width: 100%;