feat: 增加宁德时代图表
This commit is contained in:
+21
-3
@@ -53,11 +53,29 @@ button {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.reload-button:hover { opacity: .88; transform: rotate(-45deg); }
|
||||
/* 图标继承按钮的 color(currentColor),尺寸只在这里定义,换图标不用动 JS。
|
||||
20px 是配 36px 圆钮的光学最优值:新图标的弧线比原 ↻ 字形细,19px 会显轻。 */
|
||||
.reload-button .reload-icon {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* 悬停转一圈:刷新语义的标准反馈,比原先 ↻ 字形的 -45° 倾斜更贴合新图标 */
|
||||
.reload-button:hover { opacity: .88; }
|
||||
.reload-button:hover .reload-icon { animation: reload-spin .6s ease-in-out; }
|
||||
|
||||
@keyframes reload-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reload-button:hover .reload-icon { animation: none; }
|
||||
}
|
||||
.export-button:hover { color: var(--accent); border-color: var(--accent); }
|
||||
|
||||
/* ── 合并队列(购物车)────────────────────────────── */
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
company,gp_per_wh,margin_data
|
||||
宁德时代,0.122,20.63
|
||||
比亚迪*,0.097,21.00
|
||||
LG新能源†,-0.010,-0.70
|
||||
中创新航*,0.077,16.71
|
||||
国轩高科*,0.063,14.02
|
||||
亿纬锂能,0.078,16.19
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,calb,guoxuan,eve
|
||||
2021,0.784,0.720,0.660,0.683,
|
||||
2022,0.978,0.800,0.872,0.840,
|
||||
2023,0.889,0.700,0.618,0.720,
|
||||
2024,0.664,0.550,0.496,0.626,
|
||||
2025,0.585,0.470,0.459,0.473,0.516
|
||||
2026H1,0.591,0.460,0.459,0.452,0.483
|
||||
|
@@ -0,0 +1,7 @@
|
||||
company,gp_2023,gp_2026h1
|
||||
宁德时代,0.198,0.122
|
||||
比亚迪*,0.154,0.097
|
||||
LG新能源†,0.119,-0.010
|
||||
中创新航*,0.080,0.077
|
||||
国轩高科*,0.112,0.063
|
||||
亿纬锂能,,0.078
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,peer_avg,gap_multiple
|
||||
2021,0.172,0.080,2.15
|
||||
2022,0.168,0.118,1.42
|
||||
2023,0.198,0.096,2.06
|
||||
2024,0.159,0.087,1.83
|
||||
2025,0.139,0.075,1.85
|
||||
2026H1,0.122,0.073,1.68
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan,eve
|
||||
2021,0.172,0.144,0.067,0.037,0.123,
|
||||
2022,0.168,0.144,0.091,0.090,0.147,
|
||||
2023,0.198,0.154,0.119,0.080,0.112,
|
||||
2024,0.159,0.121,0.032,0.079,0.095,
|
||||
2025,0.139,0.103,0.068,0.077,0.068,0.080
|
||||
2026H1,0.122,0.097,-0.010,0.077,0.063,0.078
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan,global_total
|
||||
2021,99.5,26.4,59.4,8.0,6.7,301.5
|
||||
2022,191.6,70.4,70.4,20.0,14.1,517.9
|
||||
2023,259.7,111.4,95.8,33.4,17.1,705.5
|
||||
2024,339.3,153.7,96.3,39.4,28.5,894.4
|
||||
2025,464.7,194.8,108.8,62.8,53.5,1187.0
|
||||
2026H1,242.7,87.7,52.6,31.2,28.0,608.5
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan
|
||||
2021,33.0,8.7,19.7,2.6,2.2
|
||||
2022,37.0,13.6,13.6,3.9,2.7
|
||||
2023,36.8,15.8,13.6,4.7,2.4
|
||||
2024,37.9,17.2,10.8,4.4,3.2
|
||||
2025,39.2,16.4,9.2,5.3,4.5
|
||||
2026H1,39.9,14.4,8.6,5.1,4.6
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan,eve
|
||||
2021,22.00,20.00,4.31,5.60,18.00,
|
||||
2022,17.17,18.00,4.74,10.32,17.50,
|
||||
2023,22.27,22.00,6.41,13.00,15.50,
|
||||
2024,23.94,22.00,2.25,15.89,15.14,
|
||||
2025,23.84,22.00,5.69,16.69,14.39,15.50
|
||||
2026H1,20.63,21.00,-0.70,16.71,14.02,16.19
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan
|
||||
2021,95.1,,73.4,95.2,41.0
|
||||
2022,83.3,71.0,73.6,93.8,46.0
|
||||
2023,70.5,72.0,69.3,75.6,48.0
|
||||
2024,76.3,70.0,57.8,78.7,46.0
|
||||
2025,96.9,82.0,47.6,,71.0
|
||||
2026H1,94.9,,52.8,,
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,sales_gwh,revenue_yi,asp,cost_per_wh,gp_per_wh,gross_margin
|
||||
2021,116.7,914.9,0.784,0.612,0.172,22.00
|
||||
2022,242.0,2365.9,0.978,0.810,0.168,17.17
|
||||
2023,321.0,2852.5,0.889,0.691,0.198,22.27
|
||||
2024,381.0,2530.4,0.664,0.505,0.159,23.94
|
||||
2025,541.0,3165.1,0.585,0.446,0.139,23.84
|
||||
2026H1,325,1921.2,0.591,0.469,0.122,20.63
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,china,korea,japan,others,global_total
|
||||
2021,48.2,30.2,12.0,9.6,301.5
|
||||
2022,60.4,23.7,7.3,8.6,517.9
|
||||
2023,63.5,23.2,6.1,7.2,705.5
|
||||
2024,67.1,18.5,3.9,10.5,894.4
|
||||
2025,70.4,15.3,3.7,10.6,1187.0
|
||||
2026H1,72.4,11.7,3.7,12.2,608.5
|
||||
|
@@ -0,0 +1,11 @@
|
||||
rank,company,region,gwh,share,yoy,global_total
|
||||
1,宁德时代,中国,242.7,39.9,25.3,608.5
|
||||
2,比亚迪,中国,87.7,14.4,1.6,608.5
|
||||
3,LG新能源,韩国,52.6,8.6,8.4,608.5
|
||||
4,中创新航,中国,31.2,5.1,39.5,608.5
|
||||
5,国轩高科,中国,28.0,4.6,43.3,608.5
|
||||
6,松下,日本,22.7,3.7,10.2,608.5
|
||||
7,亿纬锂能,中国,20.9,3.4,51.7,608.5
|
||||
8,SK On,韩国,19.0,3.1,-6.7,608.5
|
||||
9,蜂巢能源,中国,15.7,2.6,40.9,608.5
|
||||
10,欣旺达,中国,14.5,2.4,13.7,608.5
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,panasonic,sk_on,samsung_sdi,calb,guoxuan,eve,sunwoda,svolt,farasis,global_total
|
||||
2021,99.5,26.4,59.4,36.3,17.3,14.5,8.0,6.7,,2.6,,2.4,301.5
|
||||
2022,191.6,70.4,70.4,38.0,27.8,24.3,20.0,14.1,,9.2,,7.4,517.9
|
||||
2023,259.7,111.4,95.8,42.8,34.7,33.1,33.4,17.1,16.2,10.5,,,705.5
|
||||
2024,339.3,153.7,96.3,35.1,39.0,29.6,39.4,28.5,20.3,18.8,,,894.4
|
||||
2025,464.7,194.8,108.8,44.2,44.5,28.9,62.8,53.5,31.3,,28.5,,1187.0
|
||||
2026H1,242.7,87.7,52.6,22.7,19.0,,31.2,28.0,20.9,14.5,15.7,,608.5
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,panasonic,sk_on,samsung_sdi,calb,guoxuan,eve,sunwoda,svolt,farasis,cr3,cr5,cr10,china_top10_count,china_share
|
||||
2021,33.0,8.7,19.7,12.0,5.7,4.8,2.6,2.2,,0.9,,0.8,64.7,79.1,90.4,6,48.2
|
||||
2022,37.0,13.6,13.6,7.3,5.4,4.7,3.9,2.7,,1.8,,1.4,64.2,76.9,91.4,6,60.4
|
||||
2023,36.8,15.8,13.6,6.1,4.9,4.7,4.7,2.4,2.3,1.5,,,66.2,77.2,92.8,6,63.5
|
||||
2024,37.9,17.2,10.8,3.9,4.4,3.3,4.4,3.2,2.3,2.1,,,65.9,74.7,89.5,6,67.1
|
||||
2025,39.2,16.4,9.2,3.7,3.7,2.4,5.3,4.5,2.6,,2.4,,64.8,74.6,89.4,6,70.4
|
||||
2026H1,39.9,14.4,8.6,3.7,3.1,,5.1,4.6,3.4,2.4,2.6,,62.9,72.6,87.8,7,72.4
|
||||
|
@@ -0,0 +1,36 @@
|
||||
# 由 scripts/build_sweet_spot_catl.py 生成,请勿手工编辑。
|
||||
# 口径:总股本 46.27 亿股 · 净现金 2,263.8 亿元(每股 48.93 元)· 2026E 归母 900/964/1,000 亿元 · 分红率 50% · 现价 316.21 元(2026-09-15)
|
||||
# PE 与股息率保留两位小数(见脚本 docstring 的精度例外说明)
|
||||
price,market_cap,pe_bear,pe_base,pe_bull,pe_ex_cash,dividend_yield,net_cash_pct
|
||||
220.00,10179.4,11.31,10.56,10.18,8.21,4.74,22.2
|
||||
240.00,11104.8,12.34,11.52,11.10,9.17,4.34,20.4
|
||||
260.00,12030.2,13.37,12.48,12.03,10.13,4.01,18.8
|
||||
265.00,12261.6,13.62,12.72,12.26,10.37,3.93,18.5
|
||||
280.00,12955.6,14.40,13.44,12.96,11.09,3.72,17.5
|
||||
290.00,13418.3,14.91,13.92,13.42,11.57,3.59,16.9
|
||||
300.00,13881.0,15.42,14.40,13.88,12.05,3.47,16.3
|
||||
303.00,14019.8,15.58,14.54,14.02,12.20,3.44,16.1
|
||||
316.21,14631.0,16.26,15.18,14.63,12.83,3.29,15.5
|
||||
320.00,14806.4,16.45,15.36,14.81,13.01,3.26,15.3
|
||||
340.00,15731.8,17.48,16.32,15.73,13.97,3.06,14.4
|
||||
360.00,16657.2,18.51,17.28,16.66,14.93,2.89,13.6
|
||||
378.00,17490.1,19.43,18.14,17.49,15.79,2.76,12.9
|
||||
380.00,17582.6,19.54,18.24,17.58,15.89,2.74,12.9
|
||||
400.00,18508.0,20.56,19.20,18.51,16.85,2.60,12.2
|
||||
420.00,19433.4,21.59,20.16,19.43,17.81,2.48,11.6
|
||||
440.00,20358.8,22.62,21.12,20.36,18.77,2.37,11.1
|
||||
460.00,21284.2,23.65,22.08,21.28,19.73,2.26,10.6
|
||||
480.00,22209.6,24.68,23.04,22.21,20.69,2.17,10.2
|
||||
500.00,23135.0,25.71,24.00,23.14,21.65,2.08,9.8
|
||||
520.00,24060.4,26.73,24.96,24.06,22.61,2.00,9.4
|
||||
540.00,24985.8,27.76,25.92,24.99,23.57,1.93,9.1
|
||||
560.00,25911.2,28.79,26.88,25.91,24.53,1.86,8.7
|
||||
580.00,26836.6,29.82,27.84,26.84,25.49,1.80,8.4
|
||||
600.00,27762.0,30.85,28.80,27.76,26.45,1.74,8.2
|
||||
605.00,27993.4,31.10,29.04,27.99,26.69,1.72,8.1
|
||||
617.00,28548.6,31.72,29.61,28.55,27.27,1.69,7.9
|
||||
620.00,28687.4,31.87,29.76,28.69,27.41,1.68,7.9
|
||||
640.00,29612.8,32.90,30.72,29.61,28.37,1.63,7.6
|
||||
660.00,30538.2,33.93,31.68,30.54,29.33,1.58,7.4
|
||||
680.00,31463.6,34.96,32.64,31.46,30.29,1.53,7.2
|
||||
700.00,32389.0,35.99,33.60,32.39,31.25,1.49,7.0
|
||||
|
@@ -0,0 +1,32 @@
|
||||
# 由 scripts/build_sweet_spot_catl.py 生成,请勿手工编辑。
|
||||
# 口径:总股本 46.27 亿股 · 净现金 2,263.8 亿元(每股 48.93 元)· 2026E 归母 900/964/1,000 亿元 · 分红率 50% · 现价 316.21 元(2026-09-15)
|
||||
# PE 与股息率保留两位小数(见脚本 docstring 的精度例外说明)
|
||||
price,market_cap,pe_base,dividend_yield,cagr_price,cagr_total
|
||||
220.00,10179.4,10.56,4.74,40.1,46.7
|
||||
240.00,11104.8,11.52,4.34,36.1,42.0
|
||||
260.00,12030.2,12.48,4.01,32.5,37.8
|
||||
265.00,12261.6,12.72,3.93,31.7,36.9
|
||||
280.00,12955.6,13.44,3.72,29.3,34.1
|
||||
290.00,13418.3,13.92,3.59,27.8,32.4
|
||||
300.00,13881.0,14.40,3.47,26.3,30.7
|
||||
303.00,14019.8,14.54,3.44,25.9,30.3
|
||||
316.21,14631.0,15.18,3.29,24.1,28.2
|
||||
320.00,14806.4,15.36,3.26,23.7,27.7
|
||||
340.00,15731.8,16.32,3.06,21.2,24.9
|
||||
360.00,16657.2,17.28,2.89,18.9,22.3
|
||||
378.00,17490.1,18.14,2.76,17.0,20.2
|
||||
380.00,17582.6,18.24,2.74,16.8,20.0
|
||||
400.00,18508.0,19.20,2.60,14.8,17.8
|
||||
420.00,19433.4,20.16,2.48,12.9,15.7
|
||||
440.00,20358.8,21.12,2.37,11.2,13.8
|
||||
450.00,20821.5,21.60,2.31,10.4,12.9
|
||||
460.00,21284.2,22.08,2.26,9.6,12.0
|
||||
480.00,22209.6,23.04,2.17,8.0,10.4
|
||||
500.00,23135.0,24.00,2.08,6.6,8.8
|
||||
520.00,24060.4,24.96,2.00,5.2,7.3
|
||||
540.00,24985.8,25.92,1.93,3.9,5.9
|
||||
560.00,25911.2,26.88,1.86,2.6,4.5
|
||||
580.00,26836.6,27.84,1.80,1.4,3.2
|
||||
600.00,27762.0,28.80,1.74,0.3,2.0
|
||||
605.00,27993.4,29.04,1.72,0.0,1.7
|
||||
620.00,28687.4,29.76,1.68,-0.8,0.9
|
||||
|
+1040
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,15 @@ import {
|
||||
moutaiBaseWineVolumeChart, moutaiFlyingPriceChart, moutaiPriceAdjustmentChart,
|
||||
moutaiTonPriceChart, moutaiDividendChart, moutaiDistributorsChart,
|
||||
} from './moutai.js';
|
||||
import {
|
||||
peerInstallChart, peerUtilizationChart, peerShareChart,
|
||||
peerGpPerWhChart, peerAspChart, peerMarginChart, peer2026h1Chart,
|
||||
peerGpGapChart, peerGpChangeChart, catlUnitEconomicsChart, catlVolumeGpChart,
|
||||
globalCrChart, globalTop10Chart, globalRegionShareChart,
|
||||
} from './catl.js';
|
||||
import {
|
||||
sweetCatlPricePeChart, sweetCatlNetCashChart, sweetCatlAnyPriceChart,
|
||||
} from './sweet_spot.js';
|
||||
|
||||
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
||||
export const chartRegistry = {
|
||||
@@ -114,6 +123,23 @@ export const chartRegistry = {
|
||||
'moutai-ton-price': moutaiTonPriceChart,
|
||||
'moutai-dividend': moutaiDividendChart,
|
||||
'moutai-distributors': moutaiDistributorsChart,
|
||||
'catl-unit-economics': catlUnitEconomicsChart,
|
||||
'catl-volume-gp': catlVolumeGpChart,
|
||||
'peer-install': peerInstallChart,
|
||||
'peer-utilization': peerUtilizationChart,
|
||||
'peer-share': peerShareChart,
|
||||
'peer-gp-per-wh': peerGpPerWhChart,
|
||||
'peer-asp': peerAspChart,
|
||||
'peer-margin': peerMarginChart,
|
||||
'peer-2026h1': peer2026h1Chart,
|
||||
'peer-gp-gap': peerGpGapChart,
|
||||
'peer-gp-change': peerGpChangeChart,
|
||||
'global-cr': globalCrChart,
|
||||
'global-top10': globalTop10Chart,
|
||||
'global-region-share': globalRegionShareChart,
|
||||
'sweet-catl-price-pe': sweetCatlPricePeChart,
|
||||
'sweet-catl-net-cash': sweetCatlNetCashChart,
|
||||
'sweet-catl-any-price': sweetCatlAnyPriceChart,
|
||||
};
|
||||
|
||||
export function getChartBuilder(key) {
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
import {
|
||||
PALETTE, FONT, FINANCIAL_COLORS, axisCommon, titleStyle, standardLegend,
|
||||
lineSeries, footnote, watermarks, LAYOUT, tooltipOf,
|
||||
} from '../theme.js';
|
||||
|
||||
/**
|
||||
* 甜蜜击球区 · 宁德时代(300750)—— 股价 ↔ 2026E 估值 的换算图表
|
||||
*
|
||||
* 上游底稿:资料库节点「宁德时代_股价PE对照表」
|
||||
* https://www.workbuddy.cn/space/d/EsAWMaLKt4ECKPDvESKKvT(2026-09-15 版)
|
||||
* 数据由 scripts/build_sweet_spot_catl.py 投影成 data/sweetspot/*.csv。
|
||||
*
|
||||
* 【口径】所有 PE 都是**静态预期 PE**:市值 ÷ 2026E 归母净利(悲观 900 / 中性 964 /
|
||||
* 乐观 1,000 亿元,65 家机构一致预期)。扣现金 PE =(市值 − 净现金 2,263.8 亿元)÷
|
||||
* 2026E 净利,即"只给主营电池业务定价"。股息率按 2023–2025 年实际分红率 50% 测算。
|
||||
*
|
||||
* 【甜蜜区框架】8x 白送 · 10x 甜点 · 12x 上沿 是巴菲特锚;资料库原表用的是五档
|
||||
* 更宽的区域(按中性情景 PE 划分),因为它的分子是"含净现金的市值"。两套档位不要
|
||||
* 混读:本文所有区域带一律用原表的五档,定义集中在 PE_ZONES,改档位只改这一处。
|
||||
*
|
||||
* 【图 1】股价 → PE 对照(PE 轴上的横向区域带)
|
||||
* 【图 2】含现金 PE vs 扣现金 PE(沙色带 = 净现金的厚度)
|
||||
* 【图 3】任意股价试算(股价轴上的纵向区域带 + 三年预期年化收益)
|
||||
*
|
||||
* 精度例外:PE 保留两位小数。项目默认一位,但"现价落在买入区还是合理区"取决于
|
||||
* 15.18x 与 15.5x 这条边界,一位小数会把 15.18 读成 15.2、与 15.5 分不清。
|
||||
*
|
||||
* 硬编码说明:区域边界的**股价**不是写死的,而是从 CSV 的 pe_base 反解(peToPrice),
|
||||
* 所以改股本或利润预期后纵向区域带会自动跟着走。本文只有两个手工常量:现价 NOW 与
|
||||
* 净现金 NET_CASH(仅用于文案),都与 build_sweet_spot_catl.py 同源。
|
||||
*/
|
||||
|
||||
/** 现价(2026-09-15 收盘)。与生成脚本同源,重跑脚本时要同步。 */
|
||||
const NOW = 316.21;
|
||||
|
||||
/** 净现金(亿元)。仅用于图 2 的文案;沙色带的几何量取自 CSV 的 pe_base − pe_ex_cash。 */
|
||||
const NET_CASH = 2263.8;
|
||||
|
||||
/** 2028E 目标价(1,400 亿 × 20x ÷ 46.27 亿股),图 3 的年化收益基准。 */
|
||||
const TARGET_2028 = 605;
|
||||
|
||||
/**
|
||||
* 五档估值区域(按中性情景 964 亿 PE 的上沿划分)。
|
||||
* 原表把 15.5–25x 细分为「合理偏低」与「合理」两档、但共用同一个配色,
|
||||
* 这里并为一条色带,避免在 PE 轴上叠出两块无差别的底色。
|
||||
*/
|
||||
const PE_ZONES = [
|
||||
{ name: '击球区', upTo: 13, color: PALETTE.green, note: '<13x' },
|
||||
{ name: '买入区', upTo: 15.5, color: PALETTE.blue, note: '13–15.5x' },
|
||||
{ name: '合理', upTo: 25, color: PALETTE.orange, note: '15.5–25x' },
|
||||
{ name: '偏贵', upTo: 30, color: PALETTE.red, note: '25–30x' },
|
||||
{ name: '卖出区', upTo: Infinity, color: PALETTE.purple, note: '>30x' },
|
||||
];
|
||||
|
||||
const PE_MIN = 8;
|
||||
const PE_MAX = 38;
|
||||
const PRICE_MIN = 200;
|
||||
const PRICE_MAX = 720;
|
||||
|
||||
const SOURCE_NOTE = '数据来源:宁德时代 2026 中报(本地财报库)· 65 家机构 2026E 一致预期';
|
||||
|
||||
/** 十六进制转 rgba —— 区域带要低透明度叠在网格上,不能直接铺饱和色 */
|
||||
function tint(hex, alpha) {
|
||||
const value = parseInt(hex.slice(1), 16);
|
||||
return `rgba(${(value >> 16) & 255},${(value >> 8) & 255},${value & 255},${alpha})`;
|
||||
}
|
||||
|
||||
/** 取某价位的某个指标;缺失返回 NaN,让标签渲染成 NaN 而不是抛错 */
|
||||
function metricAt(rows, price, key) {
|
||||
const row = rows.find((item) => item.price === price);
|
||||
return row ? row[key] : Number.NaN;
|
||||
}
|
||||
|
||||
/** 数值轴(PE / 收益率共用),与 catl.js 的 valueAxis 同一套样式 */
|
||||
const valueAxis = (name, extra = {}) => ({
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 56,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
...extra,
|
||||
});
|
||||
|
||||
/**
|
||||
* 股价轴:数值轴。
|
||||
* 不画竖向网格(规范只保留浅色横线)—— 图 3 的纵向区域带本身就是参考线,
|
||||
* 图 1、图 2 靠「现价」竖线定位。
|
||||
*/
|
||||
const priceAxis = (extra = {}) => ({
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name: '股价(元)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 34,
|
||||
min: PRICE_MIN,
|
||||
max: PRICE_MAX,
|
||||
axisLine: { show: true, lineStyle: { color: PALETTE.axisLine } },
|
||||
splitLine: { show: false },
|
||||
...extra,
|
||||
});
|
||||
|
||||
const GRID = { left: 78, right: 78, top: LAYOUT.gridTop, bottom: 80 };
|
||||
|
||||
function baseOption({
|
||||
title, subtitle, xAxis, yAxis, series, legend, width, height,
|
||||
tooltipFormatter, extraNote, sourceNote, grid, extraGraphics = [],
|
||||
}) {
|
||||
const notes = [sourceNote ?? SOURCE_NOTE];
|
||||
if (extraNote) notes.push(extraNote);
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: { ...titleStyle, text: title, subtext: subtitle },
|
||||
legend: standardLegend(legend, { itemGap: 22 }),
|
||||
// 三行脚注(来源 + 两行口径)会顶到 height-66,而横轴刻度底边在 height-52。
|
||||
// 本项目这几张图的横轴带轴名(「股价(元)」),它比刻度再低约 32px;实测
|
||||
// grid.bottom=118 时轴名与脚注只剩 7px 净空(同站三行脚注的图通常有 23px),
|
||||
// 所以统一提到 134 —— 改脚注行数时这里要跟着重算。
|
||||
grid: { ...GRID, ...grid },
|
||||
tooltip: tooltipOf(series, tooltipFormatter),
|
||||
xAxis,
|
||||
yAxis,
|
||||
series,
|
||||
graphic: [...watermarks(width, height), ...extraGraphics, footnote(notes.join('\n') + ' | @Joey知行记')],
|
||||
};
|
||||
}
|
||||
|
||||
/** 反解 PE 阈值对应的股价:在 pe_base 单调递增的序列上线性插值 */
|
||||
function peToPrice(rows, pe) {
|
||||
for (let i = 1; i < rows.length; i += 1) {
|
||||
const prev = rows[i - 1];
|
||||
const cur = rows[i];
|
||||
if ((prev.pe_base - pe) * (cur.pe_base - pe) <= 0) {
|
||||
const ratio = (pe - prev.pe_base) / (cur.pe_base - prev.pe_base);
|
||||
return prev.price + ratio * (cur.price - prev.price);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* PE 轴上的横向区域带(图 1、图 2 共用)。
|
||||
* 必须挂在**第一个系列**上:markArea 在系列自己的分组里、先于该系列图元绘制,
|
||||
* 而系列分组按声明顺序绘制 —— 挂在后面的系列上会盖住前一条线。
|
||||
*/
|
||||
function peBandMarkAreas() {
|
||||
let low = PE_MIN;
|
||||
return PE_ZONES.map((zone) => {
|
||||
const band = [{
|
||||
yAxis: low,
|
||||
itemStyle: { color: tint(zone.color, 0.07) },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'insideTopLeft',
|
||||
distance: 10,
|
||||
formatter: `${zone.name} ${zone.note}`,
|
||||
color: zone.color,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
fontFamily: FONT,
|
||||
},
|
||||
}, { yAxis: Math.min(zone.upTo, PE_MAX) }];
|
||||
low = zone.upTo;
|
||||
return band;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 股价轴上的纵向区域带(图 3 用)。
|
||||
* 标签贴底:顶部留给折线标签与「现价」竖线的标注,贴底才不会撞。
|
||||
*/
|
||||
function priceBandMarkAreas(rows) {
|
||||
const edges = [PRICE_MIN, ...PE_ZONES
|
||||
.map((zone) => peToPrice(rows, zone.upTo))
|
||||
.filter((price) => price !== null), PRICE_MAX];
|
||||
return PE_ZONES.map((zone, index) => [{
|
||||
xAxis: edges[index],
|
||||
itemStyle: { color: tint(zone.color, 0.08) },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'insideBottom',
|
||||
distance: 12,
|
||||
formatter: zone.name,
|
||||
color: zone.color,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
fontFamily: FONT,
|
||||
},
|
||||
}, { xAxis: edges[index + 1] }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 股价 → 指标折线。
|
||||
* 只在 labeledPrices 上的点标数值:30+ 个数据点全标会糊成一片,
|
||||
* 而这几个点正好是决策价位。
|
||||
*/
|
||||
function priceLine({
|
||||
name, rows, key, color, width = 2.6, labeledPrices = [],
|
||||
unit = '', decimals = 2, position = 'right', distance = 8,
|
||||
}) {
|
||||
const line = lineSeries({ name, data: [], color, width });
|
||||
return {
|
||||
...line,
|
||||
label: { show: false },
|
||||
data: rows.map((row) => ({
|
||||
value: [row.price, row[key]],
|
||||
label: labeledPrices.includes(row.price)
|
||||
? {
|
||||
...line.label,
|
||||
show: true,
|
||||
position,
|
||||
distance,
|
||||
formatter: `${row[key].toFixed(decimals)}${unit}`,
|
||||
}
|
||||
: { show: false },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 现价竖线:三张图共用。
|
||||
* label 一律关掉 —— markLine 的标签会**跟着竖线旋转 90°**,长串中文变成一列竖排字,
|
||||
* 极难读。文字改用 nowGraphics() 以 graphic 单独排,保持水平。
|
||||
*/
|
||||
function nowMarkLine() {
|
||||
return {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { type: 'dashed', width: 1.6, color: PALETTE.brown },
|
||||
label: { show: false },
|
||||
data: [{ xAxis: NOW }],
|
||||
};
|
||||
}
|
||||
|
||||
/** 现价竖线上方的水平标注,位置由 grid 反推(图表尺寸变化时会重算) */
|
||||
function nowGraphics(width, text) {
|
||||
const x = GRID.left
|
||||
+ ((NOW - PRICE_MIN) / (PRICE_MAX - PRICE_MIN)) * (width - GRID.left - GRID.right);
|
||||
return [{
|
||||
type: 'text',
|
||||
left: Math.round(x + 8),
|
||||
top: LAYOUT.gridTop + 4,
|
||||
silent: true,
|
||||
style: { text, fill: PALETTE.brown, fontSize: 12, fontWeight: 600, fontFamily: FONT },
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* 图 1:股价 → 2026E PE 对照 —— 资料库原表第 1 节的图形版
|
||||
*
|
||||
* 一条直线就是一个情景:PE 与股价严格成正比,所以三档利润在图上表现为三条**平行**的
|
||||
* 窄带(±6% 的利润预期落到 PE 上不足 1.7x)。这张图能回答"现价落在哪个区域",
|
||||
* 但回答不了"贵还是便宜"—— 那取决于利润能不能兑现,属于分母问题,不在本图射程内。
|
||||
*/
|
||||
export function sweetCatlPricePeChart(rows, { width, height }) {
|
||||
const nowPe = metricAt(rows, NOW, 'pe_base');
|
||||
const hero = priceLine({
|
||||
name: '中性 964 亿',
|
||||
rows,
|
||||
key: 'pe_base',
|
||||
color: FINANCIAL_COLORS.netProfit,
|
||||
width: 3.5,
|
||||
labeledPrices: [265, NOW, 480, 605],
|
||||
unit: 'x',
|
||||
position: 'right',
|
||||
});
|
||||
hero.markLine = nowMarkLine();
|
||||
|
||||
const bear = {
|
||||
...lineSeries({ name: '悲观 900 亿', data: [], color: FINANCIAL_COLORS.operatingCashFlow, width: 2.4 }),
|
||||
label: { show: false },
|
||||
data: rows.map((row) => [row.price, row.pe_bear]),
|
||||
};
|
||||
// markArea 挂在第一个系列上(见 peBandMarkAreas 注释)
|
||||
bear.markArea = { silent: true, data: peBandMarkAreas() };
|
||||
|
||||
const bull = {
|
||||
...lineSeries({ name: '乐观 1,000 亿', data: [], color: FINANCIAL_COLORS.investingCashFlow, width: 2.4 }),
|
||||
label: { show: false },
|
||||
data: rows.map((row) => [row.price, row.pe_bull]),
|
||||
};
|
||||
|
||||
return baseOption({
|
||||
title: '股价 → 2026E PE 对照',
|
||||
subtitle: `单位:倍 · 三档利润情景 · 区域底带按中性 964 亿划分 · 现价 ${NOW} 元`,
|
||||
xAxis: priceAxis(),
|
||||
yAxis: valueAxis('PE(倍)', { min: PE_MIN, max: PE_MAX, interval: 5 }),
|
||||
series: [bear, hero, bull],
|
||||
legend: ['悲观 900 亿', '中性 964 亿', '乐观 1,000 亿'],
|
||||
width,
|
||||
height,
|
||||
grid: { bottom: 134 },
|
||||
extraGraphics: nowGraphics(width, `现价 ${NOW} 元 = 中性 ${nowPe.toFixed(2)}x`),
|
||||
tooltipFormatter: (value) => `${Number(Array.isArray(value) ? value[1] : value).toFixed(2)}x`,
|
||||
extraNote: 'PE = 市值 ÷ 2026E 归母净利,与股价严格成正比;三档情景只差 ±6% 利润,落到 PE 上不足 1.7x\n'
|
||||
+ '原表把 15.5–25x 细分为「合理偏低」与「合理」,此处并为一条色带;扣现金 PE 见图 2',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 图 2:净现金让 PE 便宜了多少 —— 资料库原表第 2 节右图的图形版
|
||||
*
|
||||
* 含现金 PE 与扣现金 PE 是两条**严格平行**的线,间距恒为 净现金 ÷ 2026E 净利
|
||||
* (约 2.35x)—— 间距与股价无关,但折价占市值的比例随股价下降而放大
|
||||
* (220 元 22.2% → 700 元 7.0%)。所以这张图要看的不是两条线的高低,
|
||||
* 而是中间那层沙色:它是一块不产生经营利润、但实实在在归股东的资产。
|
||||
*
|
||||
* 实现说明:ECharts 没有"两个系列之间填充",只能用 stack —— 下段是扣现金 PE、
|
||||
* 上段是差额,差额段线宽 0、面积填沙色,上沿即含现金 PE。stack 的叠放顺序不能改
|
||||
* (改了基准就错),于是用 z 控制绘制层级,防止沙色覆盖掉扣现金那条实线。
|
||||
*/
|
||||
export function sweetCatlNetCashChart(rows, { width, height }) {
|
||||
const gap = rows[0].pe_base - rows[0].pe_ex_cash;
|
||||
const nowEx = metricAt(rows, NOW, 'pe_ex_cash');
|
||||
|
||||
const cashLine = {
|
||||
...lineSeries({ name: '扣现金 PE', data: [], color: PALETTE.brown, width: 3.5 }),
|
||||
label: { show: false },
|
||||
stack: 'pe',
|
||||
z: 3,
|
||||
areaStyle: { opacity: 0 },
|
||||
data: rows.map((row) => [row.price, row.pe_ex_cash]),
|
||||
};
|
||||
cashLine.markLine = nowMarkLine();
|
||||
cashLine.markArea = { silent: true, data: peBandMarkAreas() };
|
||||
|
||||
const band = {
|
||||
...lineSeries({ name: '净现金差额', data: [], color: PALETTE.sand, width: 0 }),
|
||||
label: { show: false },
|
||||
stack: 'pe',
|
||||
z: 2,
|
||||
lineStyle: { color: PALETTE.sand, width: 0, opacity: 0 },
|
||||
itemStyle: { color: PALETTE.sand },
|
||||
showSymbol: false,
|
||||
// opacity 必须显式写:areaStyle 的默认 opacity 是 0.7,只给 rgba 底色会被再乘一次
|
||||
areaStyle: { color: PALETTE.sand, opacity: 0.7 },
|
||||
data: rows.map((row) => [row.price, row.pe_base - row.pe_ex_cash]),
|
||||
};
|
||||
|
||||
const gross = {
|
||||
...lineSeries({ name: '含现金 PE', data: [], color: FINANCIAL_COLORS.investingCashFlow, width: 3 }),
|
||||
label: { show: false },
|
||||
z: 4,
|
||||
data: rows.map((row) => [row.price, row.pe_base]),
|
||||
};
|
||||
|
||||
return baseOption({
|
||||
title: '净现金让 PE 便宜了多少',
|
||||
subtitle: `单位:倍 · 中性 964 亿口径 · 沙色带即净现金 ${NET_CASH.toLocaleString('zh-CN')} 亿元对应的 PE`,
|
||||
xAxis: priceAxis(),
|
||||
yAxis: valueAxis('PE(倍)', { min: PE_MIN, max: PE_MAX, interval: 5 }),
|
||||
series: [cashLine, band, gross],
|
||||
// 「净现金差额」不进图例(线宽 0,图例色块会是空的),靠副标题与脚注交代
|
||||
legend: ['含现金 PE', '扣现金 PE'],
|
||||
width,
|
||||
height,
|
||||
grid: { bottom: 134 },
|
||||
extraGraphics: nowGraphics(width, `现价 ${NOW} 元 = 扣现金 ${nowEx.toFixed(2)}x`),
|
||||
tooltipFormatter: (value) => `${Number(Array.isArray(value) ? value[1] : value).toFixed(2)}x`,
|
||||
extraNote: '含现金 PE = 市值 ÷ 2026E 净利;扣现金 PE =(市值 − 净现金)÷ 2026E 净利,即只给主营电池业务定价\n'
|
||||
+ `两条线严格平行:差额恒为 ${gap.toFixed(2)}x,但占市值比例从 220 元的 22.2% 收窄到 700 元的 7.0% —— 股价越低,这层折价越厚`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 图 3:任意股价试算 —— 资料库原表第 5 节计算器的图形版
|
||||
*
|
||||
* 计算器要输入一个价格才出结果,图表反过来:220–700 元全档都画出来,任意价位
|
||||
* 沿竖线直接读。两条线是同一个目标价(605 元 = 2028E 1,400 亿 × 20x)下的三年
|
||||
* 年化收益,差额即 50% 分红率的贡献。纵轴穿过 0 的位置就是"三年白干"—— 正好
|
||||
* 是 605 元这条卖点线,所以零线本身就是一条结论。
|
||||
*
|
||||
* 纵向底带把估值区域直接画在股价轴上:横带看 PE、竖带看股价,是同一套档位。
|
||||
* 击球区上沿 270.9 元、买入区上沿 322.9 元、合理区上沿 520.9 元(由 pe_base 反解)。
|
||||
*/
|
||||
export function sweetCatlAnyPriceChart(rows, { width, height }) {
|
||||
const hero = priceLine({
|
||||
name: '含分红年化',
|
||||
rows,
|
||||
key: 'cagr_total',
|
||||
color: FINANCIAL_COLORS.netProfit,
|
||||
width: 3.5,
|
||||
labeledPrices: [265, NOW, 450, TARGET_2028],
|
||||
unit: '%',
|
||||
decimals: 1,
|
||||
position: 'top',
|
||||
distance: 10,
|
||||
});
|
||||
hero.markLine = nowMarkLine();
|
||||
// markArea 挂在第一个系列上,否则会盖住同图上先声明的折线
|
||||
hero.markArea = { silent: true, data: priceBandMarkAreas(rows) };
|
||||
|
||||
const priceOnly = priceLine({
|
||||
name: '仅价差年化',
|
||||
rows,
|
||||
key: 'cagr_price',
|
||||
color: FINANCIAL_COLORS.investingCashFlow,
|
||||
width: 2.6,
|
||||
labeledPrices: [265, NOW, 450],
|
||||
unit: '%',
|
||||
decimals: 1,
|
||||
position: 'bottom',
|
||||
distance: 10,
|
||||
});
|
||||
|
||||
return baseOption({
|
||||
title: '任意股价试算:三年预期年化收益',
|
||||
subtitle: '单位:% · 目标价 605 元(2028E 1,400 亿 × 20x)· 纵带为估值区域',
|
||||
xAxis: priceAxis(),
|
||||
yAxis: valueAxis('年化收益(%)', { min: -10, max: 50, interval: 10 }),
|
||||
series: [hero, priceOnly],
|
||||
legend: ['含分红年化', '仅价差年化'],
|
||||
width,
|
||||
height,
|
||||
grid: { bottom: 134 },
|
||||
extraGraphics: nowGraphics(width, `现价 ${NOW} 元 = 含分红年化 ${metricAt(rows, NOW, 'cagr_total').toFixed(1)}%`),
|
||||
tooltipFormatter: (value) => `${Number(Array.isArray(value) ? value[1] : value).toFixed(1)}%`,
|
||||
extraNote: `年化收益 =(${TARGET_2028} ÷ 股价)^(1/3) − 1;含分红口径再乘 (1 + 股息率),股息按当前价每年派现、不复投\n`
|
||||
+ '纵带边界由 pe_base 反解:击球区上沿 270.9 元 | 买入区上沿 322.9 元 | 合理区上沿 520.9 元',
|
||||
});
|
||||
}
|
||||
+56
@@ -361,6 +361,62 @@ export function stackedSharePoint(value, { pxPerUnit, labelColor, dy = 0, minIns
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* tooltip 彩色圆点标记。
|
||||
*
|
||||
* 为什么不直接用 ECharts 的默认 marker:折线系列的 visualDrawType 恒为 'fill'
|
||||
* (Series.js 原型默认值,只有轨迹图 / 箱线图改成 'stroke'),而空心圆折线的
|
||||
* itemStyle.color 必须是 #fff —— tooltip 取到的 marker 就是白色,在白底提示框上
|
||||
* 等于隐形。这里按系列语义色自绘标记,不依赖 ECharts 内部取色。
|
||||
*/
|
||||
export function tooltipMarker(color) {
|
||||
return `<span style="display:inline-block;width:10px;height:10px;border-radius:50%;`
|
||||
+ `background:${color};margin-right:6px;vertical-align:-1px"></span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准 tooltip:每一项前面带该系列语义色的圆点。
|
||||
*
|
||||
* 不能用 ECharts 默认 marker:折线系列 visualDrawType 为 'fill',
|
||||
* 而空心圆折线的 itemStyle.color 是 #fff,默认 marker 会被取成白色而在白底上隐形。
|
||||
* 取色规则:
|
||||
* - 柱:优先用该数据点自身的颜色(支持逐项着色,如按公司或国别上色)
|
||||
* - 折线:用 lineStyle.color(空心圆的 itemStyle.color 是白色,不能作语义色)
|
||||
*
|
||||
* @param {Array} series 已构造好的系列数组
|
||||
* @param {(value: number) => string} formatValue 默认数值格式化
|
||||
* @param {Object<string, (value: number) => string>} [formatBySeries] 按系列名覆盖
|
||||
* —— 柱线组合图两条序列单位不同(如 GWh 与 %)时必需
|
||||
*/
|
||||
export function tooltipOf(series, formatValue, formatBySeries) {
|
||||
const seriesColors = {};
|
||||
series.forEach((item) => {
|
||||
const color = item.lineStyle?.color ?? item.itemStyle?.color;
|
||||
if (item.name && color) seriesColors[item.name] = color;
|
||||
});
|
||||
|
||||
return {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT, fontSize: 13 },
|
||||
formatter: (params) => {
|
||||
const list = Array.isArray(params) ? params : [params];
|
||||
if (list.length === 0) return '';
|
||||
const head = `<span style="color:${PALETTE.ink};font-weight:600">`
|
||||
+ `${list[0].axisValueLabel ?? list[0].name ?? ''}</span>`;
|
||||
const body = list.map((item) => {
|
||||
const fallback = seriesColors[item.seriesName] ?? PALETTE.grey;
|
||||
const color = item.seriesType === 'bar' ? (item.color || fallback) : fallback;
|
||||
const fmt = formatBySeries?.[item.seriesName] ?? formatValue;
|
||||
const raw = item.value;
|
||||
const value = raw === null || raw === undefined || raw === '' ? '—' : fmt(raw);
|
||||
return `${tooltipMarker(color)}${item.seriesName}:${value}`;
|
||||
});
|
||||
return [head, ...body].join('<br/>');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 底部注释 */
|
||||
export function footnote(text) {
|
||||
return {
|
||||
|
||||
@@ -88,6 +88,42 @@
|
||||
{ "id": "price-shareholder", "name": "价格与股东回报", "chartRefs": ["moutai-flying-price", "moutai-price-adjustment", "moutai-dividend"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "catl",
|
||||
"name": "宁德时代",
|
||||
"chartCatalog": {
|
||||
"catl-unit-economics": { "key": "catl-unit-economics", "title": "动力电池单 Wh 经济性", "csv": "data/catl/catl_unit_economics.csv", "height": 700, "filename": "宁德时代动力电池单Wh经济性" },
|
||||
"catl-volume-gp": { "key": "catl-volume-gp", "title": "动力电池销量与单 Wh 毛利", "csv": "data/catl/catl_unit_economics.csv", "height": 700, "filename": "宁德时代销量与单Wh毛利" },
|
||||
"peer-install": { "key": "peer-install", "title": "Top5 动力电池装车量走势", "csv": "data/catl/catl_peer_install.csv", "height": 700, "filename": "Top5动力电池装车量走势" },
|
||||
"peer-utilization": { "key": "peer-utilization", "title": "Top5 动力电池产能利用率", "csv": "data/catl/catl_peer_utilization.csv", "height": 700, "filename": "Top5动力电池产能利用率" },
|
||||
"peer-share": { "key": "peer-share", "title": "Top5 动力电池全球市占率走势", "csv": "data/catl/catl_peer_install_share.csv", "height": 700, "filename": "Top5动力电池全球市占率走势" },
|
||||
"peer-gp-per-wh": { "key": "peer-gp-per-wh", "title": "Top6 动力电池单 Wh 毛利走势", "csv": "data/catl/catl_peer_gp_per_wh.csv", "height": 720, "filename": "Top6动力电池单Wh毛利走势" },
|
||||
"peer-asp": { "key": "peer-asp", "title": "Top6 动力电池 ASP 对比", "csv": "data/catl/catl_peer_asp.csv", "height": 700, "filename": "Top6动力电池ASP对比" },
|
||||
"peer-margin": { "key": "peer-margin", "title": "Top6 毛利率与营业利润率", "csv": "data/catl/catl_peer_margin.csv", "height": 700, "filename": "Top6毛利率与营业利润率" },
|
||||
"peer-2026h1": { "key": "peer-2026h1", "title": "2026H1 单 Wh 毛利与盈利能力", "csv": "data/catl/catl_peer_2026h1.csv", "height": 700, "filename": "2026H1单Wh毛利与盈利能力" },
|
||||
"peer-gp-gap": { "key": "peer-gp-gap", "title": "宁德 vs 二线厂均值:单 Wh 毛利差距", "csv": "data/catl/catl_peer_gp_gap.csv", "height": 700, "filename": "宁德vs二线厂单Wh毛利差距" },
|
||||
"peer-gp-change": { "key": "peer-gp-change", "title": "单 Wh 毛利:2026H1 vs 2023", "csv": "data/catl/catl_peer_gp_change.csv", "height": 700, "filename": "单Wh毛利2026H1对比2023" },
|
||||
"global-cr": { "key": "global-cr", "title": "全球装车量集中度 CR3 / CR5 / CR10", "csv": "data/catl/global_top10_share.csv", "height": 700, "filename": "全球动力电池装车量集中度" },
|
||||
"global-top10": { "key": "global-top10", "title": "2026H1 全球装车量前十与同比增速", "csv": "data/catl/global_top10_2026h1.csv", "height": 720, "filename": "2026H1全球装车量前十与同比增速" },
|
||||
"global-region-share": { "key": "global-region-share", "title": "中日韩厂商全球份额结构", "csv": "data/catl/global_region_share.csv", "height": 700, "filename": "中日韩厂商全球份额结构" }
|
||||
},
|
||||
"tabs": [
|
||||
{ "id": "unit-economics", "name": "单位经济", "chartRefs": ["catl-unit-economics", "catl-volume-gp"] },
|
||||
{ "id": "peer", "name": "同业对比", "chartRefs": ["peer-install", "peer-utilization", "peer-share", "global-cr", "global-top10", "global-region-share", "peer-gp-per-wh", "peer-asp", "peer-margin", "peer-2026h1", "peer-gp-gap", "peer-gp-change"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sweet-spot",
|
||||
"name": "甜蜜击球区",
|
||||
"chartCatalog": {
|
||||
"sweet-catl-price-pe": { "key": "sweet-catl-price-pe", "title": "股价 → 2026E PE 对照", "csv": "data/sweetspot/catl_price_pe.csv", "height": 740, "filename": "宁德时代股价PE对照" },
|
||||
"sweet-catl-net-cash": { "key": "sweet-catl-net-cash", "title": "净现金让 PE 便宜了多少", "csv": "data/sweetspot/catl_price_pe.csv", "height": 740, "filename": "宁德时代净现金与PE" },
|
||||
"sweet-catl-any-price": { "key": "sweet-catl-any-price", "title": "任意股价试算:三年预期年化收益", "csv": "data/sweetspot/catl_price_returns.csv", "height": 740, "filename": "宁德时代任意股价试算" }
|
||||
},
|
||||
"tabs": [
|
||||
{ "id": "catl", "name": "宁德时代", "chartRefs": ["sweet-catl-price-pe", "sweet-catl-net-cash", "sweet-catl-any-price"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "brk",
|
||||
"name": "巴菲特致股东信",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""甜蜜击球区 · 宁德时代(300750):股价 → 估值指标 投影表生成器
|
||||
|
||||
数据层定位:本脚本是 data/sweetspot/*.csv 的唯一生成入口(CSV 是运行时投影,
|
||||
禁止手工编辑)。上游底稿是资料库节点「宁德时代_股价PE对照表」
|
||||
(https://www.workbuddy.cn/space/d/EsAWMaLKt4ECKPDvESKKvT,2026-09-15 版)。
|
||||
|
||||
计算基准(改这几个常量就能整表重算):
|
||||
总股本 46.27 亿股
|
||||
净现金 2,263.8 亿元 (每股 48.93 元)
|
||||
2026E 归母 悲观 900 / 中性 964 / 乐观 1,000 亿元(65 家机构一致预期)
|
||||
分红率 50%(2023–2025 年实际值)
|
||||
现价 316.21 元(2026-09-15 收盘)
|
||||
2028E 目标 1,400 亿 × 20x → 605 元(老唐法三年后合理估值,用于试算年化收益)
|
||||
无风险利率 1.68%(10 年期国债,仅用于股息率倍数的脚注说明)
|
||||
|
||||
精度例外:PE 与股息率保留两位小数。CLAUDE.md 默认一位,但 PE 一位会把
|
||||
15.18x 与 15.2x 揉成同一个读数,而「现价到底落在哪个区间」正是这张表要回答的
|
||||
问题(买入区上沿 15.5x 与合理区下沿贴得极近),故按行业惯例取两位。
|
||||
|
||||
用法:
|
||||
python3 scripts/build_sweet_spot_catl.py
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
SHARES = 46.27 # 亿股
|
||||
NET_CASH = 2263.8 # 亿元
|
||||
NOW = 316.21 # 元
|
||||
PAYOUT = 0.50
|
||||
NI = {"bear": 900.0, "base": 964.0, "bull": 1000.0}
|
||||
TARGET_2028 = 605.0 # 元(2028E 1,400 亿 × 20x ÷ 46.27 亿股)
|
||||
|
||||
OUT_DIR = Path(__file__).resolve().parent.parent / "data" / "sweetspot"
|
||||
|
||||
|
||||
def price_grid(top, step=20, keys=()):
|
||||
"""等距网格 + 关键价位去重后升序,保证关键价位本身就是数据点(可被打标签)。"""
|
||||
prices = {float(p) for p in range(220, top + 1, step)}
|
||||
prices.update(float(k) for k in keys)
|
||||
return sorted(prices)
|
||||
|
||||
|
||||
def pct(value):
|
||||
return value * 100
|
||||
|
||||
|
||||
def main():
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
header = (
|
||||
"# 由 scripts/build_sweet_spot_catl.py 生成,请勿手工编辑。\n"
|
||||
"# 口径:总股本 46.27 亿股 · 净现金 2,263.8 亿元(每股 48.93 元)· "
|
||||
"2026E 归母 900/964/1,000 亿元 · 分红率 50% · 现价 316.21 元(2026-09-15)\n"
|
||||
"# PE 与股息率保留两位小数(见脚本 docstring 的精度例外说明)\n"
|
||||
)
|
||||
|
||||
# ---------- 1) 股价 → PE 对照(图 1、图 2 共用) ----------
|
||||
rows = price_grid(700, 20, (265, 290, 303, NOW, 378, 605, 617))
|
||||
lines = [
|
||||
"price,market_cap,pe_bear,pe_base,pe_bull,pe_ex_cash,dividend_yield,net_cash_pct"
|
||||
]
|
||||
for px in rows:
|
||||
cap = px * SHARES
|
||||
lines.append(
|
||||
",".join([
|
||||
f"{px:.2f}",
|
||||
f"{cap:.1f}",
|
||||
f"{cap / NI['bear']:.2f}",
|
||||
f"{cap / NI['base']:.2f}",
|
||||
f"{cap / NI['bull']:.2f}",
|
||||
f"{(cap - NET_CASH) / NI['base']:.2f}",
|
||||
f"{pct(NI['base'] * PAYOUT / cap):.2f}",
|
||||
f"{pct(NET_CASH / cap):.1f}",
|
||||
])
|
||||
)
|
||||
path = OUT_DIR / "catl_price_pe.csv"
|
||||
path.write_text(header + "\n".join(lines) + "\n", encoding="utf-8")
|
||||
print(f"{path.relative_to(OUT_DIR.parent.parent)} {len(rows)} 行")
|
||||
|
||||
# ---------- 2) 股价 → 试算(图 3) ----------
|
||||
rows = price_grid(620, 20, (265, 290, 303, NOW, 378, 450, TARGET_2028))
|
||||
lines = [
|
||||
"price,market_cap,pe_base,dividend_yield,cagr_price,cagr_total"
|
||||
]
|
||||
for px in rows:
|
||||
cap = px * SHARES
|
||||
dy = NI["base"] * PAYOUT / cap
|
||||
cagr = (TARGET_2028 / px) ** (1 / 3) - 1
|
||||
# 总收益按 (1 + 价差年化) × (1 + 股息率) − 1:股息以买入价口径年年复投,
|
||||
# 与资料库试算器的「未含分红」口径并列展示,两者差额即分红的贡献。
|
||||
lines.append(
|
||||
",".join([
|
||||
f"{px:.2f}",
|
||||
f"{cap:.1f}",
|
||||
f"{cap / NI['base']:.2f}",
|
||||
f"{pct(dy):.2f}",
|
||||
f"{pct(cagr):.1f}",
|
||||
f"{pct((1 + cagr) * (1 + dy) - 1):.1f}",
|
||||
])
|
||||
)
|
||||
path = OUT_DIR / "catl_price_returns.csv"
|
||||
path.write_text(header + "\n".join(lines) + "\n", encoding="utf-8")
|
||||
print(f"{path.relative_to(OUT_DIR.parent.parent)} {len(rows)} 行")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user