Selaa lähdekoodia

feat(stock/etf): 新增 4.2.3 ETF 策略模型页面

- 模型一·ETF配置模型:银行/煤炭/科创ETF,ES 风险反比加权,月末调仓
- 模型二·全天候策略:沪深300/中证500/5年国债/10年国债/黄金/豆粕ETF,风险平价
- 双线净值走势(策略 vs 沪深300基准)、权重堆叠柱、最新权重、明细表
- 路由 /stock/etf 挂载到权益分析树

数据源:4.2.3-ETF模型-数据底稿.xlsx
ligb 1 päivä sitten
vanhempi
commit
9973a2c597

+ 6 - 0
web-app/src/router/index.js

@@ -223,6 +223,12 @@ const router = createRouter({
       component: () => import('@/views/stock/us/index.vue'),
       meta: { layout: 'app', title: '权益分析 · 美股分析' },
     },
+    {
+      path: '/stock/etf',
+      name: 'stock-etf',
+      component: () => import('@/views/stock/etf/index.vue'),
+      meta: { layout: 'app', title: 'ETF 策略模型' },
+    },
     {
       path: '/gold/index',
       name: 'gold-index',

+ 602 - 0
web-app/src/views/stock/etf/charts.js

@@ -0,0 +1,602 @@
+import { palette as C } from './data'
+
+const tipOpt = {
+  backgroundColor: 'rgba(12,43,94,.94)',
+  borderWidth: 0,
+  textStyle: { color: '#fff', fontSize: 12 },
+  padding: [8, 12],
+  extraCssText: 'box-shadow:0 6px 18px rgba(12,43,94,.3);border-radius:6px;',
+}
+
+function axisStyle(opts = {}) {
+  return {
+    axisLine: { lineStyle: { color: '#E4E9F2' } },
+    axisTick: { show: false },
+    axisLabel: { color: C.mut, fontSize: 11 },
+    splitLine: { lineStyle: { color: '#F2F5FA', type: 'dashed' } },
+    ...opts,
+  }
+}
+
+function downsample(series, max = 200) {
+  if (series.length <= max) return series
+  const step = Math.ceil(series.length / max)
+  const out = []
+  for (let i = 0; i < series.length; i += step) out.push(series[i])
+  if (out[out.length - 1] !== series[series.length - 1]) out.push(series[series.length - 1])
+  return out
+}
+
+function netValueLineOption(series, opts, benchmark) {
+  const data = downsample(series)
+  const benchData = benchmark ? downsample(benchmark) : []
+  const dates = data.map((s) => s[0])
+  const values = data.map((s) => s[1])
+  const benchValues = benchData.map((s) => s[1])
+
+  const legendData = benchmark ? [opts.name, opts.benchName || '沪深300基准'] : [opts.name]
+
+  // 让 Y 轴上下边界同时覆盖主序列与基准
+  const allValues = benchmark && benchValues.length ? [...values, ...benchValues] : values
+  const yMin = Math.min(...allValues)
+  const yMax = Math.max(...allValues)
+  const pad = (yMax - yMin) * 0.05 || 0.01
+
+  const buildSeries = ({ name, vals, color, fill, fillEnd, isMain }) => ({
+    name,
+    type: 'line',
+    data: vals,
+    symbol: 'none',
+    smooth: true,
+    sampling: 'lttb',
+    lineStyle: { color, width: isMain ? 2.4 : 1.8 },
+    areaStyle: isMain ? {
+      color: {
+        type: 'linear',
+        x: 0, y: 0, x2: 0, y2: 1,
+        colorStops: [
+          { offset: 0, color: fill },
+          { offset: 1, color: fillEnd },
+        ],
+      },
+    } : undefined,
+    markLine: isMain ? {
+      symbol: 'none',
+      silent: true,
+      label: { color: C.mut, fontSize: 10, position: 'insideEndTop' },
+      lineStyle: { color: C.faint, type: 'dashed' },
+      data: [{ yAxis: 1, name: '基准 1.00' }],
+    } : undefined,
+    markPoint: {
+      symbol: 'circle',
+      symbolSize: isMain ? 9 : 7,
+      itemStyle: { color, borderColor: '#fff', borderWidth: 2 },
+      label: {
+        formatter: (p) => p.value.toFixed(3),
+        color,
+        fontSize: 10.5,
+        position: 'top',
+        fontWeight: 600,
+      },
+      data: [{ name: '当前', value: vals[vals.length - 1], xAxis: dates[dates.length - 1], yAxis: vals[vals.length - 1] }],
+    },
+  })
+
+  const seriesList = [buildSeries({
+    name: opts.name,
+    vals: values,
+    color: opts.lineColor || C.brand,
+    fill: opts.areaFill || 'rgba(21,82,208,0.30)',
+    fillEnd: opts.areaFillEnd || 'rgba(21,82,208,0.02)',
+    isMain: true,
+  })]
+
+  if (benchmark && benchValues.length) {
+    seriesList.push(buildSeries({
+      name: opts.benchName || '沪深300基准',
+      vals: benchValues,
+      color: opts.benchColor || '#9097A0',
+      fill: 'transparent',
+      fillEnd: 'transparent',
+      isMain: false,
+    }))
+  }
+
+  return {
+    tooltip: {
+      ...tipOpt,
+      trigger: 'axis',
+      axisPointer: { type: 'line' },
+      valueFormatter: (v) => v.toFixed(4),
+    },
+    legend: benchmark ? {
+      data: legendData,
+      top: 4,
+      right: 12,
+      textStyle: { color: C.mut, fontSize: 11 },
+      icon: 'roundRect',
+      itemWidth: 10,
+      itemHeight: 10,
+    } : undefined,
+    grid: { top: benchmark ? 36 : 30, 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: +((yMin - pad).toFixed(4)),
+      max: +(yMax + pad).toFixed(4),
+      ...axisStyle({
+        axisLabel: {
+          color: C.mut,
+          fontSize: 11,
+          formatter: (v) => v.toFixed(2),
+        },
+      }),
+    },
+    series: seriesList,
+  }
+}
+
+export function etfNetLineOption(series, benchmark) {
+  return netValueLineOption(series, {
+    name: 'ETF 配置模型 · 净值',
+    lineColor: C.gold,
+    areaFill: 'rgba(196,154,42,0.30)',
+    areaFillEnd: 'rgba(196,154,42,0.02)',
+    benchName: '沪深300基准',
+    benchColor: '#9097A0',
+  }, benchmark)
+}
+
+export function allweatherNetLineOption(series) {
+  return netValueLineOption(series, {
+    name: '全天候策略 · 净值',
+    lineColor: C.brand,
+    areaFill: 'rgba(21,82,208,0.30)',
+    areaFillEnd: 'rgba(21,82,208,0.02)',
+  })
+}
+
+// ===== 回撤曲线 =====
+export function drawdownLineOption(series, opts) {
+  const data = downsample(series)
+  const dates = data.map((s) => s[0])
+  // 若 series[2] 有 drawdown 直接使用;否则用净值自计算
+  const hasInline = series[0].length >= 3 && series[0][2] != null
+  if (hasInline) {
+    const values = data.map((s) => +((s[2] || 0) * 100).toFixed(3))
+    return {
+      tooltip: {
+        ...tipOpt,
+        trigger: 'axis',
+        axisPointer: { type: 'line' },
+        valueFormatter: (v) => `${v.toFixed(2)}%`,
+      },
+      grid: { top: 24, 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}%' } }),
+      },
+      series: [
+        {
+          name: opts.name,
+          type: 'line',
+          data: values,
+          symbol: 'none',
+          smooth: true,
+          sampling: 'lttb',
+          lineStyle: { color: opts.lineColor || C.up, width: 1.8 },
+          areaStyle: {
+            color: {
+              type: 'linear',
+              x: 0, y: 0, x2: 0, y2: 1,
+              colorStops: [
+                { offset: 0, color: opts.areaFill || 'rgba(225,59,59,0.25)' },
+                { offset: 1, color: opts.areaFillEnd || 'rgba(225,59,59,0.02)' },
+              ],
+            },
+          },
+          markPoint: {
+            symbol: 'circle',
+            symbolSize: 8,
+            itemStyle: { color: opts.lineColor || C.up, borderColor: '#fff', borderWidth: 2 },
+            label: {
+              formatter: (p) => `${p.value.toFixed(2)}%`,
+              color: opts.lineColor || C.up,
+              fontSize: 10.5,
+              position: 'top',
+              fontWeight: 600,
+            },
+            data: [{
+              name: '最大回撤',
+              value: Math.min(...values),
+              xAxis: dates[values.indexOf(Math.min(...values))],
+              yAxis: Math.min(...values),
+            }],
+          },
+        },
+      ],
+    }
+  }
+
+  // 自计算回撤
+  let peak = -Infinity
+  const drawdown = []
+  for (const s of data) {
+    if (s[1] > peak) peak = s[1]
+    drawdown.push(+(((s[1] - peak) / peak) * 100).toFixed(3))
+  }
+  const dates2 = data.map((s) => s[0])
+  return {
+    tooltip: {
+      ...tipOpt,
+      trigger: 'axis',
+      axisPointer: { type: 'line' },
+      valueFormatter: (v) => `${v.toFixed(2)}%`,
+    },
+    grid: { top: 24, left: 50, right: 24, bottom: 32, containLabel: true },
+    xAxis: { type: 'category', data: dates2, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) },
+    yAxis: {
+      type: 'value',
+      ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }),
+    },
+    series: [
+      {
+        name: opts.name,
+        type: 'line',
+        data: drawdown,
+        symbol: 'none',
+        smooth: true,
+        sampling: 'lttb',
+        lineStyle: { color: opts.lineColor || C.up, width: 1.8 },
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0, y: 0, x2: 0, y2: 1,
+            colorStops: [
+              { offset: 0, color: opts.areaFill || 'rgba(225,59,59,0.25)' },
+              { offset: 1, color: opts.areaFillEnd || 'rgba(225,59,59,0.02)' },
+            ],
+          },
+        },
+        markPoint: {
+          symbol: 'circle',
+          symbolSize: 8,
+          itemStyle: { color: opts.lineColor || C.up, borderColor: '#fff', borderWidth: 2 },
+          label: {
+            formatter: (p) => `${p.value.toFixed(2)}%`,
+            color: opts.lineColor || C.up,
+            fontSize: 10.5,
+            position: 'top',
+            fontWeight: 600,
+          },
+          data: [{
+            name: '最大回撤',
+            value: Math.min(...drawdown),
+            xAxis: dates2[drawdown.indexOf(Math.min(...drawdown))],
+            yAxis: Math.min(...drawdown),
+          }],
+        },
+      },
+    ],
+  }
+}
+
+export function allweatherDrawdownOption(series) {
+  return drawdownLineOption(series, {
+    name: '回撤',
+    lineColor: C.up,
+    areaFill: 'rgba(225,59,59,0.25)',
+    areaFillEnd: 'rgba(225,59,59,0.02)',
+  })
+}
+
+// ===== 调仓权重堆叠柱 =====
+export function etfWeightStackOption(weights) {
+  const dates = weights.map((w) => w.date)
+  const COLORS = {
+    coal: '#8C5A2B',
+    bank: '#C49A2A',
+    tech: '#1552D0',
+  }
+  return {
+    color: [COLORS.coal, COLORS.bank, COLORS.tech],
+    tooltip: {
+      ...tipOpt,
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      formatter: (p) => {
+        const i = p[0].dataIndex
+        const w = weights[i]
+        const lines = [
+          `${w.date} · ${w.type}`,
+          `煤炭 ETF:${(w.coal * 100).toFixed(1)}%`,
+          `银行 ETF:${(w.bank * 100).toFixed(1)}%`,
+          `科创 ETF:${(w.tech * 100).toFixed(1)}%`,
+        ]
+        return lines.join('<br/>')
+      },
+    },
+    legend: {
+      data: ['煤炭 ETF', '银行 ETF', '科创 ETF'],
+      top: 0,
+      right: 8,
+      textStyle: { color: C.mut, fontSize: 11 },
+      icon: 'roundRect',
+      itemWidth: 10,
+      itemHeight: 10,
+    },
+    grid: { top: 32, left: 40, right: 24, bottom: 50, containLabel: true },
+    xAxis: {
+      type: 'category',
+      data: dates,
+      axisLabel: { color: C.mut, fontSize: 10.5, rotate: 35 },
+      axisLine: { lineStyle: { color: '#E4E9F2' } },
+      axisTick: { show: false },
+    },
+    yAxis: {
+      type: 'value',
+      max: 100,
+      min: 0,
+      axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' },
+      axisLine: { show: false },
+      axisTick: { show: false },
+      splitLine: { lineStyle: { color: '#F2F5FA', type: 'dashed' } },
+    },
+    series: [
+      {
+        name: '煤炭 ETF',
+        type: 'bar',
+        stack: 'w',
+        barWidth: '60%',
+        data: weights.map((w) => ({
+          value: +(w.coal * 100).toFixed(2),
+          itemStyle: { color: COLORS.coal },
+        })),
+      },
+      {
+        name: '银行 ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.bank * 100).toFixed(2),
+          itemStyle: { color: COLORS.bank },
+        })),
+      },
+      {
+        name: '科创 ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.tech * 100).toFixed(2),
+          itemStyle: { color: COLORS.tech },
+        })),
+      },
+    ],
+  }
+}
+
+export function allweatherWeightStackOption(weights) {
+  const dates = weights.map((w) => w.date)
+  const COLORS = {
+    hs300: '#1552D0',
+    zz500: '#3D5A80',
+    c5: '#7B91FF',
+    c10: '#0E3A96',
+    gold: '#C49A2A',
+    meal: '#189A65',
+  }
+  return {
+    color: [COLORS.hs300, COLORS.zz500, COLORS.c5, COLORS.c10, COLORS.gold, COLORS.meal],
+    tooltip: {
+      ...tipOpt,
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      formatter: (p) => {
+        const i = p[0].dataIndex
+        const w = weights[i]
+        const lines = [
+          `${w.date} · 信号日 ${w.signalDate}`,
+          `沪深300ETF:${(w.hs300 * 100).toFixed(1)}%`,
+          `中证500ETF:${(w.zz500 * 100).toFixed(1)}%`,
+          `五年国债ETF:${(w.c5 * 100).toFixed(1)}%`,
+          `十年国债ETF:${(w.c10 * 100).toFixed(1)}%`,
+          `黄金ETF:${(w.gold * 100).toFixed(1)}%`,
+          `豆粕ETF:${(w.meal * 100).toFixed(1)}%`,
+        ]
+        return lines.join('<br/>')
+      },
+    },
+    legend: {
+      data: ['沪深300ETF', '中证500ETF', '五年国债ETF', '十年国债ETF', '黄金ETF', '豆粕ETF'],
+      top: 0,
+      right: 8,
+      textStyle: { color: C.mut, fontSize: 10.5 },
+      icon: 'roundRect',
+      itemWidth: 10,
+      itemHeight: 10,
+    },
+    grid: { top: 36, left: 40, right: 16, bottom: 60, containLabel: true },
+    xAxis: {
+      type: 'category',
+      data: dates,
+      axisLabel: { color: C.mut, fontSize: 10, rotate: 35 },
+      axisLine: { lineStyle: { color: '#E4E9F2' } },
+      axisTick: { show: false },
+    },
+    yAxis: {
+      type: 'value',
+      max: 100,
+      min: 0,
+      axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' },
+      axisLine: { show: false },
+      axisTick: { show: false },
+      splitLine: { lineStyle: { color: '#F2F5FA', type: 'dashed' } },
+    },
+    series: [
+      {
+        name: '沪深300ETF',
+        type: 'bar',
+        stack: 'w',
+        barWidth: '60%',
+        data: weights.map((w) => ({
+          value: +(w.hs300 * 100).toFixed(2),
+          itemStyle: { color: COLORS.hs300 },
+        })),
+      },
+      {
+        name: '中证500ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.zz500 * 100).toFixed(2),
+          itemStyle: { color: COLORS.zz500 },
+        })),
+      },
+      {
+        name: '五年国债ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.c5 * 100).toFixed(2),
+          itemStyle: { color: COLORS.c5 },
+        })),
+      },
+      {
+        name: '十年国债ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.c10 * 100).toFixed(2),
+          itemStyle: { color: COLORS.c10 },
+        })),
+      },
+      {
+        name: '黄金ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.gold * 100).toFixed(2),
+          itemStyle: { color: COLORS.gold },
+        })),
+      },
+      {
+        name: '豆粕ETF',
+        type: 'bar',
+        stack: 'w',
+        data: weights.map((w) => ({
+          value: +(w.meal * 100).toFixed(2),
+          itemStyle: { color: COLORS.meal },
+        })),
+      },
+    ],
+  }
+}
+
+// ===== 横向权重柱:最新一期各类占比 =====
+export function etfLatestWeightOption(weights) {
+  const w = weights[weights.length - 1]
+  const items = [
+    { name: '煤炭 ETF', value: w.coal * 100, color: '#3D5A80' },
+    { name: '银行 ETF', value: w.bank * 100, color: '#C49A2A' },
+    { name: '科创 ETF', value: w.tech * 100, color: '#5BA0F2' },
+  ]
+  return {
+    tooltip: {
+      ...tipOpt,
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      formatter: (p) => `${p[0].name}<br/>权重占比:${p[0].value.toFixed(1)}%`,
+    },
+    grid: { top: 18, left: 8, right: 40, bottom: 8, containLabel: true },
+    xAxis: {
+      type: 'value',
+      max: 100,
+      ...axisStyle({
+        axisLabel: { color: C.mut, fontSize: 10.5, formatter: '{value}%' },
+      }),
+    },
+    yAxis: {
+      type: 'category',
+      data: items.map((i) => i.name),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: C.ink, fontSize: 12, fontWeight: 600 },
+    },
+    series: [
+      {
+        type: 'bar',
+        barWidth: 22,
+        data: items.map((i) => ({
+          value: i.value,
+          itemStyle: { color: i.color, borderRadius: [0, 4, 4, 0] },
+        })),
+        label: {
+          show: true,
+          position: 'right',
+          color: C.ink,
+          fontSize: 11,
+          fontWeight: 600,
+          formatter: (p) => `${p.value.toFixed(1)}%`,
+        },
+      },
+    ],
+  }
+}
+
+export function allweatherLatestWeightOption(weights) {
+  const w = weights[weights.length - 1]
+  const items = [
+    { name: '沪深300ETF', value: w.hs300 * 100, color: '#1552D0' },
+    { name: '中证500ETF', value: w.zz500 * 100, color: '#3D5A80' },
+    { name: '五年国债ETF', value: w.c5 * 100, color: '#7B91FF' },
+    { name: '十年国债ETF', value: w.c10 * 100, color: '#0E3A96' },
+    { name: '黄金ETF', value: w.gold * 100, color: '#C49A2A' },
+    { name: '豆粕ETF', value: w.meal * 100, color: '#189A65' },
+  ]
+  return {
+    tooltip: {
+      ...tipOpt,
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      formatter: (p) => `${p[0].name}<br/>权重占比:${p[0].value.toFixed(1)}%`,
+    },
+    grid: { top: 18, left: 8, right: 40, bottom: 8, containLabel: true },
+    xAxis: {
+      type: 'value',
+      max: 100,
+      ...axisStyle({
+        axisLabel: { color: C.mut, fontSize: 10.5, formatter: '{value}%' },
+      }),
+    },
+    yAxis: {
+      type: 'category',
+      data: items.map((i) => i.name),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: C.ink, fontSize: 11, fontWeight: 600 },
+    },
+    series: [
+      {
+        type: 'bar',
+        barWidth: 18,
+        data: items.map((i) => ({
+          value: i.value,
+          itemStyle: { color: i.color, borderRadius: [0, 4, 4, 0] },
+        })),
+        label: {
+          show: true,
+          position: 'right',
+          color: C.ink,
+          fontSize: 11,
+          fontWeight: 600,
+          formatter: (p) => `${p.value.toFixed(1)}%`,
+        },
+      },
+    ],
+  }
+}

+ 1085 - 0
web-app/src/views/stock/etf/data.js

@@ -0,0 +1,1085 @@
+// ETF 策略模型 · 数据底稿
+// 数据源:4.2.3-ETF模型-数据底稿.xlsx
+// 数据截至 2026-10-04 / 2026-09-30
+
+export const palette = {
+  ink: '#1A2438',
+  mut: '#6B7A96',
+  faint: '#98A5BD',
+  line: '#E4E9F2',
+  brand: '#1552D0',
+  brandD: '#0E3A96',
+  brandLt: '#EAF1FF',
+  gold: '#C49A2A',
+  up: '#E13B3B',
+  upBg: '#FDEAEA',
+  down: '#189A65',
+  downBg: '#E4F5EE',
+  warn: '#E8912D',
+}
+
+// ===== 共用:日期格式化 =====
+export function formatDate(s) {
+  if (!s) return ''
+  const str = String(s).replace(/\./g, '/').replace(/-/g, '/')
+  if (str === '#######') return ''
+  const m = str.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})/)
+  if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`
+  return str
+}
+
+// ===== 模型一:ETF 配置模型(银行/煤炭/科创) =====
+// 净值数据:日频,来自 sheet 「ETF配置-净值走势图」(列 A/E/F)
+const RAW_ETF_DAILY = `
+2025/1/2	0.999700107	1
+2025/1/3	0.993112707	0.98977
+2025/1/6	0.994350163	0.98567
+2025/1/7	1.003419169	0.99258
+2025/1/8	1.001928699	0.99105
+2025/1/9	0.996027517	0.989
+2025/1/10	0.985846141	0.978
+2025/1/13	0.97899651	0.97416
+2025/1/14	1.000430274	0.99923
+2025/1/15	0.999086167	0.99335
+2025/1/16	1.001980312	0.99386
+2025/1/17	1.001607937	0.99719
+2025/1/20	0.997324767	1.00205
+2025/1/21	0.99502901	1.00333
+2025/1/22	0.984922528	0.99412
+2025/1/23	0.994134178	0.99693
+2025/1/24	0.999825485	1.00512
+2025/1/27	1.00114194	0.99949
+2025/2/5	0.99968986	0.99437
+2025/2/6	1.009978719	1.00588
+2025/2/7	1.013943492	1.02021
+2025/2/10	1.019849289	1.02123
+2025/2/11	1.018224178	1.01714
+2025/2/12	1.026209071	1.02686
+2025/2/13	1.020628126	1.0243
+2025/2/14	1.022346016	1.03198
+2025/2/17	1.024987746	1.03377
+2025/2/18	1.019369321	1.02635
+2025/2/19	1.024278662	1.03172
+2025/2/20	1.022993614	1.02865
+2025/2/21	1.036603204	1.04247
+2025/2/24	1.034531407	1.03914
+2025/2/25	1.0297156	1.02865
+2025/2/26	1.043130918	1.03709
+2025/2/27	1.047483704	1.04042
+2025/2/28	1.026024135	1.02174
+2025/3/3	1.023268734	1.01842
+2025/3/4	1.024408057	1.0174
+2025/3/5	1.030760298	1.02174
+2025/3/6	1.038250857	1.03581
+2025/3/7	1.037845295	1.03326
+2025/3/10	1.036441492	1.02916
+2025/3/11	1.038318373	1.03223
+2025/3/12	1.035178174	1.02968
+2025/3/13	1.042770005	1.02456
+2025/3/14	1.052022525	1.0504
+2025/3/17	1.053405446	1.04733
+2025/3/18	1.053147727	1.04963
+2025/3/19	1.056495808	1.05116
+2025/3/20	1.052595566	1.04272
+2025/3/21	1.040324527	1.02584
+2025/3/24	1.046779287	1.03019
+2025/3/25	1.051417562	1.0307
+2025/3/26	1.042868843	1.02661
+2025/3/27	1.044863725	1.03019
+2025/3/28	1.036312809	1.02533
+2025/3/31	1.032797514	1.01919
+2025/4/1	1.033015372	1.01867
+2025/4/2	1.037262743	1.01791
+2025/4/3	1.0392052	1.01279
+2025/4/7	0.972767337	0.94525
+2025/4/8	0.995963082	0.95421
+2025/4/9	0.993419139	0.96342
+2025/4/10	1.001756682	0.97953
+2025/4/11	1.003780139	0.98235
+2025/4/14	1.013509912	0.98439
+2025/4/15	1.022626455	0.98516
+2025/4/16	1.029197455	0.98746
+2025/4/17	1.030925998	0.98823
+2025/4/18	1.034539712	0.99079
+2025/4/21	1.028112169	0.9913
+2025/4/22	1.031615255	0.99207
+2025/4/23	1.027216997	0.99309
+2025/4/24	1.034328712	0.99309
+2025/4/25	1.032290397	0.99309
+2025/4/28	1.036077111	0.99156
+2025/4/29	1.031945453	0.98977
+2025/4/30	1.017719266	0.989
+2025/5/6	1.021912362	0.99847
+2025/5/7	1.0318207	1.0046
+2025/5/8	1.036933739	1.01049
+2025/5/9	1.038537381	1.00921
+2025/5/12	1.04226276	1.02072
+2025/5/13	1.050146037	1.02174
+2025/5/14	1.058764573	1.03479
+2025/5/15	1.053965373	1.02609
+2025/5/16	1.045298375	1.02047
+2025/5/19	1.044546754	1.01893
+2025/5/20	1.046748875	1.02405
+2025/5/21	1.052930225	1.02814
+2025/5/22	1.056284008	1.02763
+2025/5/23	1.047436852	1.02021
+2025/5/26	1.043442194	1.01484
+2025/5/27	1.041493237	1.00947
+2025/5/28	1.040864575	1.00844
+2025/5/29	1.045880312	1.01458
+2025/5/30	1.046733788	1.01279
+2025/6/3	1.052898418	1.01228
+2025/6/4	1.059193261	1.01663
+2025/6/5	1.055916115	1.01919
+2025/6/6	1.056456612	1.0197
+2025/6/9	1.060566209	1.022
+2025/6/10	1.056932688	1.01612
+2025/6/11	1.061254106	1.0243
+2025/6/12	1.059766112	1.02481
+2025/6/13	1.053567864	1.01816
+2025/6/16	1.059936588	1.02072
+2025/6/17	1.060678182	1.02047
+2025/6/18	1.062634888	1.02125
+2025/6/19	1.055765749	1.01314
+2025/6/20	1.059832373	1.01445
+2025/6/23	1.074358685	1.01785
+2025/6/24	1.078734164	1.02962
+2025/6/25	1.090375164	1.04662
+2025/6/26	1.088613742	1.0427
+2025/6/27	1.078922985	1.03668
+2025/6/30	1.081184419	1.04165
+2025/7/1	1.084590419	1.04322
+2025/7/2	1.09072974	1.04427
+2025/7/3	1.088763466	1.05029
+2025/7/4	1.097095166	1.05473
+2025/7/7	1.096484166	1.05003
+2025/7/8	1.101510415	1.05892
+2025/7/9	1.103618613	1.05813
+2025/7/10	1.114694034	1.06337
+2025/7/11	1.110670085	1.06703
+2025/7/14	1.11314161	1.06677
+2025/7/15	1.100099915	1.06677
+2025/7/16	1.097167966	1.06441
+2025/7/17	1.098531891	1.07305
+2025/7/18	1.105843676	1.08089
+2025/7/21	1.112858257	1.08743
+2025/7/22	1.144126412	1.09894
+2025/7/23	1.140045753	1.09842
+2025/7/24	1.144586074	1.10522
+2025/7/25	1.14451744	1.09947
+2025/7/28	1.134199647	1.10234
+2025/7/29	1.13688787	1.10784
+2025/7/30	1.134834183	1.10705
+2025/7/31	1.11672756	1.08691
+2025/8/1	1.112364169	1.08115
+2025/8/4	1.122585034	1.08613
+2025/8/5	1.132109355	1.09423
+2025/8/6	1.139912768	1.09711
+2025/8/7	1.140767178	1.09685
+2025/8/8	1.130308141	1.09423
+2025/8/11	1.131283741	1.0992
+2025/8/12	1.147483271	1.10601
+2025/8/13	1.146494959	1.11516
+2025/8/14	1.146547201	1.11333
+2025/8/15	1.155099294	1.12353
+2025/8/18	1.167882238	1.1319
+2025/8/19	1.158579792	1.12615
+2025/8/20	1.182065829	1.14158
+2025/8/21	1.185128313	1.14524
+2025/8/22	1.245271703	1.17167
+2025/8/25	1.269054243	1.19416
+2025/8/26	1.256346559	1.18788
+2025/8/27	1.244741871	1.17062
+2025/8/28	1.296379206	1.19259
+2025/8/29	1.282142433	1.20358
+2025/9/1	1.283322946	1.2075
+2025/9/2	1.27728195	1.2007
+2025/9/3	1.25433493	1.18998
+2025/9/4	1.227850613	1.16565
+2025/9/5	1.24575415	1.19128
+2025/9/8	1.244039746	1.19155
+2025/9/9	1.238572833	1.18448
+2025/9/10	1.242060123	1.1871
+2025/9/11	1.27333295	1.21901
+2025/9/12	1.273156376	1.20881
+2025/9/15	1.273144513	1.2109
+2025/9/16	1.276579539	1.20855
+2025/9/17	1.285160043	1.21718
+2025/9/18	1.274567713	1.20175
+2025/9/19	1.274562843	1.20436
+2025/9/22	1.286404476	1.20829
+2025/9/23	1.292776159	1.20907
+2025/9/24	1.311782159	1.2198
+2025/9/25	1.314133506	1.22817
+2025/9/26	1.305602023	1.21666
+2025/9/29	1.3074692	1.2368
+2025/9/30	1.312692294	1.2402
+2025/10/9	1.33717998	1.25982
+2025/10/10	1.33535054	1.23445
+2025/10/13	1.340028231	1.22738
+2025/10/14	1.356290481	1.21509
+2025/10/15	1.361558645	1.23105
+2025/10/16	1.384028276	1.23497
+2025/10/17	1.368774114	1.2096
+2025/10/20	1.394823488	1.21378
+2025/10/21	1.395078745	1.23209
+2025/10/22	1.387532548	1.22817
+2025/10/23	1.405719598	1.23288
+2025/10/24	1.401678001	1.24779
+2025/10/27	1.411403927	1.26244
+2025/10/28	1.400901857	1.25616
+2025/10/29	1.403996236	1.27185
+2025/10/30	1.395030019	1.26165
+2025/10/31	1.380371943	1.24413
+2025/11/3	1.399122639	1.24596
+2025/11/4	1.404901306	1.23706
+2025/11/5	1.416502953	1.23863
+2025/11/6	1.429367302	1.25694
+2025/11/7	1.425186702	1.25433
+2025/11/10	1.430428234	1.25747
+2025/11/11	1.41877047	1.24596
+2025/11/12	1.411142622	1.24648
+2025/11/13	1.42106472	1.25878
+2025/11/14	1.407339289	1.2402
+2025/11/17	1.411151836	1.23209
+2025/11/18	1.386526225	1.22503
+2025/11/19	1.387022008	1.22869
+2025/11/20	1.373115379	1.2232
+2025/11/21	1.340401817	1.1939
+2025/11/24	1.332876751	1.19207
+2025/11/25	1.345179583	1.20253
+2025/11/26	1.339646151	1.21012
+2025/11/27	1.344143867	1.20907
+2025/11/28	1.343685905	1.21247
+2025/12/1	1.354404955	1.22608
+2025/12/2	1.350042832	1.22058
+2025/12/3	1.347879391	1.21509
+2025/12/4	1.343939164	1.21823
+2025/12/5	1.33899515	1.22895
+2025/12/8	1.336339564	1.23785
+2025/12/9	1.3279399	1.23157
+2025/12/10	1.321173768	1.23
+2025/12/11	1.311855018	1.22111
+2025/12/12	1.313655427	1.22791
+2025/12/15	1.313299809	1.22084
+2025/12/16	1.2948259	1.20855
+2025/12/17	1.3034948	1.22922
+2025/12/18	1.318522155	1.22111
+2025/12/19	1.315438864	1.22634
+2025/12/22	1.317109318	1.23732
+2025/12/23	1.316538282	1.23994
+2025/12/24	1.316264566	1.24439
+2025/12/25	1.313873866	1.24674
+2025/12/26	1.313329266	1.25145
+2025/12/29	1.319849766	1.24596
+2025/12/30	1.318836766	1.24857
+2025/12/31	1.311364349	1.24334
+2026/1/5	1.320235049	1.26715
+2026/1/6	1.331226149	1.28677
+2026/1/7	1.337509849	1.28206
+2026/1/8	1.331276849	1.27212
+2026/1/9	1.332335649	1.27787
+2026/1/12	1.341107449	1.2852
+2026/1/13	1.336100649	1.28075
+2026/1/14	1.324995649	1.2729
+2026/1/15	1.316288349	1.27526
+2026/1/16	1.307725449	1.27107
+2026/1/19	1.303195849	1.27161
+2026/1/20	1.311991049	1.26785
+2026/1/21	1.304176849	1.26946
+2026/1/22	1.305275349	1.26865
+2026/1/23	1.297586649	1.26248
+2026/1/26	1.303035949	1.26463
+2026/1/27	1.298706349	1.26409
+2026/1/28	1.307249049	1.26812
+2026/1/29	1.312252349	1.27966
+2026/1/30	1.308189444	1.26436
+2026/2/2	1.272325944	1.23457
+2026/2/3	1.276179344	1.25175
+2026/2/4	1.301961644	1.26329
+2026/2/5	1.295337744	1.25577
+2026/2/6	1.288743244	1.24772
+2026/2/9	1.303180644	1.26865
+2026/2/10	1.312102444	1.27027
+2026/2/11	1.312624844	1.26758
+2026/2/12	1.314215644	1.26865
+2026/2/13	1.302018244	1.25363
+2026/2/24	1.309758244	1.2657
+2026/2/25	1.309314144	1.27483
+2026/2/26	1.310014744	1.2708
+2026/2/27	1.320302413	1.26812
+2026/3/2	1.326278813	1.27188
+2026/3/3	1.317174713	1.2555
+2026/3/4	1.302205213	1.23725
+2026/3/5	1.313162013	1.24906
+2026/3/6	1.312918113	1.25255
+2026/3/9	1.311036713	1.24235
+2026/3/10	1.311210213	1.25685
+2026/3/11	1.318177613	1.26436
+2026/3/12	1.332841613	1.2598
+2026/3/13	1.329361813	1.25524
+2026/3/16	1.326336413	1.25604
+2026/3/17	1.314421113	1.24692
+2026/3/18	1.314134813	1.25121
+2026/3/19	1.310938713	1.2343
+2026/3/20	1.302958313	1.22813
+2026/3/23	1.270154613	1.18894
+2026/3/24	1.287914613	1.2021
+2026/3/25	1.294158313	1.21954
+2026/3/26	1.291960813	1.20451
+2026/3/27	1.290722613	1.20988
+2026/3/30	1.287920013	1.20773
+2026/3/31	1.270237213	1.1978
+2026/4/1	1.274209613	1.21686
+2026/4/2	1.275932513	1.20478
+2026/4/3	1.253456813	1.19539
+2026/4/7	1.271311813	1.19324
+2026/4/8	1.275742813	1.23511
+2026/4/9	1.266782713	1.22706
+2026/4/10	1.271522813	1.24584
+2026/4/13	1.279212813	1.24853
+2026/4/14	1.281122113	1.26463
+2026/4/15	1.287181913	1.2598
+2026/4/16	1.290156413	1.27375
+2026/4/17	1.277929713	1.27188
+2026/4/20	1.286514113	1.27885
+2026/4/21	1.298789313	1.28181
+2026/4/22	1.302837713	1.2912
+2026/4/23	1.319559113	1.28852
+2026/4/24	1.322904613	1.28342
+2026/4/27	1.336605513	1.28342
+2026/4/28	1.356596413	1.2802
+2026/4/29	1.367880213	1.29388
+2026/4/30	1.377299147	1.29469
+2026/5/6	1.397763647	1.31186
+2026/5/7	1.380472347	1.31831
+2026/5/8	1.367554847	1.31133
+2026/5/11	1.388773847	1.3328
+2026/5/12	1.385275247	1.33199
+2026/5/13	1.392824347	1.34649
+2026/5/14	1.376459147	1.32609
+2026/5/15	1.372047047	1.30918
+2026/5/18	1.378386447	1.3022
+2026/5/19	1.396603747	1.30677
+2026/5/20	1.400558747	1.3073
+2026/5/21	1.371334047	1.29039
+2026/5/22	1.373708947	1.30489
+2026/5/25	1.413254647	1.32502
+2026/5/26	1.403854047	1.33441
+2026/5/27	1.396543247	1.32367
+2026/5/28	1.407126747	1.32367
+2026/5/29	1.402124909	1.32126
+2026/6/1	1.417173309	1.3065
+2026/6/2	1.422036809	1.32475
+2026/6/3	1.426126709	1.33253
+2026/6/4	1.421244209	1.32206
+2026/6/5	1.418996409	1.29979
+2026/6/8	1.417853109	1.27188
+2026/6/9	1.424182109	1.29522
+2026/6/10	1.432494109	1.28395
+2026/6/11	1.429826309	1.2751
+2026/6/12	1.447347609	1.29308
+2026/6/15	1.432310609	1.32018
+2026/6/16	1.413439309	1.31777
+2026/6/17	1.409226309	1.33065
+2026/6/18	1.387018909	1.33763
+2026/6/22	1.401171509	1.36635
+2026/6/23	1.406351909	1.32636
+2026/6/24	1.381185109	1.33307
+2026/6/25	1.378410309	1.35481
+2026/6/26	1.360720309	1.31696
+2026/6/29	1.381131509	1.33092
+2026/6/30	1.364049983	1.34702
+2026/7/1	1.362026883	1.34139
+2026/7/2	1.331958283	1.30167
+2026/7/3	1.332107383	1.30864
+2026/7/6	1.359676983	1.30891
+2026/7/7	1.351388083	1.29522
+2026/7/8	1.363837883	1.28771
+2026/7/9	1.395000483	1.31938
+2026/7/10	1.368298683	1.29603
+2026/7/13	1.360169583	1.27322
+2026/7/14	1.377147883	1.29818
+2026/7/15	1.368195583	1.29845
+2026/7/16	1.336571283	1.27563
+2026/7/17	1.305490083	1.23162
+2026/7/20	1.340157283	1.24799
+2026/7/21	1.365302383	1.28476
+2026/7/22	1.370078683	1.27885
+2026/7/23	1.360759383	1.28476
+2026/7/24	1.345997083	1.26168
+2026/7/27	1.353050683	1.27563
+2026/7/28	1.331412383	1.24182
+2026/7/29	1.337030583	1.24987
+2026/7/30	1.328890583	1.23591
+2026/7/31	1.334819152	1.24879
+2026/8/3	1.320016952	1.2343
+2026/8/4	1.303165652	1.24826
+2026/8/5	1.300956152	1.26517
+2026/8/6	1.320780052	1.26382
+2026/8/7	1.315027852	1.2751
+2026/8/10	1.325708452	1.27724
+2026/8/11	1.322259552	1.26892
+2026/8/12	1.319768552	1.27429
+2026/8/13	1.319137752	1.26919
+2026/8/14	1.318217852	1.26839
+2026/8/17	1.322544152	1.28852
+2026/8/18	1.330988352	1.28476
+2026/8/19	1.335382052	1.24906
+2026/8/20	1.336830752	1.24879
+2026/8/21	1.336325452	1.25604
+2026/8/24	1.349699652	1.24182
+2026/8/25	1.344086852	1.23886
+2026/8/26	1.353840352	1.24853
+2026/8/27	1.359403652	1.25899
+2026/8/28	1.352994052	1.25577
+2026/8/31	1.373237875	1.25738
+2026/9/1	1.377937675	1.25711
+2026/9/2	1.366953475	1.23994
+2026/9/3	1.360435175	1.24021
+2026/9/4	1.363154175	1.23886
+2026/9/7	1.352288975	1.24369
+2026/9/8	1.358093675	1.24101
+2026/9/9	1.363802775	1.2445
+2026/9/10	1.369429675	1.23913
+2026/9/11	1.357819775	1.22893
+2026/9/14	1.356477075	1.22169
+2026/9/15	1.349559675	1.2139
+2026/9/16	1.357175075	1.22115
+2026/9/17	1.350512675	1.21632
+2026/9/18	1.355560375	1.22974
+2026/9/21	1.365165575	1.23672
+2026/9/22	1.367978075	1.23806
+2026/9/23	1.356221675	1.23189
+2026/9/24	1.355051775	1.21176
+2026/9/28	1.339547175	1.18546
+2026/9/29	1.343479575	1.18519
+2026/9/30	1.349019782	1.18948
+2026/10/8	1.363307282	1.17794
+2026/10/9	1.359156997	1.17687
+`;
+
+export const etfModel = {
+  name: 'ETF 配置模型',
+  description:
+    '使用预期损失(ES)替代传统波动率,资产的目标权重与其 ES 风险成反比,每月末重新调整权重,区分上涨/下跌风险,降低处于下行中的资产权重。',
+  pool: '银行 ETF · 煤炭 ETF · 科创 50 ETF',
+  initialWeight: '40% / 30% / 30%',
+  source: '副本4.2.3-ETF模型-数据底稿.xlsx · ETF配置-净值走势图',
+}
+
+// 解析日频策略净值序列:date / 策略净值 / 沪深300基准净值
+function parseTwoSeries(raw) {
+  return raw.split('\n')
+    .map((line) => {
+      const parts = line.split('\t').map((x) => x.trim())
+      const d = formatDate(parts[0])
+      if (!d) return null
+      const nv = parseFloat(parts[1])
+      const bv = parseFloat(parts[2])
+      if (!Number.isFinite(nv) || !Number.isFinite(bv)) return null
+      return [d, nv, bv]
+    })
+    .filter(Boolean)
+}
+
+const _twoCols = parseTwoSeries(RAW_ETF_DAILY)
+export const etfNetSeries = _twoCols.map(([d, nv]) => [d, nv, null])
+export const etfBenchmarkSeries = _twoCols.map(([d, , bv]) => [d, bv])
+
+// 注入 etfModel 的起止日期(取自净值序列首末行,与底稿保持一致)
+etfModel.startDate = etfNetSeries[0][0]
+etfModel.asOf = etfNetSeries[etfNetSeries.length - 1][0]
+
+// 月度调仓权重:sheet 「ETF配置-每月调仓权重」
+const RAW_W = `2025-01-02	首次建仓	30.0%	40.0%	30.0%
+2025-01-27	月末调仓	18.3%	49.7%	32.0%
+2025-02-28	月末调仓	25.6%	52.6%	21.8%
+2025-03-31	月末调仓	25.7%	64.8%	9.5%
+2025-04-30	月末调仓	15.2%	54.2%	30.6%
+2025-05-30	月末调仓	34.6%	43.1%	22.3%
+2025-06-30	月末调仓	34.6%	37.4%	28.0%
+2025-07-31	月末调仓	21.1%	24.4%	54.5%
+2025-08-29	月末调仓	21.9%	35.9%	42.2%
+2025-09-30	月末调仓	44.9%	34.3%	20.8%
+2025-10-31	月末调仓	48.6%	30.2%	21.2%
+2025-11-28	月末调仓	41.3%	36.4%	22.3%
+2025-12-31	月末调仓	18.1%	65.5%	16.4%
+2026-01-30	月末调仓	23.5%	36.7%	39.8%
+2026-02-27	月末调仓	22.6%	41.0%	36.4%
+2026-03-31	月末调仓	51.3%	29.7%	19.0%
+2026-04-30	月末调仓	32.4%	37.5%	30.2%
+2026-05-29	月末调仓	14.7%	71.4%	13.9%
+2026-06-30	月末调仓	22.5%	37.9%	39.6%
+2026-07-31	月末调仓	21.0%	67.5%	11.6%
+2026-08-31	月末调仓	17.2%	60.8%	22.0%
+2026-09-30	月末调仓	30.7%	62.7%	6.6%`
+
+export const etfWeights = RAW_W.split('\n').map((line) => {
+  const [d, type, c, b, t] = line.split('\t')
+  return {
+    date: d,
+    type,
+    coal: parseFloat(c) / 100,
+    bank: parseFloat(b) / 100,
+    tech: parseFloat(t) / 100,
+  }
+})
+
+// ===== 模型二:全天候策略 =====
+const RAW_ALLWEATHER_NET = `2025/1/2	0.999704
+2025/1/3	0.999858
+2025/1/6	0.999304
+2025/1/7	0.997445
+2025/1/8	0.998048
+2025/1/9	0.996464
+2025/1/10	0.997715
+2025/1/13	1.000866
+2025/1/14	1.002862
+2025/1/15	1.004844
+2025/1/16	1.003919
+2025/1/17	1.005369
+2025/1/20	1.006607
+2025/1/21	1.008704
+2025/1/22	1.009469
+2025/1/23	1.008737
+2025/1/24	1.010094
+2025/1/27	1.010288
+2025/2/5	1.020067
+2025/2/6	1.020126
+2025/2/7	1.021546
+2025/2/10	1.02329
+2025/2/11	1.024707
+2025/2/12	1.023136
+2025/2/13	1.02316
+2025/2/14	1.02467
+2025/2/17	1.019757
+2025/2/18	1.019753
+2025/2/19	1.023952
+2025/2/20	1.02428
+2025/2/21	1.023828
+2025/2/24	1.020358
+2025/2/25	1.020521
+2025/2/26	1.019662
+2025/2/27	1.020011
+2025/2/28	1.017192
+2025/3/3	1.020919
+2025/3/4	1.020982
+2025/3/5	1.019335
+2025/3/6	1.020016
+2025/3/7	1.017816
+2025/3/10	1.017905
+2025/3/11	1.013171
+2025/3/12	1.01533
+2025/3/13	1.016442
+2025/3/14	1.020959
+2025/3/17	1.019118
+2025/3/18	1.020965
+2025/3/19	1.022283
+2025/3/20	1.021695
+2025/3/21	1.019207
+2025/3/24	1.018992
+2025/3/25	1.020236
+2025/3/26	1.019718
+2025/3/27	1.020769
+2025/3/28	1.021403
+2025/3/31	1.023323
+2025/4/1	1.023217
+2025/4/2	1.024211
+2025/4/3	1.029457
+2025/4/7	1.019279
+2025/4/8	1.023368
+2025/4/9	1.025644
+2025/4/10	1.029629
+2025/4/11	1.032599
+2025/4/14	1.034521
+2025/4/15	1.033325
+2025/4/16	1.035512
+2025/4/17	1.036673
+2025/4/18	1.036866
+2025/4/21	1.040522
+2025/4/22	1.045951
+2025/4/23	1.036263
+2025/4/24	1.037467
+2025/4/25	1.036211
+2025/4/28	1.033338
+2025/4/29	1.034745
+2025/4/30	1.033953
+2025/5/6	1.036073
+2025/5/7	1.036587
+2025/5/8	1.037246
+2025/5/9	1.036614
+2025/5/12	1.035568
+2025/5/13	1.034292
+2025/5/14	1.034421
+2025/5/15	1.03136
+2025/5/16	1.031011
+2025/5/19	1.031428
+2025/5/20	1.032047
+2025/5/21	1.035863
+2025/5/22	1.035429
+2025/5/23	1.034816
+2025/5/26	1.034109
+2025/5/27	1.033727
+2025/5/28	1.033275
+2025/5/29	1.032591
+2025/5/30	1.033758
+2025/6/3	1.033858
+2025/6/4	1.034781
+2025/6/5	1.035991
+2025/6/6	1.038402
+2025/6/9	1.038982
+2025/6/10	1.038997
+2025/6/11	1.04042
+2025/6/12	1.041027
+2025/6/13	1.040881
+2025/6/16	1.041237
+2025/6/17	1.042611
+2025/6/18	1.04098
+2025/6/19	1.040695
+2025/6/20	1.040018
+2025/6/23	1.039417
+2025/6/24	1.039446
+2025/6/25	1.039264
+2025/6/26	1.037605
+2025/6/27	1.037953
+2025/6/30	1.038079
+2025/7/1	1.039239
+2025/7/2	1.039516
+2025/7/3	1.040686
+2025/7/4	1.040698
+2025/7/7	1.039864
+2025/7/8	1.040374
+2025/7/9	1.039656
+2025/7/10	1.039675
+2025/7/11	1.040169
+2025/7/14	1.040381
+2025/7/15	1.041123
+2025/7/16	1.0408
+2025/7/17	1.042449
+2025/7/18	1.043155
+2025/7/21	1.043686
+2025/7/22	1.044358
+2025/7/23	1.044705
+2025/7/24	1.041123
+2025/7/25	1.040256
+2025/7/28	1.041124
+2025/7/29	1.039755
+2025/7/30	1.041388
+2025/7/31	1.040716
+2025/8/1	1.04066
+2025/8/4	1.042701
+2025/8/5	1.043913
+2025/8/6	1.044997
+2025/8/7	1.045684
+2025/8/8	1.046484
+2025/8/11	1.044443
+2025/8/12	1.045067
+2025/8/13	1.048808
+2025/8/14	1.04758
+2025/8/15	1.048358
+2025/8/18	1.048224
+2025/8/19	1.048011
+2025/8/20	1.048626
+2025/8/21	1.047703
+2025/8/22	1.048829
+2025/8/25	1.05335
+2025/8/26	1.052863
+2025/8/27	1.051566
+2025/8/28	1.053293
+2025/8/29	1.054767
+2025/9/1	1.059139
+2025/9/2	1.058156
+2025/9/3	1.060883
+2025/9/4	1.058634
+2025/9/5	1.059738
+2025/9/8	1.061407
+2025/9/9	1.063286
+2025/9/10	1.061625
+2025/9/11	1.063546
+2025/9/12	1.064655
+2025/9/15	1.063785
+2025/9/16	1.066138
+2025/9/17	1.066149
+2025/9/18	1.063173
+2025/9/19	1.063348
+2025/9/22	1.066448
+2025/9/23	1.057578
+2025/9/24	1.058953
+2025/9/25	1.058591
+2025/9/26	1.058237
+2025/9/29	1.060324
+2025/9/30	1.063147
+2025/10/9	1.074168
+2025/10/10	1.068464
+2025/10/13	1.074597
+2025/10/14	1.074258
+2025/10/15	1.080032
+2025/10/16	1.081626
+2025/10/17	1.083659
+2025/10/20	1.078119
+2025/10/21	1.085311
+2025/10/22	1.076787
+2025/10/23	1.077225
+2025/10/24	1.077414
+2025/10/27	1.080188
+2025/10/28	1.074321
+2025/10/29	1.07784
+2025/10/30	1.076238
+2025/10/31	1.077436
+2025/11/3	1.078028
+2025/11/4	1.075363
+2025/11/5	1.078459
+2025/11/6	1.079073
+2025/11/7	1.078984
+2025/11/10	1.080279
+2025/11/11	1.079822
+2025/11/12	1.080084
+2025/11/13	1.082469
+2025/11/14	1.082189
+2025/11/17	1.07763
+2025/11/18	1.080912
+2025/11/19	1.078784
+2025/11/20	1.077912
+2025/11/21	1.073901
+2025/11/24	1.077231
+2025/11/25	1.079534
+2025/11/26	1.07793
+2025/11/27	1.080158
+2025/11/28	1.081154
+2025/12/1	1.080661
+2025/12/2	1.07932
+2025/12/3	1.079035
+2025/12/4	1.077222
+2025/12/5	1.079022
+2025/12/8	1.079161
+2025/12/9	1.078587
+2025/12/10	1.079438
+2025/12/11	1.079248
+2025/12/12	1.080951
+2025/12/15	1.080211
+2025/12/16	1.078861
+2025/12/17	1.080785
+2025/12/18	1.080531
+2025/12/19	1.081754
+2025/12/22	1.083234
+2025/12/23	1.086075
+2025/12/24	1.086595
+2025/12/25	1.086457
+2025/12/26	1.083988
+2025/12/29	1.081495
+2025/12/30	1.079573
+2025/12/31	1.078234
+2026/1/5	1.082829
+2026/1/6	1.085607
+2026/1/7	1.08659
+2026/1/8	1.085913
+2026/1/9	1.089486
+2026/1/12	1.092449
+2026/1/13	1.092112
+2026/1/14	1.09312
+2026/1/15	1.092838
+2026/1/16	1.091918
+2026/1/19	1.092087
+2026/1/20	1.092974
+2026/1/21	1.096682
+2026/1/22	1.097656
+2026/1/23	1.101297
+2026/1/26	1.10408
+2026/1/27	1.104056
+2026/1/28	1.109531
+2026/1/29	1.117824
+2026/1/30	1.105163
+2026/2/2	1.098228
+2026/2/3	1.10179
+2026/2/4	1.103977
+2026/2/5	1.102931
+2026/2/6	1.102541
+2026/2/9	1.105947
+2026/2/10	1.105175
+2026/2/11	1.106735
+2026/2/12	1.107606
+2026/2/13	1.10681
+2026/2/24	1.109046
+2026/2/25	1.110225
+2026/2/26	1.108882
+2026/2/27	1.109884
+2026/3/2	1.112898
+2026/3/3	1.1124
+2026/3/4	1.114836
+2026/3/5	1.112501
+2026/3/6	1.116829
+2026/3/9	1.121488
+2026/3/10	1.119216
+2026/3/11	1.123427
+2026/3/12	1.122531
+2026/3/13	1.127467
+2026/3/16	1.122448
+2026/3/17	1.121109
+2026/3/18	1.122427
+2026/3/19	1.120446
+2026/3/20	1.116547
+2026/3/23	1.113763
+2026/3/24	1.116065
+2026/3/25	1.109006
+2026/3/26	1.108442
+2026/3/27	1.106833
+2026/3/30	1.10867
+2026/3/31	1.104269
+2026/4/1	1.104905
+2026/4/2	1.104282
+2026/4/3	1.103603
+2026/4/7	1.10365
+2026/4/8	1.107127
+2026/4/9	1.107117
+2026/4/10	1.108629
+2026/4/13	1.110271
+2026/4/14	1.111961
+2026/4/15	1.111916
+2026/4/16	1.113927
+2026/4/17	1.114099
+2026/4/20	1.114548
+2026/4/21	1.115677
+2026/4/22	1.118615
+2026/4/23	1.117197
+2026/4/24	1.115571
+2026/4/27	1.115173
+2026/4/28	1.11532
+2026/4/29	1.116824
+2026/4/30	1.116825
+2026/5/6	1.117908
+2026/5/7	1.118324
+2026/5/8	1.118836
+2026/5/11	1.120303
+2026/5/12	1.120571
+2026/5/13	1.121963
+2026/5/14	1.119973
+2026/5/15	1.116739
+2026/5/18	1.116918
+2026/5/19	1.119034
+2026/5/20	1.118163
+2026/5/21	1.116022
+2026/5/22	1.116901
+2026/5/25	1.117451
+2026/5/26	1.117903
+2026/5/27	1.11769
+2026/5/28	1.118318
+2026/5/29	1.118934
+2026/6/1	1.118234
+2026/6/2	1.11838
+2026/6/3	1.118087
+2026/6/4	1.117434
+2026/6/5	1.11563
+2026/6/6	1.11563
+2026/6/7	1.11563
+2026/6/8	1.111408
+2026/6/9	1.111811
+2026/6/10	1.11015
+2026/6/11	1.108189
+2026/6/12	1.109884
+2026/6/13	1.109884
+2026/6/14	1.109884
+2026/6/15	1.113018
+2026/6/16	1.114166
+2026/6/17	1.115773
+2026/6/18	1.115576
+2026/6/19	1.115576
+2026/6/20	1.115576
+2026/6/21	1.115576
+2026/6/22	1.115507
+2026/6/23	1.112406
+2026/6/24	1.113677
+2026/6/25	1.1069
+2026/6/26	1.106604
+2026/6/27	1.106604
+2026/6/28	1.106604
+2026/6/29	1.108551
+2026/6/30	1.107895
+2026/7/1	1.106803
+2026/7/2	1.112943
+2026/7/3	1.11516
+2026/7/4	1.11516
+2026/7/5	1.11516
+2026/7/6	1.12044
+2026/7/7	1.121452
+2026/7/8	1.123682
+2026/7/9	1.12321
+2026/7/10	1.12073
+2026/7/11	1.12073
+2026/7/12	1.12073
+2026/7/13	1.122781
+2026/7/14	1.123425
+2026/7/15	1.125054
+2026/7/16	1.115476
+2026/7/17	1.109374
+2026/7/18	1.109374
+2026/7/19	1.109374
+2026/7/20	1.115014
+2026/7/21	1.120955
+2026/7/22	1.123424
+2026/7/23	1.126293
+2026/7/24	1.127592
+2026/7/25	1.127592
+2026/7/26	1.127592
+2026/7/27	1.126517
+2026/7/28	1.110686
+2026/7/29	1.112054
+2026/7/30	1.106774
+2026/7/31	1.109027
+2026/8/1	1.109027
+2026/8/2	1.109027
+2026/8/3	1.108559
+2026/8/4	1.109587
+2026/8/5	1.111961
+2026/8/6	1.113516
+2026/8/7	1.115304
+2026/8/8	1.115304
+2026/8/9	1.115304
+2026/8/10	1.116624
+2026/8/11	1.116029
+2026/8/12	1.116904
+2026/8/13	1.116741
+2026/8/14	1.116872
+2026/8/15	1.116872
+2026/8/16	1.116872
+2026/8/17	1.118841
+2026/8/18	1.120294
+2026/8/19	1.118113
+2026/8/20	1.119683
+2026/8/21	1.120447
+2026/8/22	1.120447
+2026/8/23	1.120447
+2026/8/24	1.121979
+2026/8/25	1.121811
+2026/8/26	1.122087
+2026/8/27	1.121997
+2026/8/28	1.121847
+2026/8/29	1.121847
+2026/8/30	1.121847
+2026/8/31	1.1202
+2026/9/1	1.121465
+2026/9/2	1.119523
+2026/9/3	1.121631
+2026/9/4	1.122273
+2026/9/5	1.122273
+2026/9/6	1.122273
+2026/9/7	1.1219
+2026/9/8	1.122361
+2026/9/9	1.121618
+2026/9/10	1.120038
+2026/9/11	1.119057
+2026/9/12	1.119057
+2026/9/13	1.119057
+2026/9/14	1.118213
+2026/9/15	1.118388
+2026/9/16	1.120406
+2026/9/17	1.120063
+2026/9/18	1.114307
+2026/9/19	1.114307
+2026/9/20	1.114307
+2026/9/21	1.11413
+2026/9/22	1.114574
+2026/9/23	1.114043
+2026/9/24	1.113514
+2026/9/25	1.113514
+2026/9/26	1.113514
+2026/9/27	1.113514
+2026/9/28	1.110631
+2026/9/29	1.110886
+2026/9/30	1.111167`
+
+export const allweatherModel = {
+  name: '全天候策略模型',
+  description:
+    '将宏观经济环境划分为增长与通胀的四种组合状态,将风险平均分配于四种宏观情景,再为每类情景配置占优资产,从而构建一个在各类经济环境下均稳健的投资组合(Bridgewater All-Weather 思路)。',
+  pool: '沪深 300 ETF · 中证 500 ETF · 五年国债 ETF · 十年国债 ETF · 黄金 ETF · 豆粕 ETF',
+  source: '4.2.3-ETF模型-数据底稿.xlsx · 全天候策略-净值走势图',
+}
+
+export const allweatherNetSeries = RAW_ALLWEATHER_NET.split('\n')
+  .map((line) => {
+    const [d, nv, dd] = line.split('\t')
+    const fmt = formatDate(d)
+    if (!fmt) return null
+    const v = parseFloat(nv)
+    const drawdown = parseFloat(dd)
+    if (!Number.isFinite(v)) return null
+    return [fmt, v, Number.isFinite(drawdown) ? drawdown : null]
+  })
+  .filter(Boolean)
+
+// 注入 allweatherModel 的起止日期(取自净值序列首末行,与底稿保持一致)
+allweatherModel.startDate = allweatherNetSeries[0][0]
+allweatherModel.asOf = allweatherNetSeries[allweatherNetSeries.length - 1][0]
+
+// 全天候每月调仓权重:sheet 「全天候策略-每月调仓权重」
+const RAW_AW_W = `2025-01-02	2024-12-31	4.9%	3.9%	38.6%	25.3%	16.0%	11.4%
+2025-01-27	2025-01-24	4.6%	3.5%	39.5%	27.4%	16.0%	8.9%
+2025-02-28	2025-02-27	6.3%	4.5%	40.0%	28.4%	11.9%	9.0%
+2025-03-31	2025-03-28	6.2%	5.5%	40.0%	26.3%	13.0%	9.0%
+2025-04-30	2025-04-29	4.7%	3.2%	47.9%	28.9%	5.9%	9.4%
+2025-05-30	2025-05-29	4.6%	3.3%	45.0%	32.3%	5.0%	9.6%
+2025-06-30	2025-06-27	3.3%	2.9%	46.9%	34.5%	5.5%	6.9%
+2025-07-31	2025-07-30	5.6%	4.4%	45.8%	26.5%	8.9%	8.8%
+2025-08-29	2025-08-28	3.1%	2.8%	47.7%	25.4%	12.4%	8.6%
+2025-09-30	2025-09-29	6.7%	5.2%	28.1%	25.9%	16.1%	18.0%
+2025-10-31	2025-10-30	5.4%	4.0%	36.2%	30.6%	6.0%	17.9%
+2025-11-28	2025-11-27	4.2%	3.2%	44.4%	33.5%	5.8%	8.9%
+2025-12-31	2025-12-30	6.6%	4.8%	41.5%	24.1%	8.4%	14.7%
+2026-01-30	2026-01-29	4.5%	2.8%	49.1%	28.3%	3.5%	11.8%
+2026-02-27	2026-02-26	3.4%	1.9%	51.2%	30.1%	2.0%	11.3%
+2026-03-31	2026-03-30	4.4%	2.8%	48.7%	35.6%	3.0%	5.4%
+2026-04-30	2026-04-29	3.8%	2.5%	50.7%	34.0%	3.8%	5.3%
+2026-05-29	2026-05-28	3.1%	2.0%	52.6%	32.9%	4.1%	5.3%
+2026-06-30	2026-06-29	4.1%	3.6%	41.1%	26.9%	7.5%	16.8%
+2026-07-31	2026-07-30	2.0%	1.3%	53.9%	34.2%	5.7%	3.0%
+2026-08-31	2026-08-30	2.1%	1.2%	57.2%	32.6%	3.7%	3.2%
+2026-09-30	2026-09-29	5.0%	3.4%	53.4%	24.9%	7.5%	5.8%`
+
+export const allweatherWeights = RAW_AW_W.split('\n').map((line) => {
+  const [d, sig, hs300, zz500, c5, c10, gold, meal] = line.split('\t')
+  return {
+    date: d,
+    signalDate: sig,
+    hs300: parseFloat(hs300) / 100,
+    zz500: parseFloat(zz500) / 100,
+    c5: parseFloat(c5) / 100,
+    c10: parseFloat(c10) / 100,
+    gold: parseFloat(gold) / 100,
+    meal: parseFloat(meal) / 100,
+  }
+})
+
+// ===== 通用指标:净值统计 =====
+export function summarizeSeries(series) {
+  if (!series.length) return null
+  const nv = series.map((s) => s[1])
+  const latest = nv[nv.length - 1]
+  const totalRet = (latest - 1) * 100
+  let peak = -Infinity
+  let mdd = 0
+  for (const v of nv) {
+    if (v > peak) peak = v
+    const dd = (v - peak) / peak
+    if (dd < mdd) mdd = dd
+  }
+
+  let weeklyRet = null
+  if (series[0].length >= 3 && series[0][2] != null) {
+    weeklyRet = series[series.length - 1][2]
+  } else if (nv.length >= 5) {
+    weeklyRet = (nv[nv.length - 1] / nv[nv.length - 5] - 1) * 100
+  }
+
+  return {
+    latest,
+    totalReturn: totalRet,
+    maxDrawdown: mdd * 100,
+    weeklyReturn: weeklyRet,
+    start: series[0][0],
+    end: series[series.length - 1][0],
+  }
+}
+
+export const etfSummary = summarizeSeries(etfNetSeries)
+export const allweatherSummary = summarizeSeries(allweatherNetSeries)

+ 844 - 0
web-app/src/views/stock/etf/index.vue

@@ -0,0 +1,844 @@
+<template>
+  <div class="etf-page">
+    <header class="page-hd">
+      <div>
+        <div class="page-title">ETF 策略模型</div>
+        <div class="page-sub">
+          ETF配置模型(全天候策略)· 数据截至 ETF {{ etfModel.asOf }} / AW {{ allweatherModel.asOf }}
+        </div>
+      </div>
+      <div class="page-actions">
+        <!-- <span class="chip" :class="etfSummaryCls">
+          ETF · 累计 {{ etfSummaryText }}
+        </span>
+        <span class="chip" :class="awSummaryCls">
+          全天候 · 累计 {{ awSummaryText }}
+        </span> -->
+        <button class="btn" @click="reload" :disabled="loading">
+          <span :class="['ic-refresh', { spinning: loading }]">↻</span> 刷新
+        </button>
+      </div>
+    </header>
+
+    <!-- 双模型 KPI 总览 -->
+    <section class="kpi-grid">
+      <div class="kpi-card hero" :class="etfSummaryCls">
+        <div class="label">ETF 配置模型 · 累计收益</div>
+        <div class="value">
+          {{ formatPct(etfSummary.totalReturn) }}<span class="unit"> %</span>
+        </div>
+        <div class="chg">
+          起始 {{ etfModel.startDate }} · 当前净值 {{ etfSummary.latest.toFixed(4) }} · 最大回撤 -12.37%
+        </div>
+      </div>
+      <div class="kpi-card">
+        <div class="label">ETF 模型 · 本周收益</div>
+        <div class="value">
+          {{ formatPct(etfSummary.weeklyReturn) }}<span class="unit"> %</span>
+        </div>
+        <div class="chg">日频净值 · 数据截至 {{ etfModel.asOf }}</div>
+      </div>
+      <div class="kpi-card hero" :class="awSummaryCls">
+        <div class="label">全天候策略 · 累计收益</div>
+        <div class="value">
+          {{ formatPct(allweatherSummary.totalReturn) }}<span class="unit"> %</span>
+        </div>
+        <div class="chg">
+          起始 {{ allweatherModel.startDate }} · 当前净值 {{ allweatherSummary.latest.toFixed(4) }} · 最大回撤 {{ formatPct(allweatherSummary.maxDrawdown) }}%
+        </div>
+      </div>
+      <div class="kpi-card">
+        <div class="label">全天候策略 · 本周收益</div>
+        <div class="value">
+          {{ formatPct(allweatherSummary.weeklyReturn) }}<span class="unit"> %</span>
+        </div>
+        <div class="chg">日频净值 · 数据周期(2026.9.28-9.30)</div>
+      </div>
+    </section>
+
+    <!-- ====== 模型一:ETF 配置模型 ====== -->
+    <section class="section">
+      <div class="section-hd">
+        <span class="dot" style="background:#C49A2A"></span>
+        <h2>ETF 配置模型</h2>
+        <span class="meta">风险平价 · ES 反比加权 · 月末调仓</span>
+      </div>
+
+      <!-- 模型说明卡 -->
+      <div class="card desc-card">
+        <div class="desc-hd">
+          <span class="ic">说</span>
+          <span class="desc-title">模型说明</span>
+        </div>
+        <div class="desc-body">
+          <div class="desc-row">
+            <span class="k">底层标的</span>
+            <span class="v">{{ etfModel.pool }}</span>
+          </div>
+          <div class="desc-row">
+            <span class="k">初始权重</span>
+            <span class="v">{{ etfModel.initialWeight }}</span>
+          </div>
+          <div class="desc-row">
+            <span class="k">主要逻辑</span>
+            <span class="v">{{ etfModel.description }}</span>
+          </div>
+          <!-- <div class="desc-row">
+            <span class="k">数据源</span>
+            <span class="v">{{ etfModel.source }}</span>
+          </div> -->
+        </div>
+      </div>
+
+      <!-- 净值走势 -->
+      <div class="card-grid cols-1" style="margin-top:14px">
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">模型净值走势(日频)</span>
+            </div>
+            <div class="acts">
+              <span class="meta">基准 1.00 · 当前 {{ etfSummary.latest.toFixed(4) }}</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="etfNetOpt" height="320px" />
+          </div>
+        </div>
+      </div>
+
+      <!-- 权重堆叠 + 最新权重 -->
+      <div class="card-grid cols-2-1" style="margin-top:14px">
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">三类 ETF 配置占比变化(月末调仓)</span>
+            </div>
+            <div class="acts">
+              <span class="meta">{{ etfWeights.length }} 期</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="etfWeightOpt" height="340px" />
+          </div>
+        </div>
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">最新一期权重</span>
+            </div>
+            <div class="acts">
+              <span class="meta">{{ etfWeights[etfWeights.length - 1].date }} · {{ etfWeights[etfWeights.length - 1].type }}</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="etfLatestWeightOpt" height="160px" />
+            <div class="note-block">
+              <div class="kv-list">
+                <div class="kv">
+                  <span class="k">本期类型</span>
+                  <span class="v">{{ etfWeights[etfWeights.length - 1].type }}</span>
+                </div>
+                <div class="kv">
+                  <span class="k">煤炭 ETF</span>
+                  <span class="v">{{ (etfWeights[etfWeights.length - 1].coal * 100).toFixed(1) }}%</span>
+                </div>
+                <div class="kv">
+                  <span class="k">银行 ETF</span>
+                  <span class="v">{{ (etfWeights[etfWeights.length - 1].bank * 100).toFixed(1) }}%</span>
+                </div>
+                <div class="kv">
+                  <span class="k">科创 ETF</span>
+                  <span class="v">{{ (etfWeights[etfWeights.length - 1].tech * 100).toFixed(1) }}%</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 调仓权重明细表 -->
+      <div class="card-grid cols-1" style="margin-top:14px">
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">表</span>
+              <span class="title-text">三类 ETF 配置占比变化 · 明细表</span>
+            </div>
+            <div class="acts">
+              <span class="meta">月末调仓 · 季度首月列为首次建仓 / 调仓</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <div class="table-wrap">
+              <table class="weight-tbl">
+                <thead>
+                  <tr>
+                    <th>调仓日期</th>
+                    <th>类型</th>
+                    <th class="num">煤炭 ETF</th>
+                    <th class="num">银行 ETF</th>
+                    <th class="num">科创 ETF</th>
+                    <th class="num">最大权重</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(w, i) in etfWeights" :key="i">
+                    <td><b>{{ w.date }}</b></td>
+                    <td>
+                      <span :class="['type-pill', w.type === '首次建仓' ? 'init' : 'adjust']">
+                        {{ w.type }}
+                      </span>
+                    </td>
+                    <td class="num">{{ (w.coal * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.bank * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.tech * 100).toFixed(1) }}%</td>
+                    <td class="num">
+                      <span :class="['leader-pill', leaderKey(w)]">
+                        {{ leaderName(w) }} {{ (leaderValue(w) * 100).toFixed(1) }}%
+                      </span>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <!-- ====== 模型二:全天候策略 ====== -->
+    <section class="section">
+      <div class="section-hd">
+        <span class="dot" style="background:#1552D0"></span>
+        <h2>全天候策略模型</h2>
+        <span class="meta">Bridgewater All-Weather · 风险平价 · 月末调仓</span>
+      </div>
+
+      <!-- 模型说明卡 -->
+      <div class="card desc-card">
+        <div class="desc-hd">
+          <span class="ic">说</span>
+          <span class="desc-title">模型说明</span>
+        </div>
+        <div class="desc-body">
+          <div class="desc-row">
+            <span class="k">底层标的</span>
+            <span class="v">{{ allweatherModel.pool }}</span>
+          </div>
+          <div class="desc-row">
+            <span class="k">主要逻辑</span>
+            <span class="v">{{ allweatherModel.description }}</span>
+          </div>
+          <!-- <div class="desc-row">
+            <span class="k">数据源</span>
+            <span class="v">{{ allweatherModel.source }}</span>
+          </div> -->
+        </div>
+      </div>
+
+      <div class="card-grid cols-1-1" style="margin-top:14px">
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">模型净值走势(日频)</span>
+            </div>
+            <div class="acts">
+              <span class="meta">基准 1.00 · 当前 {{ allweatherSummary.latest.toFixed(4) }}</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="awNetOpt" height="320px" />
+          </div>
+        </div>
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">历史回撤曲线</span>
+            </div>
+            <div class="acts">
+              <span class="meta">最大回撤 {{ formatPct(allweatherSummary.maxDrawdown) }}%</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="awDrawdownOpt" height="320px" />
+          </div>
+        </div>
+      </div>
+
+      <div class="card-grid cols-2-1" style="margin-top:14px">
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">风险平价 · 六类 ETF 配置占比变化</span>
+            </div>
+            <div class="acts">
+              <span class="meta">{{ allweatherWeights.length }} 期</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="awWeightOpt" height="340px" />
+          </div>
+        </div>
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">图</span>
+              <span class="title-text">最新一期权重</span>
+            </div>
+            <div class="acts">
+              <span class="meta">{{ allweatherWeights[allweatherWeights.length - 1].date }}</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <Chart :option="awLatestWeightOpt" height="200px" />
+            <div class="note-block">
+              <div class="kv-list">
+                <div class="kv"><span class="k">信号日</span><span class="v">{{ allweatherWeights[allweatherWeights.length - 1].signalDate }}</span></div>
+                <div class="kv"><span class="k">沪深300ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].hs300 * 100).toFixed(1) }}%</span></div>
+                <div class="kv"><span class="k">中证500ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].zz500 * 100).toFixed(1) }}%</span></div>
+                <div class="kv"><span class="k">五年国债ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].c5 * 100).toFixed(1) }}%</span></div>
+                <div class="kv"><span class="k">十年国债ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].c10 * 100).toFixed(1) }}%</span></div>
+                <div class="kv"><span class="k">黄金ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].gold * 100).toFixed(1) }}%</span></div>
+                <div class="kv"><span class="k">豆粕ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].meal * 100).toFixed(1) }}%</span></div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 调仓权重明细表 -->
+      <div class="card-grid cols-1" style="margin-top:14px">
+        <div class="card">
+          <div class="card-head">
+            <div class="title">
+              <span class="ic">表</span>
+              <span class="title-text">六类 ETF 配置占比变化 · 明细表</span>
+            </div>
+            <div class="acts">
+              <span class="meta">风险平价 · 月末调仓 · 信号日 T-1</span>
+            </div>
+          </div>
+          <div class="card-body">
+            <div class="table-wrap">
+              <table class="weight-tbl aw">
+                <thead>
+                  <tr>
+                    <th>调仓日期</th>
+                    <th>信号日</th>
+                    <th class="num">沪深300</th>
+                    <th class="num">中证500</th>
+                    <th class="num">5年国债</th>
+                    <th class="num">10年国债</th>
+                    <th class="num">黄金</th>
+                    <th class="num">豆粕</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(w, i) in allweatherWeights" :key="i">
+                    <td><b>{{ w.date }}</b></td>
+                    <td>{{ w.signalDate }}</td>
+                    <td class="num">{{ (w.hs300 * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.zz500 * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.c5 * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.c10 * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.gold * 100).toFixed(1) }}%</td>
+                    <td class="num">{{ (w.meal * 100).toFixed(1) }}%</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <footer class="footer">
+      数据源:4.2.3-ETF模型-数据底稿.xlsx · ETF配置-净值走势图 / ETF配置-每月调仓权重 / 全天候策略-净值走势图 / 全天候策略-每月调仓权重 · 截至 {{ etfModel.asOf }} / {{ allweatherModel.asOf }}
+    </footer>
+  </div>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import Chart from '@/components/Chart.vue'
+import {
+  etfModel,
+  etfNetSeries,
+  etfBenchmarkSeries,
+  etfWeights,
+  etfSummary,
+  allweatherModel,
+  allweatherNetSeries,
+  allweatherWeights,
+  allweatherSummary,
+} from './data'
+import {
+  etfNetLineOption,
+  etfWeightStackOption,
+  etfLatestWeightOption,
+  allweatherNetLineOption,
+  allweatherDrawdownOption,
+  allweatherWeightStackOption,
+  allweatherLatestWeightOption,
+} from './charts'
+
+const loading = ref(false)
+
+function formatPct(v) {
+  if (v == null || !Number.isFinite(v)) return '—'
+  const sign = v > 0 ? '+' : ''
+  return `${sign}${v.toFixed(2)}`
+}
+
+function summaryCls(total) {
+  if (total == null) return 'neutral'
+  if (total >= 30) return 'gold'
+  if (total >= 10) return 'over'
+  if (total >= 0) return 'neutral'
+  return 'under'
+}
+
+const etfSummaryCls = computed(() => summaryCls(etfSummary.totalReturn))
+const awSummaryCls = computed(() => summaryCls(allweatherSummary.totalReturn))
+const etfSummaryText = computed(() => `${formatPct(etfSummary.totalReturn)}%`)
+const awSummaryText = computed(() => `${formatPct(allweatherSummary.totalReturn)}%`)
+
+function leaderName(w) {
+  const max = Math.max(w.coal, w.bank, w.tech)
+  if (max === w.coal) return '煤炭'
+  if (max === w.bank) return '银行'
+  return '科创'
+}
+function leaderValue(w) {
+  return Math.max(w.coal, w.bank, w.tech)
+}
+function leaderKey(w) {
+  const max = Math.max(w.coal, w.bank, w.tech)
+  if (max === w.coal) return 'coal'
+  if (max === w.bank) return 'bank'
+  return 'tech'
+}
+
+const etfNetOpt = computed(() => etfNetLineOption(etfNetSeries, etfBenchmarkSeries))
+const etfWeightOpt = computed(() => etfWeightStackOption(etfWeights))
+const etfLatestWeightOpt = computed(() => etfLatestWeightOption(etfWeights))
+
+const awNetOpt = computed(() => allweatherNetLineOption(allweatherNetSeries))
+const awDrawdownOpt = computed(() => allweatherDrawdownOption(allweatherNetSeries))
+const awWeightOpt = computed(() => allweatherWeightStackOption(allweatherWeights))
+const awLatestWeightOpt = computed(() => allweatherLatestWeightOption(allweatherWeights))
+
+function reload() {
+  loading.value = true
+  setTimeout(() => {
+    loading.value = false
+  }, 600)
+}
+</script>
+
+<style scoped>
+.etf-page {
+  padding: 18px 24px 32px;
+  color: var(--ink, #1A2438);
+}
+
+.page-hd {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  margin-bottom: 14px;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+.page-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--ink, #1A2438);
+  margin-bottom: 3px;
+}
+.page-sub {
+  font-size: 12px;
+  color: var(--mut, #6B7A96);
+}
+.page-actions {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+.btn {
+  height: 30px;
+  padding: 0 14px;
+  border: 1px solid var(--line, #E4E9F2);
+  border-radius: 6px;
+  background: #fff;
+  color: var(--ink, #1A2438);
+  font-size: 12.5px;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  transition: 0.15s;
+}
+.btn:hover {
+  border-color: var(--brand, #1552D0);
+  color: var(--brand, #1552D0);
+}
+.chip {
+  display: inline-block;
+  padding: 4px 11px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 600;
+  white-space: nowrap;
+}
+.chip.under    { background: #FDEAEA; color: #E13B3B; }
+.chip.neutral  { background: #EEF2F7; color: #6B7A96; }
+.chip.over     { background: #E4F5EE; color: #189A65; }
+.chip.gold     { background: #FBF1D9; color: #8C6A14; }
+
+.ic-refresh {
+  display: inline-block;
+  transition: transform 0.6s;
+}
+.ic-refresh.spinning {
+  animation: spin 0.8s linear infinite;
+}
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+.kpi-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 10px;
+  margin-bottom: 16px;
+}
+.kpi-card {
+  background: #fff;
+  border: 1px solid var(--line, #E4E9F2);
+  border-radius: 8px;
+  padding: 13px 14px;
+  position: relative;
+  transition: 0.15s;
+  min-width: 0;
+}
+.kpi-card:hover {
+  box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06);
+  border-color: #D1D8E5;
+}
+.kpi-card.hero {
+  background: linear-gradient(135deg, #0E3A96, #1552D0);
+  color: #fff;
+  border-color: #0E3A96;
+}
+.kpi-card.hero .label { color: rgba(255,255,255,0.82); }
+.kpi-card.hero .value .unit { color: rgba(255,255,255,0.95); }
+.kpi-card.hero .chg { color: rgba(255,255,255,0.88); }
+.kpi-card.hero.gold {
+  background: linear-gradient(135deg, #8C6A14, #C49A2A);
+  border-color: #8C6A14;
+}
+.kpi-card.hero.under {
+  background: linear-gradient(135deg, #B72828, #E13B3B);
+  border-color: #B72828;
+}
+.kpi-card.hero.over {
+  background: linear-gradient(135deg, #13754C, #189A65);
+  border-color: #13754C;
+}
+.kpi-card.hero.neutral {
+  background: linear-gradient(135deg, #4D5A75, #6B7A96);
+  border-color: #4D5A75;
+}
+.kpi-card .label {
+  font-size: 11.5px;
+  color: var(--mut, #6B7A96);
+  margin-bottom: 6px;
+}
+.kpi-card .value {
+  font-size: 22px;
+  font-weight: 600;
+  color: var(--ink, #1A2438);
+  font-variant-numeric: tabular-nums;
+  letter-spacing: -0.5px;
+  line-height: 1.1;
+}
+.kpi-card .value .unit {
+  font-size: 12px;
+  color: var(--mut, #6B7A96);
+  font-weight: 500;
+  margin-left: 2px;
+}
+.kpi-card .chg {
+  font-size: 11px;
+  margin-top: 5px;
+  color: var(--mut, #6B7A96);
+  font-variant-numeric: tabular-nums;
+}
+
+.section {
+  margin-top: 18px;
+}
+.section-hd {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+.section-hd .dot {
+  width: 4px;
+  height: 18px;
+  background: var(--brand, #1552D0);
+  border-radius: 2px;
+}
+.section-hd h2 {
+  font-size: 14.5px;
+  font-weight: 600;
+  color: var(--ink, #1A2438);
+  margin: 0;
+}
+.section-hd .meta {
+  font-size: 11.5px;
+  color: #98A5BD;
+  margin-left: 8px;
+}
+
+.card-grid {
+  display: grid;
+  gap: 14px;
+}
+.card-grid.cols-2-1 { grid-template-columns: 2fr 1fr; }
+.card-grid.cols-1-1 { grid-template-columns: 1fr 1fr; }
+.card-grid.cols-1 { grid-template-columns: 1fr; }
+
+.card {
+  background: #fff;
+  border: 1px solid var(--line, #E4E9F2);
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+.card:hover {
+  box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06);
+}
+.card-head {
+  padding: 11px 16px;
+  border-bottom: 1px solid var(--line, #E4E9F2);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.card-head .title {
+  font-size: 13.5px;
+  font-weight: 600;
+  color: var(--ink, #1A2438);
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex: 1;
+  min-width: 0;
+}
+.card-head .title .ic {
+  display: inline-grid;
+  place-items: center;
+  width: 18px;
+  height: 18px;
+  background: var(--brand-lt, #EAF1FF);
+  color: var(--brand, #1552D0);
+  font-size: 10px;
+  border-radius: 4px;
+  font-weight: 700;
+  flex: none;
+}
+.card-head .title-text {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.card-head .acts {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex: none;
+  font-size: 11px;
+  color: var(--mut, #6B7A96);
+}
+.card-body {
+  padding: 14px 16px;
+  flex: 1;
+  min-height: 0;
+}
+
+/* 模型说明卡 */
+.desc-card {
+  background: linear-gradient(180deg, #FAFBFE 0%, #fff 100%);
+}
+.desc-hd {
+  padding: 11px 16px;
+  border-bottom: 1px solid var(--line, #E4E9F2);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.desc-hd .ic {
+  display: inline-grid;
+  place-items: center;
+  width: 18px;
+  height: 18px;
+  background: var(--brand-lt, #EAF1FF);
+  color: var(--brand, #1552D0);
+  font-size: 10px;
+  border-radius: 4px;
+  font-weight: 700;
+}
+.desc-title {
+  font-size: 13.5px;
+  font-weight: 600;
+  color: var(--ink, #1A2438);
+}
+.desc-body {
+  padding: 14px 16px;
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 10px 20px;
+}
+.desc-row {
+  display: flex;
+  gap: 10px;
+  font-size: 12px;
+  line-height: 1.7;
+}
+.desc-row .k {
+  flex: none;
+  width: 70px;
+  color: var(--mut, #6B7A96);
+}
+.desc-row .v {
+  flex: 1;
+  color: var(--ink, #1A2438);
+  font-weight: 500;
+}
+
+/* 备注 / 最新权重说明 */
+.note-block {
+  margin-top: 12px;
+  padding-top: 10px;
+  border-top: 1px dashed var(--line, #E4E9F2);
+}
+.kv-list {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 6px 12px;
+  font-size: 11.5px;
+}
+.kv {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.kv .k { color: var(--mut, #6B7A96); }
+.kv .v {
+  color: var(--ink, #1A2438);
+  font-weight: 600;
+  font-variant-numeric: tabular-nums;
+}
+
+/* 调仓权重表 */
+.table-wrap {
+  overflow-x: auto;
+}
+.weight-tbl {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 12px;
+}
+.weight-tbl th,
+.weight-tbl td {
+  padding: 9px 10px;
+  border-bottom: 1px solid var(--line, #E4E9F2);
+  text-align: left;
+  vertical-align: middle;
+}
+.weight-tbl th {
+  background: #F5F7FB;
+  color: var(--mut, #6B7A96);
+  font-weight: 600;
+  font-size: 11.5px;
+}
+.weight-tbl td.num,
+.weight-tbl th.num {
+  text-align: right;
+  font-variant-numeric: tabular-nums;
+  white-space: nowrap;
+}
+.weight-tbl tbody tr:hover {
+  background: #FAFBFE;
+}
+
+.type-pill {
+  display: inline-block;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: 600;
+}
+.type-pill.init {
+  background: #FBF1D9;
+  color: #8C6A14;
+}
+.type-pill.adjust {
+  background: #EEF2F7;
+  color: #6B7A96;
+}
+
+.leader-pill {
+  display: inline-block;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: 600;
+}
+.leader-pill.coal {
+  background: rgba(61, 90, 128, 0.12);
+  color: #3D5A80;
+}
+.leader-pill.bank {
+  background: rgba(196, 154, 42, 0.15);
+  color: #8C6A14;
+}
+.leader-pill.tech {
+  background: rgba(91, 160, 242, 0.18);
+  color: #1552D0;
+}
+
+.footer {
+  padding: 18px 24px 12px;
+  color: #98A5BD;
+  font-size: 11.5px;
+  text-align: center;
+  border-top: 1px solid var(--line, #E4E9F2);
+  background: #FAFBFE;
+  margin-top: 14px;
+  border-radius: 8px;
+}
+
+@media (max-width: 1280px) {
+  .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+@media (max-width: 980px) {
+  .kpi-grid { grid-template-columns: 1fr; }
+  .desc-body { grid-template-columns: 1fr; }
+  .card-grid.cols-2-1,
+  .card-grid.cols-1-1,
+  .card-grid.cols-1 { grid-template-columns: 1fr; }
+}
+</style>