Эх сурвалжийг харах

feat(stock/h-share, stock/us, multiasset/fund): 行情数据更新与图表优化

- stock/h-share: 更新 hMarketData 全量行情数据,charts/index 适配
- stock/us: 新增 usMarketData 数据文件,charts/data/index 调整
- multiasset/fund: charts/index 微调
ligb 4 өдөр өмнө
parent
commit
e341ce92c7

+ 2 - 1
web-app/src/views/multiasset/fund/charts.js

@@ -225,8 +225,9 @@ export function bucketScoreCompareOption(buckets) {
     },
     yAxis: {
       type: 'value',
-      min: 50,
+      min: 0,
       max: 100,
+      interval: 20,
       ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11 } }),
     },
     series: [

+ 2 - 2
web-app/src/views/multiasset/fund/index.vue

@@ -198,7 +198,7 @@
         <span class="meta">按权益仓位划分 · 各组 Top 排名</span>
       </div>
 
-      <div class="card-grid cols-1">
+      <!-- <div class="card-grid cols-1">
         <div class="card">
           <div class="card-head">
             <div class="title">
@@ -213,7 +213,7 @@
             <Chart :option="bucketCompareOpt" height="280px" />
           </div>
         </div>
-      </div>
+      </div> -->
 
       <div v-for="b in buckets" :key="b.key" class="card" style="margin-top:14px">
         <div class="card-head">

+ 221 - 39
web-app/src/views/stock/h-share/charts.js

@@ -282,12 +282,24 @@ 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])
   const dn = series.map((s) => s[3])
   const cur = series.map((s) => s[4])
+  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: 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,
@@ -296,19 +308,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: '下轨', type: 'line', data: dn, symbol: 'none', smooth: true, lineStyle: { color: C.faint, type: 'dashed', width: 1 }, z: 1 },
       { name: '上轨', type: 'line', data: up, symbol: 'none', smooth: true, lineStyle: { color: C.faint, type: 'dashed', width: 1 }, z: 1 },
@@ -333,16 +345,39 @@ 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: lastCloseDate, yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }
-export function peBandOption(series) { return priceBandOption(series, { curColor: C.brand, toFixed: 2 }) }
-export function pbBandOption(series) { return priceBandOption(series, { curColor: C.brandD, toFixed: 3 }) }
+export function peBandOption(series, closeSeries) { return priceBandOption(series, { curColor: C.brand, toFixed: 2 }, closeSeries) }
+export function pbBandOption(series, closeSeries) { return priceBandOption(series, { curColor: C.brandD, toFixed: 3 }, closeSeries) }
 
 // ===== 股息率 =====
-export function dividendLineOption(series) {
+export function dividendLineOption(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: '股息率', itemStyle: { color: C.warn } }]
+  if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.brand } })
   return {
     tooltip: {
       ...tipOpt,
@@ -350,9 +385,20 @@ export function dividendLineOption(series) {
       axisPointer: { type: 'line' },
       valueFormatter: (v) => `${v.toFixed(2)}%`,
     },
-    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({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) },
+    yAxis: hasClose
+      ? [
+          { type: 'value', name: '股息率', nameTextStyle: { color: C.warn, 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.brand, 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: '股息率',
@@ -377,14 +423,36 @@ export function dividendLineOption(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.brand, width: 1.6 },
+        markPoint: {
+          symbol: 'circle', symbolSize: 7,
+          itemStyle: { color: C.brand, borderColor: '#fff', borderWidth: 2 },
+          label: { formatter: (p) => p.value.toFixed(0), color: C.brand, fontSize: 10, position: 'bottom', fontWeight: 600 },
+          data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }],
+        },
+      }] : []),
     ],
   }
 }
 
 // ===== 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,
@@ -392,9 +460,20 @@ export function erpLineOption(series) {
       axisPointer: { type: 'line' },
       valueFormatter: (v) => `${v.toFixed(2)}%`,
     },
-    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({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) },
+    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',
@@ -412,12 +491,12 @@ export function erpLineOption(series) {
             ],
           },
         },
-        markLine: {
-          symbol: ['none', 'none'],
-          data: [
-            { yAxis: 4.0, 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: 4.0, lineStyle: { color: C.mut, type: 'dashed', width: 1 }, label: { show: true, position: 'end', formatter: '历史中枢', color: C.mut, fontSize: 10 } },
+        //   ],
+        // },
         markPoint: {
           symbol: 'circle', symbolSize: 8,
           itemStyle: { color: C.brandD, borderColor: '#fff', borderWidth: 2 },
@@ -425,22 +504,44 @@ 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, trigger: 'axis', 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,
@@ -449,24 +550,57 @@ export function liquidityBarLineOption(series) {
     xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
     yAxis: [
       { type: 'value', name: '换手率(%)', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' }, nameTextStyle: { color: C.mut, fontSize: 10 } }) },
-      { type: 'value', name: '成交额(千亿)', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11 }, nameTextStyle: { color: C.mut, fontSize: 10 } }) },
+      { type: 'value', name: hasClose ? '成交额(千亿) / 指数走势' : '成交额(千亿)', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11 }, nameTextStyle: { color: C.mut, fontSize: 10 } }) },
     ],
     series: [
       { name: '换手率', type: 'bar', data: turnover, barWidth: 12, itemStyle: { color: 'rgba(21,82,208,0.65)', borderRadius: [3, 3, 0, 0] } },
       { name: '成交额(千亿港元)', type: 'line', yAxisIndex: 1, data: volume, symbol: 'none', 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, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => v.toFixed(2) },
-    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', min: 0, max: 100, interval: 25, ...axisStyle() },
+    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', type: 'line', data: values, symbol: 'none', smooth: true,
@@ -495,19 +629,52 @@ 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, trigger: 'axis', 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', type: 'line', data: values, symbol: 'none', smooth: true,
@@ -521,10 +688,10 @@ 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 } }],
-        },
+        // 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,
           itemStyle: { color: C.up, borderColor: '#fff', borderWidth: 2 },
@@ -532,6 +699,21 @@ 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 }],
+        },
+      }] : []),
     ],
   }
 }

+ 8 - 1
web-app/src/views/stock/h-share/data.js

@@ -267,9 +267,12 @@ function buildBandSeries(valueSeries, band) {
   return valueSeries.map(([d, v]) => [d, mean, up1, down1, v])
 }
 export const hPeBandSeries = buildBandSeries(H.peTtm, H.band.PE)
-export const hPbBandSeries = buildBandSeries(H.pbLf, H.band.PE)
+export const hPbBandSeries = buildBandSeries(H.pbLf, H.band.PB)
 export const hErpBandSeries = buildBandSeries(H.riskPremium, H.band.PE)
 
+// 纯 ERP 走势(折线图用,避免被 band 包裹成常数中枢线)
+export const hErpSeries = H.riskPremium
+
 // 3) 股息率
 export const hDividendSeries = H.dividend
 
@@ -292,6 +295,9 @@ export const hLiquiditySeries = (() => {
 export const hRsiSeries = H.rsi
 export const hVoscSeries = H.vosc
 
+// 收盘价序列(来自「估值 盈利」sheet · K列 恒生指数收盘价)
+export const closeSeries = H.close || []
+
 // 当前值与数据截至
 export const hChartLatest = {
   asOf: H.asOf.pe || '2026-09-21',
@@ -305,6 +311,7 @@ export const hChartLatest = {
   volume: H.latest.amt,
   rsi: H.latest.rsi,
   vosc: H.latest.vosc,
+  close: H.latest.close,
   band: H.band,
   asOfMap: H.asOf,
 }

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 15840 - 2444
web-app/src/views/stock/h-share/hMarketData.js


+ 16 - 15
web-app/src/views/stock/h-share/index.vue

@@ -379,7 +379,7 @@
               <span class="title-text">市盈率 · 恒生 PE-Band</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 PE {{ hChartLatest.peNow.toFixed(2) }}</span>
+              <span class="meta">当前 PE {{ hChartLatest.peNow.toFixed(2) }} · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -393,7 +393,7 @@
               <span class="title-text">市净率 · 恒生 PB-Band</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 PB {{ hChartLatest.pbNow.toFixed(2) }}</span>
+              <span class="meta">当前 PB {{ hChartLatest.pbNow.toFixed(2) }} · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -407,7 +407,7 @@
               <span class="title-text">股息率 · 恒生指数</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 {{ hChartLatest.dividend.toFixed(2) }}%</span>
+              <span class="meta">当前 {{ hChartLatest.dividend.toFixed(2) }}% · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -421,7 +421,7 @@
               <span class="title-text">风险溢价 · 恒生 ERP(股债性价比)</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 {{ hChartLatest.erp.toFixed(2) }}%</span>
+              <span class="meta">当前 {{ hChartLatest.erp.toFixed(2) }}% · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -439,7 +439,7 @@
               <span class="title-text">流动性 · 恒生换手率与成交额</span>
             </div>
             <div class="acts">
-              <span class="meta">换手率 {{ hChartLatest.turnover.toFixed(2) }}% · 成交额 {{ hChartLatest.volume.toFixed(3) }}千亿港元</span>
+              <span class="meta">换手率 {{ hChartLatest.turnover.toFixed(2) }}% · 成交额 {{ hChartLatest.volume.toFixed(3) }}千亿港元 · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -453,7 +453,7 @@
               <span class="title-text">情绪指标 · 恒生 RSI</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 RSI {{ hChartLatest.rsi.toFixed(2) }}</span>
+              <span class="meta">当前 RSI {{ hChartLatest.rsi.toFixed(2) }} · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -467,7 +467,7 @@
               <span class="title-text">趋势指标 · 恒生 VOSC</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 VOSC {{ hChartLatest.vosc.toFixed(3) }}</span>
+              <span class="meta">当前 VOSC {{ hChartLatest.vosc.toFixed(3) }} · 指数走势 {{ hChartLatest.close.toFixed(0) }}</span>
             </div>
           </div>
           <div class="card-body">
@@ -499,10 +499,11 @@ import {
   hPeBandSeries,
   hPbBandSeries,
   hDividendSeries,
-  hErpBandSeries as hErpSeries,
+  hErpSeries,
   hLiquiditySeries,
   hRsiSeries,
   hVoscSeries,
+  closeSeries,
   hChartLatest,
 } from './data'
 import {
@@ -588,13 +589,13 @@ const contribOpt = computed(() => categoryContribOption(categoryWeightedCurrent)
 const radarOpt = computed(() => factorRadarOption(allFactors))
 
 const revProfitOpt = computed(() => revenueProfitLineOption(hRevenueProfitSeries))
-const peBandOpt = computed(() => peBandOption(hPeBandSeries))
-const pbBandOpt = computed(() => pbBandOption(hPbBandSeries))
-const dividendOpt = computed(() => dividendLineOption(hDividendSeries))
-const erpOpt = computed(() => erpLineOption(hErpSeries))
-const liquidityOpt = computed(() => liquidityBarLineOption(hLiquiditySeries))
-const rsiOpt = computed(() => rsiLineOption(hRsiSeries))
-const voscOpt = computed(() => voscLineOption(hVoscSeries))
+const peBandOpt = computed(() => peBandOption(hPeBandSeries, closeSeries))
+const pbBandOpt = computed(() => pbBandOption(hPbBandSeries, closeSeries))
+const dividendOpt = computed(() => dividendLineOption(hDividendSeries, closeSeries))
+const erpOpt = computed(() => erpLineOption(hErpSeries, closeSeries))
+const liquidityOpt = computed(() => liquidityBarLineOption(hLiquiditySeries, closeSeries))
+const rsiOpt = computed(() => rsiLineOption(hRsiSeries, closeSeries))
+const voscOpt = computed(() => voscLineOption(hVoscSeries, closeSeries))
 
 function reload() {
   loading.value = true

+ 50 - 14
web-app/src/views/stock/us/charts.js

@@ -248,34 +248,54 @@ export function categoryContribOption(categoryWeighted) {
   }
 }
 
-// ===== 4.3.1 新增板块 · 美国 VIX 指数 =====
+// ===== 4.3.1 新增板块 · 美国 VIX 指数 · 月度走势 =====
+// series: [{ month: 'YYYY-MM', vix, nasdaq }, ...]
 export function vixLineOption(series) {
-  const dates = series.map((s) => s[0])
-  const values = series.map((s) => s[1])
+  const months = series.map((s) => s.month)
+  const vixVals = series.map((s) => s.vix)
+  const ndxVals = series.map((s) => s.nasdaq)
+  const lastMonth = months[months.length - 1]
+  const lastVix = vixVals[vixVals.length - 1]
+  const lastNdx = ndxVals[ndxVals.length - 1]
   return {
     tooltip: {
       ...tipOpt,
       trigger: 'axis',
       axisPointer: { type: 'line' },
-      valueFormatter: (v) => v.toFixed(2),
+      valueFormatter: (v) => (v == null ? '-' : v.toFixed(2)),
+    },
+    legend: {
+      data: ['标普500 VIX', '纳斯达克100 波动率'],
+      top: 4,
+      textStyle: { color: C.mut, fontSize: 11 },
+      itemWidth: 14,
     },
     grid: { top: 36, 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() },
+    xAxis: {
+      type: 'category',
+      data: months,
+      ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }),
+    },
+    yAxis: {
+      type: 'value',
+      min: 0,
+      ...axisStyle(),
+    },
     series: [
       {
-        name: 'VIX',
+        name: '标普500 VIX',
         type: 'line',
-        data: values,
+        data: vixVals,
         symbol: 'none',
         smooth: true,
-        lineStyle: { color: C.up, width: 2.4 },
+        lineStyle: { color: C.brand, width: 2.4 },
+        itemStyle: { color: C.brand },
         areaStyle: {
           color: {
             type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
             colorStops: [
-              { offset: 0, color: 'rgba(225,59,59,0.30)' },
-              { offset: 1, color: 'rgba(225,59,59,0.02)' },
+              { offset: 0, color: 'rgba(21,82,208,0.22)' },
+              { offset: 1, color: 'rgba(21,82,208,0.02)' },
             ],
           },
         },
@@ -289,10 +309,26 @@ export function vixLineOption(series) {
         },
         markPoint: {
           symbol: 'circle',
-          symbolSize: 8,
+          symbolSize: 7,
+          itemStyle: { color: C.brand, borderColor: '#fff', borderWidth: 2 },
+          label: { show: true, formatter: (p) => p.value.toFixed(2), color: C.brand, fontSize: 10.5, position: 'top', fontWeight: 600 },
+          data: [{ name: 'VIX 当前', value: lastVix, xAxis: lastMonth, yAxis: lastVix }],
+        },
+      },
+      {
+        name: '纳斯达克100 波动率',
+        type: 'line',
+        data: ndxVals,
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { color: C.up, width: 2, type: 'dashed' },
+        itemStyle: { color: C.up },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 7,
           itemStyle: { color: C.up, borderColor: '#fff', borderWidth: 2 },
-          label: { formatter: (p) => p.value.toFixed(2), color: C.up, fontSize: 10.5, position: 'top', fontWeight: 600 },
-          data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }],
+          label: { show: true, formatter: (p) => p.value.toFixed(2), color: C.up, fontSize: 10.5, position: 'bottom', fontWeight: 600 },
+          data: [{ name: 'NDX 当前', value: lastNdx, xAxis: lastMonth, yAxis: lastNdx }],
         },
       },
     ],

+ 7 - 5
web-app/src/views/stock/us/data.js

@@ -225,11 +225,13 @@ export const factorCompare = allFactors.map((f) => {
   }
 })
 
-// ===== 4.3.1 新增板块 · 美国 VIX 指数 =====
-import { goldMarketData as G } from '@/views/gold/index/goldMarketData.js'
+// ===== 4.3.1 新增板块 · 美国 VIX 指数 · 月度走势 =====
+import { asOf as VIX_ASOF, latest as VIX_LATEST, vixMonthly } from './usMarketData.js'
+
+export { vixMonthly }
 
-export const vixSeries = G.vix
 export const vixLatest = {
-  asOf: G.asOf.vix || '2026-09-28',
-  value: G.latest.vix,
+  asOf: VIX_ASOF,
+  vix: VIX_LATEST.vix,
+  nasdaq: VIX_LATEST.nasdaq,
 }

+ 10 - 6
web-app/src/views/stock/us/index.vue

@@ -344,12 +344,12 @@
       </div>
     </section>
 
-    <!-- 4.3.1 新增板块 · 美国 VIX 指数 -->
+    <!-- 4.3.1 新增板块 · 美国 VIX 指数 · 月度走势 -->
     <section class="section">
       <div class="section-hd">
         <span class="dot"></span>
-        <h2>波动率监控 · 美国 VIX 指数</h2>
-        <span class="meta">数据截至 {{ vixLatest.asOf }}</span>
+        <h2>波动率监控 · 美国 VIX 指数 · 月度走势</h2>
+        <span class="meta">数据截至 {{ vixLatest.asOf }} · 数据源 4.3.1-美股打分表202609 · Sheet1</span>
       </div>
       <div class="cat-grid">
         <div class="card" style="grid-column: 1 / -1">
@@ -359,7 +359,11 @@
               <span class="title-text">美国 VIX 指数 · 月度走势</span>
             </div>
             <div class="acts">
-              <span class="meta">当前 {{ vixLatest.value.toFixed(2) }} · 中枢 15 · 警戒 25 · 恐慌 35</span>
+              <span class="meta">
+                标普500 VIX {{ vixLatest.vix.toFixed(2) }} ·
+                纳斯达克100 {{ vixLatest.nasdaq.toFixed(2) }} ·
+                中枢 15 · 警戒 25 · 恐慌 35
+              </span>
             </div>
           </div>
           <div class="card-body">
@@ -387,7 +391,7 @@ import {
   factorCompare,
   SCORE_BANDS,
   classifyScore,
-  vixSeries,
+  vixMonthly,
   vixLatest,
 } from './data'
 import {
@@ -464,7 +468,7 @@ const barOpt = computed(() => categoryBarOption(categoryWeightedCurrent, categor
 const contribOpt = computed(() => categoryContribOption(categoryWeightedCurrent))
 const radarOpt = computed(() => factorRadarOption(allFactors))
 
-const vixOpt = computed(() => vixLineOption(vixSeries))
+const vixOpt = computed(() => vixLineOption(vixMonthly))
 
 function reload() {
   loading.value = true

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 9 - 0
web-app/src/views/stock/us/usMarketData.js


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно