Files
finance-talk/建材指数监控/index_template.html
T

463 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>建材价格指数 · 趋势监控</title>
<style>
:root {
--bg: #fff; --fg: #1e293b; --sub: #64748b;
--card-bg: #f8fafc; --card-border: #e2e8f0;
--th-bg: #f1f5f9; --th-fg: #334155; --th-border: #cbd5e1;
--td-border: #e2e8f0; --td-even: #f8fafc;
--green: #16a34a; --red: #dc2626; --amber: #d97706;
--up-bg: #dcfce7; --down-bg: #fee2e2;
--tag-up-bg: #dcfce7; --tag-up-fg: #166534;
--tag-down-bg: #fee2e2; --tag-down-fg: #991b1b;
--tag-neutral-bg: #fef3c7; --tag-neutral-fg: #92400e;
}
[data-theme="dark"] {
--bg: #0f172a; --fg: #e2e8f0; --sub: #94a3b8;
--card-bg: #1a2332; --card-border: #334155;
--th-bg: #1e293b; --th-fg: #cbd5e1; --th-border: #334155;
--td-border: #1e293b; --td-even: #1a2332;
--up-bg: #1c3b1c; --down-bg: #3b1c1c;
--tag-up-bg: #1c3b1c; --tag-up-fg: #86efac;
--tag-down-bg: #3b1c1c; --tag-down-fg: #fca5a5;
--tag-neutral-bg: #3b2e1c; --tag-neutral-fg: #fcd34d;
}
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; padding: 24px; max-width: 960px; margin: 0 auto; color: var(--fg); background: var(--bg); }
h1 { font-size: 1.4rem; margin-bottom: 4px; }
.sub { color: var(--sub); font-size: 0.85rem; margin-bottom: 24px; }
.toggle-btn { position: fixed; top: 16px; right: 24px; background: var(--th-bg); border: 1px solid var(--th-border); color: var(--th-fg); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; z-index: 10; }
.toggle-btn:hover { opacity: 0.8; }
/* ---- summary cards ---- */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
.card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 8px; padding: 16px; }
.card .label { font-size: 0.78rem; color: var(--sub); margin-bottom: 4px; }
.card .value { font-size: 1.6rem; font-weight: 700; }
.card .delta { font-size: 0.82rem; margin-top: 4px; }
.delta.up { color: var(--green); }
.delta.down { color: var(--red); }
/* ---- signal strip ---- */
.signal-strip { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; align-items: center; }
.signal-tag { padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 600; }
.signal-tag.up { background: var(--tag-up-bg); color: var(--tag-up-fg); }
.signal-tag.down { background: var(--tag-down-bg); color: var(--tag-down-fg); }
.signal-tag.neutral { background: var(--tag-neutral-bg); color: var(--tag-neutral-fg); }
.signal-verdict { font-size: 1.05rem; font-weight: 700; margin-left: 8px; }
/* ---- MA table ---- */
table { border-collapse: collapse; width: 100%; font-size: 0.88rem; margin: 14px 0 20px; }
th { background: var(--th-bg); color: var(--th-fg); font-weight: 600; padding: 8px 12px; border: 1px solid var(--th-border); text-align: center; white-space: nowrap; }
td { padding: 7px 12px; border: 1px solid var(--td-border); text-align: center; }
tr:nth-child(even) td { background: var(--td-even); }
td:first-child { font-weight: 500; }
.arrow-up { color: var(--green); }
.arrow-down { color: var(--red); }
.arrow-flat { color: var(--sub); }
h2 { font-size: 1.1rem; margin: 28px 0 10px; padding-bottom: 4px; border-bottom: 1px solid var(--card-border); }
.row-up td { background: var(--up-bg) !important; }
.row-down td { background: var(--down-bg) !important; }
[data-theme="dark"] .row-up td { background: var(--up-bg) !important; }
[data-theme="dark"] .row-down td { background: var(--down-bg) !important; }
.footnote { color: var(--sub); font-size: 0.78rem; margin-top: 28px; }
/* ---- history section ---- */
.history-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; font-size: 0.82rem; }
.history-stat { display: flex; align-items: baseline; gap: 4px; }
.history-stat .label { color: var(--sub); }
.history-stat .value { font-weight: 700; }
#historyChart { width: 100%; max-width: 900px; height: auto; border: 1px solid var(--card-border); border-radius: 6px; margin: 8px 0 20px; }
.cycle-table td:first-child { text-align: left; }
.cycle-table .bar-cell { width: 120px; }
.cycle-bar { display: inline-block; height: 8px; border-radius: 4px; vertical-align: middle; margin-right: 6px; }
.cycle-bar.up { background: var(--green); }
.cycle-bar.down { background: var(--red); }
.cycle-label { font-size: 0.78rem; color: var(--sub); }
</style>
</head>
<body>
<button class="toggle-btn" id="themeToggle">🌙 暗夜</button>
<h1>建材价格指数 · 趋势监控</h1>
<p class="sub" id="updateTime"></p>
<!-- ── 概览卡片 ── -->
<div class="cards">
<div class="card">
<div class="label">最新值</div>
<div class="value" id="curValue"></div>
<div class="delta" id="curDelta"></div>
</div>
<div class="card">
<div class="label">近3月涨跌</div>
<div class="value" id="chg3m"></div>
</div>
<div class="card">
<div class="label">近6月涨跌</div>
<div class="value" id="chg6m"></div>
</div>
<div class="card">
<div class="label">近1年涨跌</div>
<div class="value" id="chg1y"></div>
</div>
</div>
<!-- ── 趋势信号 ── -->
<div class="signal-strip" id="signalStrip"></div>
<!-- ── 历史全景 ── -->
<h2>历史全景 <span style="font-weight:400;font-size:0.82rem;color:var(--sub)">2011.12 2026.07</span></h2>
<div class="history-stats" id="historyStats"></div>
<canvas id="historyChart"></canvas>
<!-- ── 周期对比 ── -->
<h2>周期对比</h2>
<table class="cycle-table">
<tr><th>周期</th><th>阶段</th><th>区间</th><th>谷 → 峰</th><th>涨幅</th></tr>
<tbody id="cycleTable"></tbody>
</table>
<p id="cycleNote" class="cycle-label"></p>
<!-- ── 均线系统 ── -->
<h2>均线系统</h2>
<table>
<tr><th>均线</th><th>数值</th><th>方向</th><th>价格位置</th></tr>
<tbody id="maTable"></tbody>
</table>
<!-- ── 近30日明细 ── -->
<h2>近30日明细</h2>
<table>
<tr><th>日期</th><th>指数</th><th>日涨跌%</th><th>MA5</th><th>MA20</th><th>MA60</th></tr>
<tbody id="recentTable"></tbody>
</table>
<p class="footnote">数据来源:akshare macro_china_construction_index · 建材综合指数</p>
<script>
// ── 暗夜模式 ──
var themeBtn = document.getElementById('themeToggle');
function setTheme(dark) {
if (dark) { document.documentElement.setAttribute('data-theme', 'dark'); themeBtn.textContent = '☀️ 日间'; }
else { document.documentElement.removeAttribute('data-theme'); themeBtn.textContent = '🌙 暗夜'; }
try { localStorage.setItem('ft-bci-theme', dark ? 'dark' : 'light'); } catch(e) {}
}
themeBtn.addEventListener('click', function() {
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
});
(function() {
try {
var s = localStorage.getItem('ft-bci-theme');
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
} catch(e) {}
})();
// ── 数据渲染 ──
var D = /*__DATA_SENTINEL__*/{};
document.getElementById('updateTime').textContent = '更新至 ' + D.summary.date;
// 卡片
var sv = D.summary;
document.getElementById('curValue').textContent = sv.value;
var chgEl = document.getElementById('curDelta');
var chg = sv.change;
chgEl.textContent = (chg >= 0 ? '+' : '') + chg.toFixed(2) + '%';
chgEl.className = 'delta ' + (chg >= 0 ? 'up' : 'down');
function fmtPct(v) {
var s = (v >= 0 ? '+' : '') + v.toFixed(2) + '%';
return '<span class="' + (v >= 0 ? 'arrow-up' : 'arrow-down') + '">' + s + '</span>';
}
document.getElementById('chg3m').innerHTML = fmtPct(sv.chg_3m);
document.getElementById('chg6m').innerHTML = fmtPct(sv.chg_6m);
document.getElementById('chg1y').innerHTML = fmtPct(sv.chg_1y);
// 信号
var strip = document.getElementById('signalStrip');
D.signals.forEach(function(s) {
var cls = s.signal === '偏多' ? 'up' : 'down';
strip.innerHTML += '<span class="signal-tag ' + cls + '">' + s.period + ': ' + s.signal + '</span>';
});
strip.innerHTML += '<span class="signal-verdict" style="color:' + D.overall.color + '">综合: ' + D.overall.signal + '</span>';
// 均线表
var maHtml = '';
['MA5','MA10','MA20','MA60'].forEach(function(k) {
var m = D.mas[k];
var dirIcon = m.direction === 'up' ? '↑' : (m.direction === 'down' ? '↓' : '→');
var dirCls = 'arrow-' + m.direction;
var posText = m.price_vs_ma === 'above' ? '价格在上 ↑' : '价格在下 ↓';
var posCls = m.price_vs_ma === 'above' ? 'arrow-up' : 'arrow-down';
maHtml += '<tr><td>' + k + '</td><td><strong>' + m.value + '</strong></td><td><span class="' + dirCls + '">' + dirIcon + '</span></td><td><span class="' + posCls + '">' + posText + '</span></td></tr>';
});
document.getElementById('maTable').innerHTML = maHtml;
// 近30日明细
var recentHtml = '';
D.recent.forEach(function(r) {
var rowCls = (r.chg >= 0) ? 'row-up' : 'row-down';
recentHtml += '<tr class="' + rowCls + '">';
recentHtml += '<td>' + r.date + '</td>';
recentHtml += '<td>' + r.value + '</td>';
recentHtml += '<td>' + (r.chg >= 0 ? '+' : '') + r.chg.toFixed(2) + '%</td>';
recentHtml += '<td>' + (r.MA5 != null ? r.MA5 : '-') + '</td>';
recentHtml += '<td>' + (r.MA20 != null ? r.MA20 : '-') + '</td>';
recentHtml += '<td>' + (r.MA60 != null ? r.MA60 : '-') + '</td>';
recentHtml += '</tr>';
});
document.getElementById('recentTable').innerHTML = recentHtml;
// ── 历史全景渲染 ──
(function renderHistory() {
var H = D.history;
if (!H || !H.monthly) return;
// 统计条
var s = H.stats;
var stHtml = '';
stHtml += '<div class="history-stat"><span class="label">均值</span><span class="value">' + s.mean + '</span></div>';
stHtml += '<div class="history-stat"><span class="label">历史低位</span><span class="value" style="color:var(--green)">' + s.min + '</span><span class="label">(' + s.min_date + ')</span></div>';
stHtml += '<div class="history-stat"><span class="label">历史高位</span><span class="value" style="color:var(--red)">' + s.max + '</span><span class="label">(' + s.max_date + ')</span></div>';
stHtml += '<div class="history-stat"><span class="label">当前分位</span><span class="value">' + s.percentile + '%</span></div>';
stHtml += '<div class="history-stat"><span class="label">下跌年数</span><span class="value">' + s.down_years + ' 年</span><span class="label">/ ' + (new Date().getFullYear() - 2012) + ' 年</span></div>';
stHtml += '<div class="history-stat"><span class="label">年迄今</span><span class="value" style="color:' + (s.ytd_chg >= 0 ? 'var(--green)' : 'var(--red)') + '">' + (s.ytd_chg >= 0 ? '+' : '') + s.ytd_chg.toFixed(1) + '%</span></div>';
document.getElementById('historyStats').innerHTML = stHtml;
// Canvas 图表
var canvas = document.getElementById('historyChart');
var dpr = window.devicePixelRatio || 1;
var W = canvas.parentElement.clientWidth - 2;
var H_c = Math.round(W * 0.42);
canvas.width = W * dpr;
canvas.height = H_c * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H_c + 'px';
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
var C = {
bg: isDark ? '#0f172a' : '#fff',
grid: isDark ? '#1e293b' : '#f1f5f9',
text: isDark ? '#94a3b8' : '#94a3b8',
price: isDark ? '#e2e8f0' : '#334155',
ma60: '#3b82f6',
upFill: isDark ? 'rgba(22,163,74,0.08)' : 'rgba(22,163,74,0.06)',
downFill: isDark ? 'rgba(220,38,38,0.06)' : 'rgba(220,38,38,0.04)',
marker: '#f59e0b',
cycleUp: 'rgba(22,163,74,0.12)',
cycleDown: 'rgba(220,38,38,0.08)',
};
var data = H.monthly;
var pad = { top: 20, right: 30, bottom: 32, left: 48 };
var pw = W - pad.left - pad.right;
var ph = H_c - pad.top - pad.bottom;
var vals = data.map(function(d) { return d.value; });
var ma60vals = data.map(function(d) { return d.ma60; });
var yMin = Math.floor(Math.min.apply(null, vals) / 10) * 10 - 20;
var yMax = Math.ceil(Math.max.apply(null, vals) / 10) * 10 + 20;
function x(i) { return pad.left + (i / (data.length - 1)) * pw; }
function y(v) { return pad.top + ph - ((v - yMin) / (yMax - yMin)) * ph; }
// 背景
ctx.fillStyle = C.bg;
ctx.fillRect(0, 0, W, H_c);
// 价格区域填充
ctx.beginPath();
ctx.moveTo(x(0), y(yMax));
for (var i = 0; i < data.length; i++) ctx.lineTo(x(i), y(vals[i]));
ctx.lineTo(x(data.length - 1), y(yMax));
ctx.closePath();
var grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + ph);
grad.addColorStop(0, isDark ? 'rgba(59,130,246,0.12)' : 'rgba(59,130,246,0.08)');
grad.addColorStop(1, 'rgba(59,130,246,0)');
ctx.fillStyle = grad;
ctx.fill();
// 网格
ctx.strokeStyle = C.grid;
ctx.lineWidth = 0.5;
var ySteps = 5;
for (var j = 0; j <= ySteps; j++) {
var vy = yMin + (yMax - yMin) * j / ySteps;
var py = y(vy);
ctx.beginPath();
ctx.moveTo(pad.left, py);
ctx.lineTo(pad.left + pw, py);
ctx.stroke();
ctx.fillStyle = C.text;
ctx.font = '10px -apple-system, sans-serif';
ctx.textAlign = 'right';
ctx.fillText(Math.round(vy), pad.left - 6, py + 4);
}
// 年份标签
ctx.fillStyle = C.text;
ctx.font = '10px -apple-system, sans-serif';
ctx.textAlign = 'center';
var lastYear = '';
for (var i = 0; i < data.length; i++) {
var yr = data[i].date.substring(0, 4);
if (yr !== lastYear) {
ctx.fillText(yr, x(i), pad.top + ph + 16);
lastYear = yr;
}
}
// 当前值虚线
ctx.setLineDash([4, 4]);
ctx.strokeStyle = C.marker;
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo(x(data.length - 1), pad.top);
ctx.lineTo(x(data.length - 1), pad.top + ph);
ctx.stroke();
ctx.setLineDash([]);
// 均值虚线
var meanY = y(s.mean);
ctx.setLineDash([3, 5]);
ctx.strokeStyle = C.text;
ctx.lineWidth = 0.6;
ctx.beginPath();
ctx.moveTo(pad.left, meanY);
ctx.lineTo(pad.left + pw, meanY);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = C.text;
ctx.textAlign = 'left';
ctx.fillText('均值 ' + s.mean, pad.left + 4, meanY - 3);
// MA60 线
ctx.beginPath();
var started = false;
for (var i = 0; i < data.length; i++) {
if (ma60vals[i] == null) continue;
if (!started) { ctx.moveTo(x(i), y(ma60vals[i])); started = true; }
else ctx.lineTo(x(i), y(ma60vals[i]));
}
ctx.strokeStyle = C.ma60;
ctx.lineWidth = 1.5;
ctx.stroke();
// 价格线
ctx.beginPath();
ctx.moveTo(x(0), y(vals[0]));
for (var i = 1; i < data.length; i++) ctx.lineTo(x(i), y(vals[i]));
ctx.strokeStyle = C.price;
ctx.lineWidth = 1.2;
ctx.stroke();
// 周期高亮区域
if (H.cycles) {
H.cycles.forEach(function(c) {
var si = data.findIndex(function(d) { return d.date >= c.start; });
var ei = data.findIndex(function(d) { return d.date > c.end; });
if (si < 0) si = 0;
if (ei < 0) ei = data.length - 1;
ctx.fillStyle = c.pct > 15 ? C.cycleUp : C.cycleDown;
ctx.fillRect(x(si), pad.top, x(ei) - x(si), ph);
// 标签
if (c.label) {
var lx = (x(si) + x(ei)) / 2;
ctx.fillStyle = C.text;
ctx.font = 'bold 9px -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(c.label + ' +' + c.pct + '%', lx, pad.top + 12);
}
});
}
// 当前反弹区域
if (H.current_rebound && H.current_rebound.pct > 0) {
var cr = H.current_rebound;
var si2 = data.findIndex(function(d) { return d.date >= cr.start; });
if (si2 >= 0) {
ctx.fillStyle = isDark ? 'rgba(245,158,11,0.10)' : 'rgba(245,158,11,0.12)';
ctx.fillRect(x(si2), pad.top, x(data.length - 1) - x(si2), ph);
ctx.fillStyle = '#d97706';
ctx.font = 'bold 9px -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('反弹 +' + cr.pct + '%', (x(si2) + x(data.length - 1)) / 2, pad.top + ph - 6);
}
}
// 当前值圆点
var cx = x(data.length - 1), cy = y(vals[data.length - 1]);
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fillStyle = C.marker;
ctx.fill();
ctx.strokeStyle = isDark ? '#0f172a' : '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
// 标注
ctx.fillStyle = C.marker;
ctx.font = 'bold 12px -apple-system, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(vals[data.length - 1], cx + 8, cy + 4);
})();
// ── 周期对比表 ──
(function renderCycles() {
var H = D.history;
if (!H) return;
var allCycles = (H.cycles || []).slice();
if (H.current_rebound) allCycles.push(H.current_rebound);
var maxPct = 0;
allCycles.forEach(function(c) { if (c.pct > maxPct) maxPct = c.pct; });
var html = '';
allCycles.forEach(function(c, idx) {
var isCurrent = c.end == null;
var rowStyle = isCurrent ? ' style="font-weight:700"' : '';
var barW = Math.max(4, Math.round((c.pct / maxPct) * 100));
var barCls = c.pct >= 0 ? 'up' : 'down';
html += '<tr' + rowStyle + '>';
html += '<td>' + (c.label || ('周期 ' + (idx + 1))) + '</td>';
html += '<td>' + (c.pct >= 15 ? '大牛市' : (c.pct >= 0 ? '反弹' : '下跌')) + '</td>';
html += '<td>' + c.start + ' → ' + (c.end || '至今') + '</td>';
html += '<td>' + c.from + ' → ' + c.to + '</td>';
html += '<td class="bar-cell"><span class="cycle-bar ' + barCls + '" style="width:' + barW + 'px"></span>' + (c.pct > 0 ? '+' : '') + c.pct.toFixed(1) + '%</td>';
html += '</tr>';
});
document.getElementById('cycleTable').innerHTML = html;
// 注释
var note = '';
if (H.ma20_vs_ma60 === 'below') {
note += '⚠ MA20 仍低于 MA60,长期空头排列尚未修复。';
}
if (H.current_rebound && H.current_rebound.pct < 15) {
if (allCycles.length > 0) {
var bigCycles = allCycles.filter(function(c) { return c.pct >= 15; });
if (bigCycles.length > 0) {
note += ' 历史大级别反弹均 >15%,当前涨幅尚属弱势反弹范畴。';
}
}
}
document.getElementById('cycleNote').textContent = note;
})();
</script>
</body>
</html>