index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <template>
  2. <div class="etf-page">
  3. <header class="page-hd">
  4. <div>
  5. <div class="page-title">全天候策略模型</div>
  6. <div class="page-sub">
  7. 全天候策略 · 数据截至 {{ allweatherModel.asOf }}
  8. </div>
  9. </div>
  10. <div class="page-actions">
  11. <button class="btn" @click="reload" :disabled="loading">
  12. <span :class="['ic-refresh', { spinning: loading }]">↻</span> 刷新
  13. </button>
  14. </div>
  15. </header>
  16. <!-- 全天候 KPI 总览 -->
  17. <section class="kpi-grid">
  18. <div class="kpi-card hero" :class="awSummaryCls">
  19. <div class="label">全天候策略 · 累计收益</div>
  20. <div class="value">
  21. {{ formatPct(allweatherSummary.totalReturn) }}<span class="unit"> %</span>
  22. </div>
  23. <div class="chg">
  24. 起始 2025-01-01 · 当前净值 {{ allweatherSummary.latest.toFixed(4) }} · 最大回撤 {{ formatPct(allweatherSummary.maxDrawdown) }}%
  25. </div>
  26. </div>
  27. <div class="kpi-card">
  28. <div class="label">全天候策略 · 本周收益</div>
  29. <div class="value">
  30. {{ formatPct(allweatherSummary.weeklyReturn) }}<span class="unit"> %</span>
  31. </div>
  32. <div class="chg">日频净值 · 数据周期(2026.9.28-9.30)</div>
  33. </div>
  34. </section>
  35. <section class="section">
  36. <div class="section-hd">
  37. <span class="dot" style="background:#1552D0"></span>
  38. <h2>全天候策略模型</h2>
  39. <span class="meta">Bridgewater All-Weather · 风险平价 · 月末调仓</span>
  40. </div>
  41. <!-- 模型说明卡 -->
  42. <div class="card desc-card">
  43. <div class="desc-hd">
  44. <span class="ic">说</span>
  45. <span class="desc-title">模型说明</span>
  46. </div>
  47. <div class="desc-body">
  48. <div class="desc-row">
  49. <span class="k">底层标的</span>
  50. <span class="v">{{ allweatherModel.pool }}</span>
  51. </div>
  52. <div class="desc-row">
  53. <span class="k">主要逻辑</span>
  54. <span class="v">{{ allweatherModel.description }}</span>
  55. </div>
  56. </div>
  57. </div>
  58. <div class="card-grid cols-1-1" style="margin-top:14px">
  59. <div class="card">
  60. <div class="card-head">
  61. <div class="title">
  62. <span class="ic">图</span>
  63. <span class="title-text">模型净值走势(日频)</span>
  64. </div>
  65. <div class="acts">
  66. <span class="meta">基准 1.00 · 当前 {{ allweatherSummary.latest.toFixed(4) }}</span>
  67. </div>
  68. </div>
  69. <div class="card-body">
  70. <Chart :option="awNetOpt" height="320px" />
  71. </div>
  72. </div>
  73. <div class="card">
  74. <div class="card-head">
  75. <div class="title">
  76. <span class="ic">图</span>
  77. <span class="title-text">历史回撤曲线</span>
  78. </div>
  79. <div class="acts">
  80. <span class="meta">最大回撤 {{ formatPct(allweatherSummary.maxDrawdown) }}%</span>
  81. </div>
  82. </div>
  83. <div class="card-body">
  84. <Chart :option="awDrawdownOpt" height="320px" />
  85. </div>
  86. </div>
  87. </div>
  88. <div class="card-grid cols-2-1" style="margin-top:14px">
  89. <div class="card">
  90. <div class="card-head">
  91. <div class="title">
  92. <span class="ic">图</span>
  93. <span class="title-text">风险平价 · 六类 ETF 配置占比变化</span>
  94. </div>
  95. <div class="acts">
  96. <span class="meta">{{ allweatherWeights.length }} 期</span>
  97. </div>
  98. </div>
  99. <div class="card-body">
  100. <Chart :option="awWeightOpt" height="340px" />
  101. </div>
  102. </div>
  103. <div class="card">
  104. <div class="card-head">
  105. <div class="title">
  106. <span class="ic">图</span>
  107. <span class="title-text">最新一期权重</span>
  108. </div>
  109. <div class="acts">
  110. <span class="meta">{{ allweatherWeights[allweatherWeights.length - 1].date }}</span>
  111. </div>
  112. </div>
  113. <div class="card-body">
  114. <Chart :option="awLatestWeightOpt" height="200px" />
  115. <div class="note-block">
  116. <div class="kv-list">
  117. <div class="kv"><span class="k">信号日</span><span class="v">{{ allweatherWeights[allweatherWeights.length - 1].signalDate }}</span></div>
  118. <div class="kv"><span class="k">沪深300ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].hs300 * 100).toFixed(1) }}%</span></div>
  119. <div class="kv"><span class="k">中证500ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].zz500 * 100).toFixed(1) }}%</span></div>
  120. <div class="kv"><span class="k">五年国债ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].c5 * 100).toFixed(1) }}%</span></div>
  121. <div class="kv"><span class="k">十年国债ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].c10 * 100).toFixed(1) }}%</span></div>
  122. <div class="kv"><span class="k">黄金ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].gold * 100).toFixed(1) }}%</span></div>
  123. <div class="kv"><span class="k">豆粕ETF</span><span class="v">{{ (allweatherWeights[allweatherWeights.length - 1].meal * 100).toFixed(1) }}%</span></div>
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. <!-- 调仓权重明细表 -->
  130. <div class="card-grid cols-1" style="margin-top:14px">
  131. <div class="card">
  132. <div class="card-head">
  133. <div class="title">
  134. <span class="ic">表</span>
  135. <span class="title-text">六类 ETF 配置占比变化 · 明细表</span>
  136. </div>
  137. <div class="acts">
  138. <span class="meta">风险平价 · 月末调仓 · 信号日 T-1</span>
  139. </div>
  140. </div>
  141. <div class="card-body">
  142. <div class="table-wrap">
  143. <table class="weight-tbl aw">
  144. <thead>
  145. <tr>
  146. <th>调仓日期</th>
  147. <th>信号日</th>
  148. <th class="num">沪深300</th>
  149. <th class="num">中证500</th>
  150. <th class="num">5年国债</th>
  151. <th class="num">10年国债</th>
  152. <th class="num">黄金</th>
  153. <th class="num">豆粕</th>
  154. </tr>
  155. </thead>
  156. <tbody>
  157. <tr v-for="(w, i) in allweatherWeights" :key="i">
  158. <td><b>{{ w.date }}</b></td>
  159. <td>{{ w.signalDate }}</td>
  160. <td class="num">{{ (w.hs300 * 100).toFixed(1) }}%</td>
  161. <td class="num">{{ (w.zz500 * 100).toFixed(1) }}%</td>
  162. <td class="num">{{ (w.c5 * 100).toFixed(1) }}%</td>
  163. <td class="num">{{ (w.c10 * 100).toFixed(1) }}%</td>
  164. <td class="num">{{ (w.gold * 100).toFixed(1) }}%</td>
  165. <td class="num">{{ (w.meal * 100).toFixed(1) }}%</td>
  166. </tr>
  167. </tbody>
  168. </table>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. </section>
  174. <footer class="footer">
  175. 数据源:4.2.3-ETF模型-数据底稿.xlsx · 全天候策略-净值走势图 / 全天候策略-每月调仓权重 · 截至 {{ allweatherModel.asOf }}
  176. </footer>
  177. </div>
  178. </template>
  179. <script setup>
  180. import { computed, ref } from 'vue'
  181. import Chart from '@/components/Chart.vue'
  182. import {
  183. allweatherModel,
  184. allweatherNetSeries,
  185. allweatherWeights,
  186. allweatherSummary,
  187. } from '../data'
  188. import {
  189. allweatherNetLineOption,
  190. allweatherDrawdownOption,
  191. allweatherWeightStackOption,
  192. allweatherLatestWeightOption,
  193. } from '../charts'
  194. const loading = ref(false)
  195. function formatPct(v) {
  196. if (v == null || !Number.isFinite(v)) return '—'
  197. const sign = v > 0 ? '+' : ''
  198. return `${sign}${v.toFixed(2)}`
  199. }
  200. function summaryCls(total) {
  201. if (total == null) return 'neutral'
  202. if (total >= 30) return 'gold'
  203. if (total >= 10) return 'over'
  204. if (total >= 0) return 'neutral'
  205. return 'under'
  206. }
  207. const awSummaryCls = computed(() => summaryCls(allweatherSummary.totalReturn))
  208. const awNetOpt = computed(() => allweatherNetLineOption(allweatherNetSeries))
  209. const awDrawdownOpt = computed(() => allweatherDrawdownOption(allweatherNetSeries))
  210. const awWeightOpt = computed(() => allweatherWeightStackOption(allweatherWeights))
  211. const awLatestWeightOpt = computed(() => allweatherLatestWeightOption(allweatherWeights))
  212. function reload() {
  213. loading.value = true
  214. setTimeout(() => {
  215. loading.value = false
  216. }, 600)
  217. }
  218. </script>
  219. <style scoped>
  220. .etf-page {
  221. padding: 18px 24px 32px;
  222. color: var(--ink, #1A2438);
  223. }
  224. .page-hd {
  225. display: flex;
  226. align-items: flex-end;
  227. justify-content: space-between;
  228. margin-bottom: 14px;
  229. flex-wrap: wrap;
  230. gap: 12px;
  231. }
  232. .page-title {
  233. font-size: 18px;
  234. font-weight: 600;
  235. color: var(--ink, #1A2438);
  236. margin-bottom: 3px;
  237. }
  238. .page-sub {
  239. font-size: 12px;
  240. color: var(--mut, #6B7A96);
  241. }
  242. .page-actions {
  243. display: flex;
  244. gap: 8px;
  245. align-items: center;
  246. }
  247. .btn {
  248. height: 30px;
  249. padding: 0 14px;
  250. border: 1px solid var(--line, #E4E9F2);
  251. border-radius: 6px;
  252. background: #fff;
  253. color: var(--ink, #1A2438);
  254. font-size: 12.5px;
  255. cursor: pointer;
  256. display: inline-flex;
  257. align-items: center;
  258. gap: 5px;
  259. transition: 0.15s;
  260. }
  261. .btn:hover {
  262. border-color: var(--brand, #1552D0);
  263. color: var(--brand, #1552D0);
  264. }
  265. .ic-refresh {
  266. display: inline-block;
  267. transition: transform 0.6s;
  268. }
  269. .ic-refresh.spinning {
  270. animation: spin 0.8s linear infinite;
  271. }
  272. @keyframes spin {
  273. from { transform: rotate(0deg); }
  274. to { transform: rotate(360deg); }
  275. }
  276. .kpi-grid {
  277. display: grid;
  278. grid-template-columns: repeat(2, minmax(0, 1fr));
  279. gap: 10px;
  280. margin-bottom: 16px;
  281. }
  282. .kpi-card {
  283. background: #fff;
  284. border: 1px solid var(--line, #E4E9F2);
  285. border-radius: 8px;
  286. padding: 13px 14px;
  287. position: relative;
  288. transition: 0.15s;
  289. min-width: 0;
  290. }
  291. .kpi-card:hover {
  292. box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06);
  293. border-color: #D1D8E5;
  294. }
  295. .kpi-card.hero {
  296. background: linear-gradient(135deg, #0E3A96, #1552D0);
  297. color: #fff;
  298. border-color: #0E3A96;
  299. }
  300. .kpi-card.hero .label { color: rgba(255,255,255,0.82); }
  301. .kpi-card.hero .value .unit { color: rgba(255,255,255,0.95); }
  302. .kpi-card.hero .chg { color: rgba(255,255,255,0.88); }
  303. .kpi-card.hero.gold {
  304. background: linear-gradient(135deg, #8C6A14, #C49A2A);
  305. border-color: #8C6A14;
  306. }
  307. .kpi-card.hero.under {
  308. background: linear-gradient(135deg, #B72828, #E13B3B);
  309. border-color: #B72828;
  310. }
  311. .kpi-card.hero.over {
  312. background: linear-gradient(135deg, #13754C, #189A65);
  313. border-color: #13754C;
  314. }
  315. .kpi-card.hero.neutral {
  316. background: linear-gradient(135deg, #4D5A75, #6B7A96);
  317. border-color: #4D5A75;
  318. }
  319. .kpi-card .label {
  320. font-size: 11.5px;
  321. color: var(--mut, #6B7A96);
  322. margin-bottom: 6px;
  323. }
  324. .kpi-card .value {
  325. font-size: 22px;
  326. font-weight: 600;
  327. color: var(--ink, #1A2438);
  328. font-variant-numeric: tabular-nums;
  329. letter-spacing: -0.5px;
  330. line-height: 1.1;
  331. }
  332. .kpi-card .value .unit {
  333. font-size: 12px;
  334. color: var(--mut, #6B7A96);
  335. font-weight: 500;
  336. margin-left: 2px;
  337. }
  338. .kpi-card .chg {
  339. font-size: 11px;
  340. margin-top: 5px;
  341. color: var(--mut, #6B7A96);
  342. font-variant-numeric: tabular-nums;
  343. }
  344. .section {
  345. margin-top: 18px;
  346. }
  347. .section-hd {
  348. display: flex;
  349. align-items: center;
  350. gap: 8px;
  351. margin-bottom: 12px;
  352. }
  353. .section-hd .dot {
  354. width: 4px;
  355. height: 18px;
  356. background: var(--brand, #1552D0);
  357. border-radius: 2px;
  358. }
  359. .section-hd h2 {
  360. font-size: 14.5px;
  361. font-weight: 600;
  362. color: var(--ink, #1A2438);
  363. margin: 0;
  364. }
  365. .section-hd .meta {
  366. font-size: 11.5px;
  367. color: #98A5BD;
  368. margin-left: 8px;
  369. }
  370. .card-grid {
  371. display: grid;
  372. gap: 14px;
  373. }
  374. .card-grid.cols-2-1 { grid-template-columns: 2fr 1fr; }
  375. .card-grid.cols-1-1 { grid-template-columns: 1fr 1fr; }
  376. .card-grid.cols-1 { grid-template-columns: 1fr; }
  377. .card {
  378. background: #fff;
  379. border: 1px solid var(--line, #E4E9F2);
  380. border-radius: 8px;
  381. overflow: hidden;
  382. display: flex;
  383. flex-direction: column;
  384. }
  385. .card:hover {
  386. box-shadow: 0 1px 3px rgba(16, 32, 64, 0.06);
  387. }
  388. .card-head {
  389. padding: 11px 16px;
  390. border-bottom: 1px solid var(--line, #E4E9F2);
  391. display: flex;
  392. align-items: center;
  393. gap: 8px;
  394. }
  395. .card-head .title {
  396. font-size: 13.5px;
  397. font-weight: 600;
  398. color: var(--ink, #1A2438);
  399. display: flex;
  400. align-items: center;
  401. gap: 6px;
  402. flex: 1;
  403. min-width: 0;
  404. }
  405. .card-head .title .ic {
  406. display: inline-grid;
  407. place-items: center;
  408. width: 18px;
  409. height: 18px;
  410. background: var(--brand-lt, #EAF1FF);
  411. color: var(--brand, #1552D0);
  412. font-size: 10px;
  413. border-radius: 4px;
  414. font-weight: 700;
  415. flex: none;
  416. }
  417. .card-head .title-text {
  418. white-space: nowrap;
  419. overflow: hidden;
  420. text-overflow: ellipsis;
  421. }
  422. .card-head .acts {
  423. display: flex;
  424. align-items: center;
  425. gap: 6px;
  426. flex: none;
  427. font-size: 11px;
  428. color: var(--mut, #6B7A96);
  429. }
  430. .card-body {
  431. padding: 14px 16px;
  432. flex: 1;
  433. min-height: 0;
  434. }
  435. /* 模型说明卡 */
  436. .desc-card {
  437. background: linear-gradient(180deg, #FAFBFE 0%, #fff 100%);
  438. }
  439. .desc-hd {
  440. padding: 11px 16px;
  441. border-bottom: 1px solid var(--line, #E4E9F2);
  442. display: flex;
  443. align-items: center;
  444. gap: 8px;
  445. }
  446. .desc-hd .ic {
  447. display: inline-grid;
  448. place-items: center;
  449. width: 18px;
  450. height: 18px;
  451. background: var(--brand-lt, #EAF1FF);
  452. color: var(--brand, #1552D0);
  453. font-size: 10px;
  454. border-radius: 4px;
  455. font-weight: 700;
  456. }
  457. .desc-title {
  458. font-size: 13.5px;
  459. font-weight: 600;
  460. color: var(--ink, #1A2438);
  461. }
  462. .desc-body {
  463. padding: 14px 16px;
  464. display: grid;
  465. grid-template-columns: repeat(2, minmax(0, 1fr));
  466. gap: 10px 20px;
  467. }
  468. .desc-row {
  469. display: flex;
  470. gap: 10px;
  471. font-size: 12px;
  472. line-height: 1.7;
  473. }
  474. .desc-row .k {
  475. flex: none;
  476. width: 70px;
  477. color: var(--mut, #6B7A96);
  478. }
  479. .desc-row .v {
  480. flex: 1;
  481. color: var(--ink, #1A2438);
  482. font-weight: 500;
  483. }
  484. /* 备注 / 最新权重说明 */
  485. .note-block {
  486. margin-top: 12px;
  487. padding-top: 10px;
  488. border-top: 1px dashed var(--line, #E4E9F2);
  489. }
  490. .kv-list {
  491. display: grid;
  492. grid-template-columns: 1fr 1fr;
  493. gap: 6px 12px;
  494. font-size: 11.5px;
  495. }
  496. .kv {
  497. display: flex;
  498. justify-content: space-between;
  499. align-items: center;
  500. }
  501. .kv .k { color: var(--mut, #6B7A96); }
  502. .kv .v {
  503. color: var(--ink, #1A2438);
  504. font-weight: 600;
  505. font-variant-numeric: tabular-nums;
  506. }
  507. /* 调仓权重表 */
  508. .table-wrap {
  509. overflow-x: auto;
  510. }
  511. .weight-tbl {
  512. width: 100%;
  513. border-collapse: collapse;
  514. font-size: 12px;
  515. }
  516. .weight-tbl th,
  517. .weight-tbl td {
  518. padding: 9px 10px;
  519. border-bottom: 1px solid var(--line, #E4E9F2);
  520. text-align: left;
  521. vertical-align: middle;
  522. }
  523. .weight-tbl th {
  524. background: #F5F7FB;
  525. color: var(--mut, #6B7A96);
  526. font-weight: 600;
  527. font-size: 11.5px;
  528. }
  529. .weight-tbl td.num,
  530. .weight-tbl th.num {
  531. text-align: right;
  532. font-variant-numeric: tabular-nums;
  533. white-space: nowrap;
  534. }
  535. .weight-tbl tbody tr:hover {
  536. background: #FAFBFE;
  537. }
  538. .footer {
  539. padding: 18px 24px 12px;
  540. color: #98A5BD;
  541. font-size: 11.5px;
  542. text-align: center;
  543. border-top: 1px solid var(--line, #E4E9F2);
  544. background: #FAFBFE;
  545. margin-top: 14px;
  546. border-radius: 8px;
  547. }
  548. @media (max-width: 980px) {
  549. .kpi-grid { grid-template-columns: 1fr; }
  550. .desc-body { grid-template-columns: 1fr; }
  551. .card-grid.cols-2-1,
  552. .card-grid.cols-1-1,
  553. .card-grid.cols-1 { grid-template-columns: 1fr; }
  554. }
  555. </style>