Bladeren bron

feat(stock/index): 政治经济双指标 + 估值/动量图表加入指数走势

- 政治经济变化:双系列展示地缘政治风险 + 经济政策不确定性(GDP加权)
- PE-Band / PB-Band / ERP / 换手率成交额 / RSI / VOSC 加入指数走势副坐标轴
- 估值盈利 sheet 收盘价数据接入(881003.WI 万得全A除金融石化)
ligb 5 dagen geleden
bovenliggende
commit
a7a3487c3c

File diff suppressed because it is too large
+ 15706 - 0
web-app/src/views/stock/index/aMarketData.js


+ 249 - 70
web-app/src/views/stock/index/charts.js

@@ -251,47 +251,85 @@ export function categoryContribOption(categoryWeighted) {
 
 // ===== 4.2.1 新增板块 =====
 
-// ===== 政治经济不确定性指数(单折线) =====
-export function epuLineOption(series) {
-  const dates = series.map((s) => s[0])
-  const values = series.map((s) => s[1])
+// ===== 政治经济不确定性标准指数(双折线) =====
+// 1) 全球:地缘政治风险指数            → geoSeries
+// 2) 全球:经济政策不确定性指数:按GDP现价加权平均 → epuSeries
+export function epuLineOption(geoSeries, epuSeries) {
+  const dates = geoSeries.map((s) => s[0])
+  const geoValues = geoSeries.map((s) => s[1])
+  const epuValues = epuSeries.map((s) => s[1])
+  const lastDate = dates[dates.length - 1]
+  const lastGeo = geoValues[geoValues.length - 1]
+  const lastEpu = epuValues[epuValues.length - 1]
   return {
     tooltip: {
       ...tipOpt,
       trigger: 'axis',
-      axisPointer: { type: 'line', lineStyle: { color: C.warn, type: 'dashed' } },
-      valueFormatter: (v) => v.toFixed(1),
+      axisPointer: { type: 'line', lineStyle: { color: C.mut, type: 'dashed' } },
+      valueFormatter: (v) => (v == null ? '—' : v.toFixed(1)),
+    },
+    legend: {
+      data: [
+        { name: '全球:地缘政治风险指数', itemStyle: { color: C.warn } },
+        { name: '全球:经济政策不确定性指数(GDP加权)', itemStyle: { color: C.brand } },
+      ],
+      top: 4,
+      textStyle: { color: C.mut, fontSize: 11 },
+      itemWidth: 14,
     },
-    grid: { top: 28, left: 50, right: 24, bottom: 32, containLabel: true },
+    grid: { top: 36, left: 50, right: 56, bottom: 32, containLabel: true },
     xAxis: {
       type: 'category',
       data: dates,
       ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }),
     },
-    yAxis: { type: 'value', ...axisStyle() },
+    yAxis: [
+      {
+        type: 'value',
+        name: '地缘政治',
+        nameTextStyle: { color: C.warn, fontSize: 10.5, padding: [0, 0, 0, -28] },
+        position: 'left',
+        ...axisStyle(),
+      },
+      {
+        type: 'value',
+        name: '经济政策不确定性',
+        nameTextStyle: { color: C.brand, fontSize: 10.5, padding: [0, -28, 0, 0] },
+        position: 'right',
+        ...axisStyle({ splitLine: { show: false } }),
+      },
+    ],
     series: [
       {
-        name: 'EPU 指数',
+        name: '全球:地缘政治风险指数',
         type: 'line',
-        data: values,
+        yAxisIndex: 0,
+        data: geoValues,
         symbol: 'none',
         smooth: true,
-        lineStyle: { color: C.warn, width: 2.4 },
-        areaStyle: {
-          color: {
-            type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
-            colorStops: [
-              { offset: 0, color: 'rgba(232,145,45,0.30)' },
-              { offset: 1, color: 'rgba(232,145,45,0.02)' },
-            ],
-          },
-        },
+        lineStyle: { color: C.warn, width: 2.2 },
         markPoint: {
           symbol: 'circle',
           symbolSize: 8,
           itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 },
           label: { formatter: (p) => p.value.toFixed(1), color: C.warn, fontSize: 10.5, position: 'top', fontWeight: 600 },
-          data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }],
+          data: [{ name: '当前', value: lastGeo, xAxis: lastDate, yAxis: lastGeo }],
+        },
+      },
+      {
+        name: '全球:经济政策不确定性指数(GDP加权)',
+        type: 'line',
+        yAxisIndex: 1,
+        data: epuValues,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.brand, width: 2.2 },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 8,
+          itemStyle: { color: C.brand, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(1), color: C.brand, fontSize: 10.5, position: 'top', fontWeight: 600 },
+          data: [{ name: '当前', value: lastEpu, xAxis: lastDate, yAxis: lastEpu }],
         },
       },
     ],
@@ -347,7 +385,7 @@ export function revenueProfitLineOption(series) {
 }
 
 // ===== PE / PB 估值带 =====
-function priceBandOption(series, opts) {
+function priceBandOption(series, opts, closeSeries) {
   const dates = series.map((s) => s[0])
   const mid = series.map((s) => s[1])
   const up = series.map((s) => s[2])
@@ -355,6 +393,17 @@ function priceBandOption(series, opts) {
   const cur = series.map((s) => s[4])
   const upperBand = up.map((v, i) => v - mid[i])
   const lowerBand = dn.map((v, i) => dn[i] - mid[i])
+  const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0
+  const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null
+  const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : []
+  const lastClose = hasClose ? (closeSeries[closeSeries.length - 1][1]) : null
+  const legendData = [
+    { name: '当前', itemStyle: { color: opts.curColor } },
+    { name: '上轨', itemStyle: { color: C.faint } },
+    { name: '中枢', itemStyle: { color: C.mut } },
+    { name: '下轨', itemStyle: { color: C.faint } },
+  ]
+  if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } })
   return {
     tooltip: {
       ...tipOpt,
@@ -363,19 +412,19 @@ function priceBandOption(series, opts) {
       valueFormatter: (v) => v.toFixed(opts.toFixed || 2),
     },
     legend: {
-      data: [
-        { name: '当前', itemStyle: { color: opts.curColor } },
-        { name: '上轨', itemStyle: { color: C.faint } },
-        { name: '中枢', itemStyle: { color: C.mut } },
-        { name: '下轨', itemStyle: { color: C.faint } },
-      ],
+      data: legendData,
       top: 4,
       textStyle: { color: C.mut, fontSize: 11 },
       itemWidth: 14,
     },
-    grid: { top: 36, left: 50, right: 24, bottom: 32, containLabel: true },
+    grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true },
     xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
-    yAxis: { type: 'value', ...axisStyle() },
+    yAxis: hasClose
+      ? [
+          { type: 'value', name: opts.toFixed === 3 ? 'PB' : 'PE', nameTextStyle: { color: opts.curColor, fontSize: 10.5, padding: [0, 0, 0, -28] }, position: 'left', ...axisStyle() },
+          { type: 'value', name: '指数走势', nameTextStyle: { color: C.warn, fontSize: 10.5, padding: [0, -28, 0, 0] }, position: 'right', ...axisStyle({ splitLine: { show: false } }) },
+        ]
+      : { type: 'value', ...axisStyle() },
     series: [
       {
         name: '下轨',
@@ -429,20 +478,44 @@ function priceBandOption(series, opts) {
           data: [{ name: '当前', value: cur[cur.length - 1], xAxis: dates[dates.length - 1], yAxis: cur[cur.length - 1] }],
         },
       },
+      ...(hasClose ? [{
+        name: '指数走势',
+        type: 'line',
+        yAxisIndex: 1,
+        data: closeAligned,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.warn, width: 1.6 },
+        z: 4,
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 7,
+          itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 },
+          data: [{ name: '当前指数走势', value: lastClose, xAxis: closeSeries[closeSeries.length - 1][0], yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }
-export function peBandOption(series) {
-  return priceBandOption(series, { curColor: C.brand, toFixed: 2 })
+export function peBandOption(series, closeSeries) {
+  return priceBandOption(series, { curColor: C.brand, toFixed: 2 }, closeSeries)
 }
-export function pbBandOption(series) {
-  return priceBandOption(series, { curColor: C.brandD, toFixed: 3 })
+export function pbBandOption(series, closeSeries) {
+  return priceBandOption(series, { curColor: C.brandD, toFixed: 3 }, closeSeries)
 }
 
 // ===== 风险溢价 ERP =====
-export function erpLineOption(series) {
+export function erpLineOption(series, closeSeries) {
   const dates = series.map((s) => s[0])
   const values = series.map((s) => s[1])
+  const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0
+  const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null
+  const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : []
+  const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null
+  const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null
+  const legendData = [{ name: 'ERP', itemStyle: { color: C.brandD } }]
+  if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } })
   return {
     tooltip: {
       ...tipOpt,
@@ -450,12 +523,23 @@ export function erpLineOption(series) {
       axisPointer: { type: 'line' },
       valueFormatter: (v) => `${v.toFixed(2)}%`,
     },
-    grid: { top: 28, left: 50, right: 24, bottom: 32, containLabel: true },
-    xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
-    yAxis: {
-      type: 'value',
-      ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }),
+    legend: {
+      data: legendData,
+      top: 4,
+      textStyle: { color: C.mut, fontSize: 11 },
+      itemWidth: 14,
     },
+    grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true },
+    xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
+    yAxis: hasClose
+      ? [
+          { type: 'value', name: 'ERP', nameTextStyle: { color: C.brandD, fontSize: 10.5, padding: [0, 0, 0, -28] }, position: 'left', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) },
+          { type: 'value', name: '指数走势', nameTextStyle: { color: C.warn, fontSize: 10.5, padding: [0, -28, 0, 0] }, position: 'right', ...axisStyle({ splitLine: { show: false } }) },
+        ]
+      : {
+          type: 'value',
+          ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }),
+        },
     series: [
       {
         name: 'ERP',
@@ -473,12 +557,12 @@ export function erpLineOption(series) {
             ],
           },
         },
-        markLine: {
-          symbol: ['none', 'none'],
-          data: [
-            { yAxis: 2.5, lineStyle: { color: C.mut, type: 'dashed', width: 1 }, label: { show: true, position: 'end', formatter: '历史中枢', color: C.mut, fontSize: 10 } },
-          ],
-        },
+        // markLine: {
+        //   symbol: ['none', 'none'],
+        //   data: [
+        //     { yAxis: 2.5, lineStyle: { color: C.mut, type: 'dashed', width: 1 }, label: { show: true, position: 'end', formatter: '历史中枢', color: C.mut, fontSize: 10 } },
+        //   ],
+        // },
         markPoint: {
           symbol: 'circle',
           symbolSize: 8,
@@ -487,15 +571,41 @@ export function erpLineOption(series) {
           data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }],
         },
       },
+      ...(hasClose ? [{
+        name: '指数走势',
+        type: 'line',
+        yAxisIndex: 1,
+        data: closeAligned,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.warn, width: 1.6 },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 7,
+          itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 },
+          data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }
 
 // ===== 换手率 + 成交额 双轴 =====
-export function liquidityBarLineOption(series) {
+export function liquidityBarLineOption(series, closeSeries) {
   const dates = series.map((s) => s[0])
   const turnover = series.map((s) => s[1])
   const volume = series.map((s) => s[2])
+  const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0
+  const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null
+  const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : []
+  const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null
+  const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null
+  const legendData = [
+    { name: '换手率', itemStyle: { color: C.brand } },
+    { name: '成交额(万亿)', itemStyle: { color: C.up } },
+  ]
+  if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } })
   return {
     tooltip: {
       ...tipOpt,
@@ -503,10 +613,7 @@ export function liquidityBarLineOption(series) {
       axisPointer: { type: 'shadow' },
     },
     legend: {
-      data: [
-        { name: '换手率', itemStyle: { color: C.brand } },
-        { name: '成交额(万亿)', itemStyle: { color: C.up } },
-      ],
+      data: legendData,
       top: 4,
       textStyle: { color: C.mut, fontSize: 11 },
       itemWidth: 14,
@@ -521,7 +628,7 @@ export function liquidityBarLineOption(series) {
       },
       {
         type: 'value',
-        name: '成交额(万亿)',
+        name: hasClose ? '成交额(万亿) / 指数走势' : '成交额(万亿)',
         ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11 }, nameTextStyle: { color: C.mut, fontSize: 10 } }),
       },
     ],
@@ -542,14 +649,37 @@ export function liquidityBarLineOption(series) {
         smooth: true,
         lineStyle: { color: C.up, width: 2.2 },
       },
+      ...(hasClose ? [{
+        name: '指数走势',
+        type: 'line',
+        yAxisIndex: 1,
+        data: closeAligned,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.warn, width: 1.6 },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 7,
+          itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 },
+          data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }
 
 // ===== RSI 单线 =====
-export function rsiLineOption(series) {
+export function rsiLineOption(series, closeSeries) {
   const dates = series.map((s) => s[0])
   const values = series.map((s) => s[1])
+  const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0
+  const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null
+  const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : []
+  const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null
+  const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null
+  const legendData = [{ name: 'RSI', itemStyle: { color: C.brand } }]
+  if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } })
   return {
     tooltip: {
       ...tipOpt,
@@ -557,15 +687,20 @@ export function rsiLineOption(series) {
       axisPointer: { type: 'line' },
       valueFormatter: (v) => v.toFixed(2),
     },
-    grid: { top: 28, left: 50, right: 24, bottom: 32, containLabel: true },
-    xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
-    yAxis: {
-      type: 'value',
-      min: 0,
-      max: 100,
-      interval: 25,
-      ...axisStyle(),
+    legend: {
+      data: legendData,
+      top: 4,
+      textStyle: { color: C.mut, fontSize: 11 },
+      itemWidth: 14,
     },
+    grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true },
+    xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
+    yAxis: hasClose
+      ? [
+          { type: 'value', min: 0, max: 100, interval: 25, ...axisStyle() },
+          { type: 'value', name: '指数走势', position: 'right', ...axisStyle({ splitLine: { show: false }, nameTextStyle: { color: C.warn, fontSize: 10 } }) },
+        ]
+      : { type: 'value', min: 0, max: 100, interval: 25, ...axisStyle() },
     series: [
       {
         name: 'RSI',
@@ -599,14 +734,37 @@ export function rsiLineOption(series) {
           data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }],
         },
       },
+      ...(hasClose ? [{
+        name: '指数走势',
+        type: 'line',
+        yAxisIndex: 1,
+        data: closeAligned,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.warn, width: 1.6 },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 7,
+          itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 },
+          data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }
 
 // ===== VOSC 单线 =====
-export function voscLineOption(series) {
+export function voscLineOption(series, closeSeries) {
   const dates = series.map((s) => s[0])
   const values = series.map((s) => s[1])
+  const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0
+  const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null
+  const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : []
+  const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null
+  const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null
+  const legendData = [{ name: 'VOSC', itemStyle: { color: C.up } }]
+  if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } })
   return {
     tooltip: {
       ...tipOpt,
@@ -614,9 +772,20 @@ export function voscLineOption(series) {
       axisPointer: { type: 'line' },
       valueFormatter: (v) => v.toFixed(3),
     },
-    grid: { top: 28, left: 50, right: 24, bottom: 32, containLabel: true },
+    legend: {
+      data: legendData,
+      top: 4,
+      textStyle: { color: C.mut, fontSize: 11 },
+      itemWidth: 14,
+    },
+    grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true },
     xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
-    yAxis: { type: 'value', ...axisStyle() },
+    yAxis: hasClose
+      ? [
+          { type: 'value', ...axisStyle() },
+          { type: 'value', name: '指数走势', position: 'right', ...axisStyle({ splitLine: { show: false }, nameTextStyle: { color: C.warn, fontSize: 10 } }) },
+        ]
+      : { type: 'value', ...axisStyle() },
     series: [
       {
         name: 'VOSC',
@@ -634,12 +803,6 @@ export function voscLineOption(series) {
             ],
           },
         },
-        markLine: {
-          symbol: ['none', 'none'],
-          data: [
-            { yAxis: 0, lineStyle: { color: C.mut, type: 'dashed' }, label: { show: true, position: 'end', formatter: '零轴', color: C.mut, fontSize: 10 } },
-          ],
-        },
         markPoint: {
           symbol: 'circle',
           symbolSize: 8,
@@ -648,6 +811,22 @@ export function voscLineOption(series) {
           data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }],
         },
       },
+      ...(hasClose ? [{
+        name: '指数走势',
+        type: 'line',
+        yAxisIndex: 1,
+        data: closeAligned,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.warn, width: 1.6 },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 7,
+          itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 },
+          data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }

+ 18 - 2
web-app/src/views/stock/index/data.js

@@ -236,8 +236,21 @@ export const factorCompare = allFactors.map((f) => {
 import { aMarketData as A } from './aMarketData.js'
 
 // 1) 地缘政治 / 经济政策不确定性 双系列
-export const epuSeries = A.epuUncertainty
 export const geoRiskSeries = A.epu
+export const epuSeries = A.epuUncertainty
+
+// 对齐到共同日期集合(取交集,避免双 y 轴 X 轴长度不一致)
+function alignDual(seriesA, seriesB) {
+  const mapB = new Map(seriesB.map(([d, v]) => [d, v]))
+  const dates = seriesA.map(([d]) => d).filter((d) => mapB.has(d))
+  const a = dates.map((d) => [d, seriesA.find(([x]) => x === d)[1]])
+  const b = dates.map((d) => [d, mapB.get(d)])
+  return { dates, a, b }
+}
+const aligned = alignDual(A.epu, A.epuUncertainty)
+export const geoRiskAligned = aligned.a
+export const epuAligned = aligned.b
+export const geoEpuDates = aligned.dates
 
 // 2) 营收 / 利润同比
 export const revenueProfitSeries = (() => {
@@ -270,6 +283,9 @@ export const peBandSeries = buildBandSeries(A.peTtm, A.band.PE)
 export const pbBandSeries = buildBandSeries(A.pbLf, A.band.PB)
 export const erpBandSeries = buildBandSeries(A.riskPremium, A.band.ERP)
 
+// 收盘价序列(来自「估值 盈利」sheet · 881003.WI 万得全A除金融石化)
+export const closeSeries = A.close || []
+
 // 纯 ERP 走势(折线图用)
 export const erpSeries = A.riskPremium
 
@@ -295,7 +311,6 @@ export const voscSeries = A.vosc
 // 当前值与数据截至
 export const aChartLatest = {
   asOf: A.asOf.epu || '2026-09-29',
-  epu: A.latest.epu,
   geoRisk: A.latest.epu,
   epuUncertainty: A.latest.epuUncertainty,
   revenueYoY: A.latest.revenueYOY,
@@ -303,6 +318,7 @@ export const aChartLatest = {
   peNow: A.latest.peTtm,
   pbNow: A.latest.pbLf,
   erp: A.latest.riskPremium,
+  close: A.latest.close,
   turnover: A.latest.turn,
   volume: A.latest.amt,
   rsi: A.latest.rsi,

+ 21 - 16
web-app/src/views/stock/index/index.vue

@@ -324,10 +324,13 @@
           <div class="card-head">
             <div class="title">
               <span class="ic">图</span>
-              <span class="title-text">1)政治经济变化 · 政治经济不确定性标准指数</span>
+              <span class="title-text">1)政治经济变化 · 政治经济不确定性标准指数(地缘政治 + 经济政策不确定性 GDP加权)</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 {{ aChartLatest.epu.toFixed(1) }} · 月频</span>
+              <span class="meta">
+                地缘政治 {{ aChartLatest.geoRisk.toFixed(1) }}
+                · 经济政策不确定性 {{ aChartLatest.epuUncertainty.toFixed(1) }} · 月频
+              </span>
             </div>
           </div>
           <div class="card-body">
@@ -363,7 +366,7 @@
               <span class="title-text">市盈率 · PE-Band</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 PE {{ aChartLatest.peNow.toFixed(2) }}</span>
+              <span class="meta">当前 PE {{ aChartLatest.peNow.toFixed(2) }} · 指数走势 {{ aChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -377,7 +380,7 @@
               <span class="title-text">市净率 · PB-Band</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 PB {{ aChartLatest.pbNow.toFixed(2) }}</span>
+              <span class="meta">当前 PB {{ aChartLatest.pbNow.toFixed(2) }} · 指数走势 {{ aChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -391,7 +394,7 @@
               <span class="title-text">风险溢价 · ERP(股债性价比)</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 {{ aChartLatest.erp.toFixed(2) }}%</span>
+              <span class="meta">当前 {{ aChartLatest.erp.toFixed(2) }}% · 指数走势 {{ aChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -409,7 +412,7 @@
               <span class="title-text">流动性 · 换手率与成交额</span>
             </div>
             <div class="acts">
-              <span class="meta">换手率 {{ aChartLatest.turnover.toFixed(2) }}% · 成交额 {{ aChartLatest.volume.toFixed(2) }}万亿</span>
+              <span class="meta">换手率 {{ aChartLatest.turnover.toFixed(2) }}% · 成交额 {{ aChartLatest.volume.toFixed(2) }}万亿 · 指数走势 {{ aChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -423,7 +426,7 @@
               <span class="title-text">情绪指标 · RSI</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 RSI {{ aChartLatest.rsi.toFixed(2) }}</span>
+              <span class="meta">当前 RSI {{ aChartLatest.rsi.toFixed(2) }} · 指数走势 {{ aChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -437,7 +440,7 @@
               <span class="title-text">趋势指标 · VOSC</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 VOSC {{ aChartLatest.vosc.toFixed(3) }}</span>
+              <span class="meta">当前 VOSC {{ aChartLatest.vosc.toFixed(3) }} · 指数走势 {{ aChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -465,16 +468,18 @@ import {
   factorCompare,
   SCORE_BANDS,
   classifyScore,
-  epuSeries,
   revenueProfitSeries,
   peBandSeries,
   pbBandSeries,
+  closeSeries,
   erpSeries,
 erpBandSeries,
   liquiditySeries,
   rsiSeries,
   voscSeries,
   aChartLatest,
+  geoRiskAligned,
+  epuAligned,
 } from './data'
 import {
   scoreGaugeOption,
@@ -558,14 +563,14 @@ const barOpt = computed(() => categoryBarOption(categoryWeightedCurrent, categor
 const contribOpt = computed(() => categoryContribOption(categoryWeightedCurrent))
 const radarOpt = computed(() => factorRadarOption(allFactors))
 
-const epuOpt = computed(() => epuLineOption(epuSeries))
+const epuOpt = computed(() => epuLineOption(geoRiskAligned, epuAligned))
 const revProfitOpt = computed(() => revenueProfitLineOption(revenueProfitSeries))
-const peBandOpt = computed(() => peBandOption(peBandSeries))
-const pbBandOpt = computed(() => pbBandOption(pbBandSeries))
-const erpOpt = computed(() => erpLineOption(erpSeries))
-const liquidityOpt = computed(() => liquidityBarLineOption(liquiditySeries))
-const rsiOpt = computed(() => rsiLineOption(rsiSeries))
-const voscOpt = computed(() => voscLineOption(voscSeries))
+const peBandOpt = computed(() => peBandOption(peBandSeries, closeSeries))
+const pbBandOpt = computed(() => pbBandOption(pbBandSeries, closeSeries))
+const erpOpt = computed(() => erpLineOption(erpSeries, closeSeries))
+const liquidityOpt = computed(() => liquidityBarLineOption(liquiditySeries, closeSeries))
+const rsiOpt = computed(() => rsiLineOption(rsiSeries, closeSeries))
+const voscOpt = computed(() => voscLineOption(voscSeries, closeSeries))
 
 function reload() {
   loading.value = true

Some files were not shown because too many files changed in this diff