@arcaneorion/dsh-teaching-board 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/cordis.patch.yml +6 -2
- package/package.json +5 -3
- package/skills/stage-panel/SKILL.md +107 -0
- package/skills/stage-panel/references/diagram-selection.md +80 -0
- package/skills/stage-panel/references/html-patterns.md +93 -0
- package/skills/stage-panel/references/visual-style.md +43 -0
- package/skills/stage-panel/submode/code/SKILL.md +17 -0
- package/skills/stage-panel/submode/code/examples/go-hello-channel.json +36 -0
- package/skills/stage-panel/submode/code/examples/go-http-api-basics.json +58 -0
- package/skills/stage-panel/submode/code/examples/py-agent-loop.json +79 -0
- package/skills/stage-panel/submode/code/examples/py-ai-parameters-intro.json +65 -0
- package/skills/stage-panel/submode/code/examples/py-fizzbuzz-intro.json +74 -0
- package/skills/stage-panel/submode/code/examples/py-training-loop.json +114 -0
- package/skills/stage-panel/submode/code/examples/rs-move-basics.json +118 -0
- package/skills/stage-panel/submode/code/references/host-toolchains.md +61 -0
- package/skills/stage-panel/submode/code/schemas/lesson.schema.json +225 -0
- package/skills/stage-panel/submode/code/templates/lab.html +561 -0
- package/skills/stage-panel/submode/game-study/SKILL.md +16 -0
- package/skills/stage-panel/submode/game-study/examples/hft-microstructure.json +60 -0
- package/skills/stage-panel/submode/game-study/examples/securities-law.json +57 -0
- package/skills/stage-panel/submode/game-study/schemas/quest.schema.json +95 -0
- package/skills/stage-panel/submode/game-study/templates/quiz.html +516 -0
- package/skills/stage-panel/submode/learn/SKILL.md +16 -0
- package/skills/stage-panel/submode/learn/examples/epsilon-delta.json +140 -0
- package/skills/stage-panel/submode/learn/examples/template-showcase.json +147 -0
- package/skills/stage-panel/submode/learn/schemas/lesson.schema.json +184 -0
- package/skills/stage-panel/submode/learn/templates/lesson.html +782 -0
- package/skills/stage-panel/submode/ml/SKILL.md +9 -0
- package/skills/stage-panel/submode/ml/examples/gd.json +7 -0
- package/skills/stage-panel/submode/ml/templates/gd.html +217 -0
- package/skills/stage-panel/submode/quant/SKILL.md +9 -0
- package/skills/stage-panel/submode/quant/references/export_backtest_data.py +192 -0
- package/skills/stage-panel/submode/quant/templates/dashboard.html +381 -0
- package/src/client.js +177 -15
- package/src/index.js +11 -0
- package/src/skills.js +88 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>因子回测仪表盘 · factor-lab</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
/* 呼应 visual-style.md:蓝灰科幻底 + 高对比黄 + 硬黑描边 + 小圆角 */
|
|
11
|
+
--bg: #0f2233; --surface: #16334a; --surface2: #1b3d57;
|
|
12
|
+
--border: #07111b; --text: #dbeaf5; --text2: #93b3c9;
|
|
13
|
+
--accent: #ffd43b; --green: #7ee787; --red: #ff8a8a; --orange: #ffb454;
|
|
14
|
+
--line: #07111b;
|
|
15
|
+
}
|
|
16
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
17
|
+
body {
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
19
|
+
background: var(--bg); color: var(--text); min-height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
.topbar {
|
|
22
|
+
background: var(--surface); border-bottom: 2px solid var(--border);
|
|
23
|
+
padding: 14px 24px; display: flex; align-items: center; gap: 16px;
|
|
24
|
+
position: sticky; top: 0; z-index: 100;
|
|
25
|
+
}
|
|
26
|
+
.topbar .logo { font-size: 18px; font-weight: 700; color: var(--accent); }
|
|
27
|
+
.topbar .sep { color: var(--text2); }
|
|
28
|
+
.topbar .sub { color: var(--text2); font-size: 13px; }
|
|
29
|
+
|
|
30
|
+
.tabs { display: flex; padding: 0 24px; background: var(--surface); border-bottom: 2px solid var(--border); overflow-x: auto; }
|
|
31
|
+
.tab { padding: 12px 20px; cursor: pointer; color: var(--text2); border-bottom: 2px solid transparent; font-size: 14px; white-space: nowrap; transition: all 0.2s; }
|
|
32
|
+
.tab:hover { color: var(--text); }
|
|
33
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
34
|
+
.tab.error { color: var(--red); opacity: 0.5; }
|
|
35
|
+
.tab.error.active { opacity: 1; border-bottom-color: var(--red); color: var(--red); }
|
|
36
|
+
|
|
37
|
+
.main { padding: 20px 24px; max-width: 1800px; margin: 0 auto; }
|
|
38
|
+
|
|
39
|
+
.section-title { font-size: 12px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; margin-top: 24px; }
|
|
40
|
+
|
|
41
|
+
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 8px; }
|
|
42
|
+
.metric-card { background: var(--surface); border: 2px solid var(--border); border-radius: 6px; padding: 12px 14px; text-align: center; }
|
|
43
|
+
.metric-card .label { font-size: 10px; color: var(--text2); text-transform: uppercase; margin-bottom: 4px; }
|
|
44
|
+
.metric-card .value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
45
|
+
.metric-card .value.positive { color: var(--green); }
|
|
46
|
+
.metric-card .value.negative { color: var(--red); }
|
|
47
|
+
.metric-card .value.neutral { color: var(--text); }
|
|
48
|
+
|
|
49
|
+
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 24px; }
|
|
50
|
+
.chart-box { background: var(--surface); border: 2px solid var(--border); border-radius: 6px; padding: 14px; }
|
|
51
|
+
.chart-box.full { grid-column: 1 / -1; }
|
|
52
|
+
.chart-box .title { font-size: 12px; font-weight: 600; color: var(--text2); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
53
|
+
.chart-box .canvas-wrap { position: relative; width: 100%; height: 260px; }
|
|
54
|
+
.chart-box .canvas-wrap canvas { width: 100% !important; height: 100% !important; }
|
|
55
|
+
|
|
56
|
+
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: var(--text2); text-align: center; }
|
|
57
|
+
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
|
|
58
|
+
.empty-state .msg { font-size: 15px; margin-bottom: 6px; }
|
|
59
|
+
|
|
60
|
+
/* 截面明细表 */
|
|
61
|
+
.xsec-box { background: var(--surface); border: 2px solid var(--border); border-radius: 6px; padding: 14px; }
|
|
62
|
+
.xsec-box .title { font-size: 12px; font-weight: 600; color: var(--text2); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
63
|
+
.xsec-tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
64
|
+
.xsec-tab { padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px; background: var(--surface2); color: var(--text2); border: 2px solid var(--border); transition: all 0.15s; }
|
|
65
|
+
.xsec-tab:hover { color: var(--text); }
|
|
66
|
+
.xsec-tab.active { background: var(--accent); color: var(--line); border-color: var(--accent); font-weight: 700; }
|
|
67
|
+
.xsec-tab.stats { font-size: 11px; color: var(--text2); padding: 6px 8px; }
|
|
68
|
+
table.xsec { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
69
|
+
table.xsec th { text-align: left; padding: 6px 10px; color: var(--text2); font-weight: 500; border-bottom: 2px solid var(--border); font-size: 10px; text-transform: uppercase; }
|
|
70
|
+
table.xsec td { padding: 5px 10px; border-bottom: 1px solid rgba(147,179,201,0.25); }
|
|
71
|
+
table.xsec tr:hover td { background: var(--surface2); }
|
|
72
|
+
table.xsec .sym { color: var(--accent); font-weight: 600; }
|
|
73
|
+
table.xsec .pos { color: var(--green); }
|
|
74
|
+
table.xsec .neg { color: var(--red); }
|
|
75
|
+
|
|
76
|
+
@media (max-width: 900px) { .charts { grid-template-columns: 1fr; } }
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
|
|
81
|
+
<div class="topbar">
|
|
82
|
+
<span class="logo">factor-lab</span>
|
|
83
|
+
<span class="sep">/</span>
|
|
84
|
+
<span class="sub">单因子回测仪表盘</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="tabs" id="tabs"></div>
|
|
88
|
+
|
|
89
|
+
<div class="main">
|
|
90
|
+
<div class="section-title">📊 IC 分析</div>
|
|
91
|
+
<div class="metrics" id="icMetrics"></div>
|
|
92
|
+
|
|
93
|
+
<div class="section-title" style="margin-top:14px;">⚡ 多空绩效</div>
|
|
94
|
+
<div class="metrics" id="lsMetrics"></div>
|
|
95
|
+
|
|
96
|
+
<div class="section-title" style="margin-top:14px;">📈 图表</div>
|
|
97
|
+
<div class="charts">
|
|
98
|
+
<div class="chart-box"><div class="title">📈 Rank IC 时序</div><div class="canvas-wrap"><canvas id="icChart"></canvas></div></div>
|
|
99
|
+
<div class="chart-box"><div class="title">📊 IC 分布直方图</div><div class="canvas-wrap"><canvas id="icHist"></canvas></div></div>
|
|
100
|
+
<div class="chart-box"><div class="title">📉 分层累计收益</div><div class="canvas-wrap"><canvas id="cumChart"></canvas></div></div>
|
|
101
|
+
<div class="chart-box"><div class="title">⚡ 多空累计收益</div><div class="canvas-wrap"><canvas id="lsChart"></canvas></div></div>
|
|
102
|
+
<div class="chart-box"><div class="title">📋 分组月均收益</div><div class="canvas-wrap"><canvas id="barChart"></canvas></div></div>
|
|
103
|
+
<div class="chart-box"><div class="title">📅 每月多空收益</div><div class="canvas-wrap"><canvas id="lsMonthly"></canvas></div></div>
|
|
104
|
+
<div class="chart-box"><div class="title">📐 因子值分布 (最新截面)</div><div class="canvas-wrap"><canvas id="distChart"></canvas></div></div>
|
|
105
|
+
<div class="chart-box"><div class="title">🏆 全因子 ICIR 对比</div><div class="canvas-wrap"><canvas id="compareChart"></canvas></div></div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="xsec-box" id="xsecBox">
|
|
109
|
+
<div class="title">📋 截面明细 — 各组 Top 10 股票</div>
|
|
110
|
+
<div class="xsec-tabs" id="xsecTabs"></div>
|
|
111
|
+
<div id="xsecStats" style="font-size:11px;color:var(--text2);margin-bottom:8px;"></div>
|
|
112
|
+
<table class="xsec" id="xsecTable"></table>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
// ======================== 数据内嵌 ========================
|
|
118
|
+
const allData = __BACKTEST_DATA__;
|
|
119
|
+
let activeFactor = null;
|
|
120
|
+
let selectedGroup = 'G0';
|
|
121
|
+
|
|
122
|
+
function initTabs() {
|
|
123
|
+
const tabsEl = document.getElementById('tabs');
|
|
124
|
+
const factors = Object.entries(allData).filter(([k]) => !k.startsWith('_'));
|
|
125
|
+
if (factors.length === 0) {
|
|
126
|
+
tabsEl.innerHTML = '<div class="tab error">暂无数据</div>';
|
|
127
|
+
showEmpty('没有可用的回测数据。请运行: uv run --project .. python export_backtest_data.py');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
factors.forEach(([key, info]) => {
|
|
131
|
+
const tab = document.createElement('div');
|
|
132
|
+
tab.className = 'tab';
|
|
133
|
+
if (info.error) tab.className += ' error';
|
|
134
|
+
tab.textContent = info.label || key;
|
|
135
|
+
tab.dataset.key = key;
|
|
136
|
+
tab.addEventListener('click', () => selectFactor(key));
|
|
137
|
+
tabsEl.appendChild(tab);
|
|
138
|
+
});
|
|
139
|
+
const firstOk = factors.find(([k, v]) => !v.error);
|
|
140
|
+
selectFactor(firstOk ? firstOk[0] : factors[0][0]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectFactor(key) {
|
|
144
|
+
activeFactor = key;
|
|
145
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.key === key));
|
|
146
|
+
const info = allData[key];
|
|
147
|
+
if (!info || info.error) {
|
|
148
|
+
showEmpty(`因子 "${info?.label || key}" 数据不可用: ${info?.error || '未知错误'}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
selectedGroup = 'G0';
|
|
152
|
+
renderInfo(info);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ======================== 渲染 ========================
|
|
156
|
+
let charts = {};
|
|
157
|
+
|
|
158
|
+
const CHART_BOX_ORDER = ['icChart','icHist','cumChart','lsChart','barChart','lsMonthly','distChart','compareChart'];
|
|
159
|
+
|
|
160
|
+
function destroyCharts() {
|
|
161
|
+
Object.values(charts).forEach(c => c.destroy());
|
|
162
|
+
charts = {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function showEmpty(msg) {
|
|
166
|
+
destroyCharts();
|
|
167
|
+
document.getElementById('icMetrics').innerHTML = '';
|
|
168
|
+
document.getElementById('lsMetrics').innerHTML = '';
|
|
169
|
+
// 图表区
|
|
170
|
+
document.querySelectorAll('.chart-box').forEach(box => {
|
|
171
|
+
const t = box.querySelector('.title');
|
|
172
|
+
box.innerHTML = '';
|
|
173
|
+
if (t) box.appendChild(t);
|
|
174
|
+
box.insertAdjacentHTML('beforeend', `<div class="empty-state"><div class="icon">📭</div><div class="msg">${msg}</div></div>`);
|
|
175
|
+
});
|
|
176
|
+
// 截面区
|
|
177
|
+
document.getElementById('xsecBox').style.display = 'none';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 恢复 canvas 元素
|
|
181
|
+
function restoreCanvases() {
|
|
182
|
+
document.querySelectorAll('.chart-box').forEach(box => {
|
|
183
|
+
const titleEl = box.querySelector('.title');
|
|
184
|
+
if (!box.querySelector('canvas') && titleEl) {
|
|
185
|
+
const titleText = titleEl.textContent;
|
|
186
|
+
let canvasId = '';
|
|
187
|
+
if (titleText.includes('IC 时序')) canvasId = 'icChart';
|
|
188
|
+
else if (titleText.includes('分布直方图') && titleText.includes('IC')) canvasId = 'icHist';
|
|
189
|
+
else if (titleText.includes('分层累计')) canvasId = 'cumChart';
|
|
190
|
+
else if (titleText.includes('多空累计')) canvasId = 'lsChart';
|
|
191
|
+
else if (titleText.includes('分组月均')) canvasId = 'barChart';
|
|
192
|
+
else if (titleText.includes('每月多空')) canvasId = 'lsMonthly';
|
|
193
|
+
else if (titleText.includes('因子值分布')) canvasId = 'distChart';
|
|
194
|
+
else if (titleText.includes('ICIR 对比')) canvasId = 'compareChart';
|
|
195
|
+
if (canvasId) {
|
|
196
|
+
box.innerHTML = '';
|
|
197
|
+
box.appendChild(titleEl);
|
|
198
|
+
box.insertAdjacentHTML('beforeend', `<div class="canvas-wrap"><canvas id="${canvasId}"></canvas></div>`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderInfo(info) {
|
|
205
|
+
destroyCharts();
|
|
206
|
+
restoreCanvases();
|
|
207
|
+
document.getElementById('xsecBox').style.display = 'block';
|
|
208
|
+
|
|
209
|
+
// ---- IC 指标卡片 ----
|
|
210
|
+
const s = info.ic_summary;
|
|
211
|
+
const icMean = s['IC 均值'];
|
|
212
|
+
const icir = s['ICIR'];
|
|
213
|
+
const icCards = [
|
|
214
|
+
{ L:'IC 均值', V:icMean, C:icMean>0.02?'positive':icMean<-0.02?'negative':'neutral', F:v=>v.toFixed(4) },
|
|
215
|
+
{ L:'IC 标准差', V:s['IC 标准差'], C:'neutral', F:v=>v.toFixed(4) },
|
|
216
|
+
{ L:'ICIR', V:icir, C:icir>0.5?'positive':icir<-0.5?'negative':'neutral', F:v=>v.toFixed(3) },
|
|
217
|
+
{ L:'IC > 0 占比', V:s['IC > 0 占比'], C:s['IC > 0 占比']>0.55?'positive':'neutral', F:v=>(v*100).toFixed(1)+'%' },
|
|
218
|
+
{ L:'|IC| > 0.02', V:s['|IC| > 0.02 占比'], C:'neutral', F:v=>(v*100).toFixed(1)+'%' },
|
|
219
|
+
{ L:'月数', V:info.ic_series.length, C:'neutral', F:v=>v },
|
|
220
|
+
];
|
|
221
|
+
document.getElementById('icMetrics').innerHTML = icCards.map(m => `<div class="metric-card"><div class="label">${m.L}</div><div class="value ${m.C}">${m.F(m.V)}</div></div>`).join('');
|
|
222
|
+
|
|
223
|
+
// ---- 多空指标卡片 ----
|
|
224
|
+
const lm = info.long_short_metrics || {};
|
|
225
|
+
const lsCards = [
|
|
226
|
+
{ L:'年化夏普', V:lm.sharpe_annual||0, C:(lm.sharpe_annual||0)>0?'positive':'negative', F:v=>v.toFixed(2) },
|
|
227
|
+
{ L:'胜率', V:lm.win_rate||0, C:(lm.win_rate||0)>0.5?'positive':'neutral', F:v=>(v*100).toFixed(1)+'%' },
|
|
228
|
+
{ L:'最大回撤', V:lm.max_drawdown||0, C:(lm.max_drawdown||0)>-0.1?'positive':'negative', F:v=>(v*100).toFixed(1)+'%' },
|
|
229
|
+
{ L:'累计收益', V:lm.total_return||0, C:(lm.total_return||0)>0?'positive':'negative', F:v=>(v*100).toFixed(1)+'%' },
|
|
230
|
+
];
|
|
231
|
+
document.getElementById('lsMetrics').innerHTML = lsCards.map(m => `<div class="metric-card"><div class="label">${m.L}</div><div class="value ${m.C}">${m.F(m.V)}</div></div>`).join('');
|
|
232
|
+
|
|
233
|
+
// ---- 图表 1: IC 时序 ----
|
|
234
|
+
const icData = info.ic_series;
|
|
235
|
+
const icLabels = icData.map(d => d.date.slice(0, 7));
|
|
236
|
+
const icValues = icData.map(d => d.ic);
|
|
237
|
+
charts.ic = new Chart(document.getElementById('icChart'), {
|
|
238
|
+
type: 'bar', data: { labels: icLabels, datasets: [{ data: icValues, backgroundColor: icValues.map(v => v>=0?'rgba(38,166,154,0.7)':'rgba(239,83,80,0.6)'), borderWidth:0, borderRadius:2 }] },
|
|
239
|
+
options: chartOpts('bar', { plugins:{ tooltip:{ callbacks:{ label:ctx=>`IC: ${ctx.raw.toFixed(4)}` } } }, scales:{ y:{ title:{ display:true, text:'Rank IC' } } } })
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ---- 图表 2: IC 分布 ----
|
|
243
|
+
const bins=15, mn=Math.min(...icValues), mx=Math.max(...icValues), bw=(mx-mn)/bins||0.01;
|
|
244
|
+
const hd=Array(bins).fill(0); icValues.forEach(v=>{ const i=Math.min(Math.floor((v-mn)/bw),bins-1); hd[i]++; });
|
|
245
|
+
charts.hist = new Chart(document.getElementById('icHist'), {
|
|
246
|
+
type: 'bar', data: { labels: Array.from({length:bins},(_,i)=>(mn+bw*i).toFixed(2)), datasets: [{ data: hd, backgroundColor:'rgba(91,141,239,0.6)', borderWidth:0, borderRadius:2 }] },
|
|
247
|
+
options: chartOpts('bar', { scales:{ y:{ title:{display:true,text:'频数'} }, x:{ title:{display:true,text:'IC 值'} } } })
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ---- 图表 3: 分层累计收益 ----
|
|
251
|
+
const cum=info.cum_returns, gc=['#ef5350','#ffa726','#ffee58','#66bb6a','#26a69a'];
|
|
252
|
+
charts.cum = new Chart(document.getElementById('cumChart'), {
|
|
253
|
+
type: 'line', data: { datasets: Object.entries(cum).map(([g,pts],i)=>({ label:g, data:pts.map(p=>({x:p.date.slice(0,7),y:p.cum_return})), borderColor:gc[i], backgroundColor:'transparent', borderWidth:1.5, pointRadius:0, tension:0.1 })) },
|
|
254
|
+
options: chartOpts('line', { scales:{ y:{ title:{display:true,text:'累计收益 (1=基准)'} } }, plugins:{ tooltip:{ callbacks:{ label:ctx=>`${ctx.dataset.label}: ${(ctx.raw.y||ctx.raw).toFixed(3)}` } } } })
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ---- 图表 4: 多空累计收益 ----
|
|
258
|
+
const lsData = info.long_short;
|
|
259
|
+
if(lsData && lsData.length>0) {
|
|
260
|
+
let cl=1;
|
|
261
|
+
charts.ls = new Chart(document.getElementById('lsChart'), {
|
|
262
|
+
type: 'line', data: { datasets: [{ label:'Long-Short', data: lsData.map(p=>{ cl*=(1+p.return); return {x:p.date.slice(0,7),y:cl}; }), borderColor:'rgba(91,141,239,1)', backgroundColor:'rgba(91,141,239,0.1)', fill:true, borderWidth:2, pointRadius:0, tension:0.1 }] },
|
|
263
|
+
options: chartOpts('line', { scales:{ y:{ title:{display:true,text:'累计收益'} } } })
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- 图表 5: 分组月均收益 ----
|
|
268
|
+
const grAvg=info.group_avg_return, gk=Object.keys(grAvg).sort(), gv=gk.map(k=>grAvg[k]*100);
|
|
269
|
+
charts.bar = new Chart(document.getElementById('barChart'), {
|
|
270
|
+
type: 'bar', data: { labels:gk, datasets: [{ data:gv, backgroundColor:gv.map((_,i)=>i===0?'rgba(239,83,80,0.7)':i===gv.length-1?'rgba(38,166,154,0.7)':'rgba(91,141,239,0.6)'), borderWidth:0, borderRadius:3 }] },
|
|
271
|
+
options: chartOpts('bar', { scales:{ y:{ title:{display:true,text:'月均收益 (%)'} } }, plugins:{ tooltip:{ callbacks:{ label:ctx=>`${ctx.raw.toFixed(2)}%` } } } })
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ---- 图表 6: 每月多空收益柱状图 (NEW) ----
|
|
275
|
+
if(lsData && lsData.length>0) {
|
|
276
|
+
const lsLabels=lsData.map(d=>d.date.slice(0,7)), lsVals=lsData.map(d=>d.return*100);
|
|
277
|
+
charts.lsMonthly = new Chart(document.getElementById('lsMonthly'), {
|
|
278
|
+
type: 'bar', data: { labels:lsLabels, datasets: [{ data:lsVals, backgroundColor:lsVals.map(v=>v>=0?'rgba(38,166,154,0.6)':'rgba(239,83,80,0.5)'), borderWidth:0, borderRadius:2 }] },
|
|
279
|
+
options: chartOpts('bar', { scales:{ y:{ title:{display:true,text:'多空收益 (%)'} } }, plugins:{ tooltip:{ callbacks:{ label:ctx=>`${ctx.raw.toFixed(2)}%` } } } })
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- 图表 7: 因子值分布直方图 (NEW) ----
|
|
284
|
+
const fd = info.factor_distribution;
|
|
285
|
+
if(fd && fd.histogram && fd.histogram.length>0) {
|
|
286
|
+
const distBins=fd.histogram, distLabels=distBins.map(b=>b.bin_start.toFixed(2)), distCounts=distBins.map(b=>b.count);
|
|
287
|
+
charts.dist = new Chart(document.getElementById('distChart'), {
|
|
288
|
+
type: 'bar', data: { labels:distLabels, datasets: [{ data:distCounts, backgroundColor:'rgba(255,167,38,0.6)', borderWidth:0, borderRadius:1 }] },
|
|
289
|
+
options: chartOpts('bar', { scales:{ y:{ title:{display:true,text:'股票数'} }, x:{ title:{display:true,text:'因子值'} } } })
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---- 图表 8: 全因子 ICIR 对比 ----
|
|
294
|
+
renderCompareChart();
|
|
295
|
+
|
|
296
|
+
// ---- 截面明细表 (NEW) ----
|
|
297
|
+
renderCrossSection(info);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ======================== 全因子 ICIR 对比 ========================
|
|
301
|
+
function renderCompareChart() {
|
|
302
|
+
const factors=Object.entries(allData).filter(([k,v])=>!k.startsWith('_')&&!v.error);
|
|
303
|
+
const labels=factors.map(([k,v])=>v.label), icirVals=factors.map(([k,v])=>v.ic_summary['ICIR']);
|
|
304
|
+
const icMeanVals=factors.map(([k,v])=>v.ic_summary['IC 均值']), hi=factors.findIndex(([k])=>k===activeFactor);
|
|
305
|
+
charts.compare = new Chart(document.getElementById('compareChart'), {
|
|
306
|
+
type: 'bar', data: { labels, datasets: [{ label:'ICIR', data:icirVals, backgroundColor:factors.map((_,i)=>i===hi?(icirVals[i]>0?'rgba(38,166,154,0.9)':'rgba(239,83,80,0.9)'):(icirVals[i]>0?'rgba(38,166,154,0.35)':'rgba(239,83,80,0.3)')), borderWidth:0, borderRadius:3 }] },
|
|
307
|
+
options: chartOpts('bar', { plugins:{ tooltip:{ callbacks:{ label:ctx=>{ const i=ctx.dataIndex; return [`ICIR: ${icirVals[i].toFixed(3)}`,`IC均值: ${icMeanVals[i].toFixed(4)}`]; } } }, legend:{display:false} }, scales:{ y:{ title:{display:true,text:'ICIR'} } } })
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ======================== 截面明细表 ========================
|
|
312
|
+
function renderCrossSection(info) {
|
|
313
|
+
const xs = info.cross_section;
|
|
314
|
+
if(!xs || !xs.groups) { document.getElementById('xsecBox').style.display='none'; return; }
|
|
315
|
+
|
|
316
|
+
const groups = Object.keys(xs.groups).sort();
|
|
317
|
+
// 顶部 Tab 导航
|
|
318
|
+
const tabsHtml = groups.map(g => {
|
|
319
|
+
const gd = xs.groups[g];
|
|
320
|
+
const fwdMean = gd.forward_mean ? (gd.forward_mean*100).toFixed(2) : '-';
|
|
321
|
+
return `<span class="xsec-tab ${g===selectedGroup?'active':''}" data-group="${g}" onclick="switchGroup('${g}')">${g}</span>`;
|
|
322
|
+
}).join('');
|
|
323
|
+
document.getElementById('xsecTabs').innerHTML = tabsHtml;
|
|
324
|
+
|
|
325
|
+
switchGroupTable(xs);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function switchGroup(g) {
|
|
329
|
+
selectedGroup = g;
|
|
330
|
+
document.querySelectorAll('.xsec-tab').forEach(t => t.classList.toggle('active', t.dataset.group===g));
|
|
331
|
+
const info = allData[activeFactor];
|
|
332
|
+
if(info) switchGroupTable(info.cross_section);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function switchGroupTable(xs) {
|
|
336
|
+
const gd = xs.groups[selectedGroup];
|
|
337
|
+
if(!gd) return;
|
|
338
|
+
|
|
339
|
+
const fwdMean = gd.forward_mean ? (gd.forward_mean*100).toFixed(2) : '-';
|
|
340
|
+
document.getElementById('xsecStats').innerHTML = `${selectedGroup}: ${gd.count} 只 | 因子均值=${gd.factor_mean.toFixed(4)} | 因子标准差=${gd.factor_std.toFixed(4)} | 下月均收益=${fwdMean}%`;
|
|
341
|
+
|
|
342
|
+
const stocks = gd.top10 || [];
|
|
343
|
+
if(stocks.length===0) { document.getElementById('xsecTable').innerHTML='<tr><td colspan="4">无数据</td></tr>'; return; }
|
|
344
|
+
document.getElementById('xsecTable').innerHTML = `<thead><tr><th>#</th><th>代码</th><th>因子值</th><th>下月收益</th></tr></thead><tbody>${
|
|
345
|
+
stocks.map((s,i) => `<tr>
|
|
346
|
+
<td style="color:var(--text2)">${i+1}</td>
|
|
347
|
+
<td class="sym">${s.symbol}</td>
|
|
348
|
+
<td>${s.factor_value.toFixed(4)}</td>
|
|
349
|
+
<td class="${(s.forward_return||0)>=0?'pos':'neg'}">${s.forward_return?((s.forward_return)*100).toFixed(2)+'%':'-'}</td>
|
|
350
|
+
</tr>`).join('')
|
|
351
|
+
}</tbody>`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ======================== 图表配置 ========================
|
|
355
|
+
function chartOpts(type, overrides={}) {
|
|
356
|
+
const base = {
|
|
357
|
+
responsive:true, maintainAspectRatio:false, animation:{duration:400},
|
|
358
|
+
plugins:{
|
|
359
|
+
legend:{ labels:{ color:'#93b3c9', usePointStyle:true, boxWidth:8, padding:12, font:{size:11} } },
|
|
360
|
+
tooltip:{ backgroundColor:'#16334a', borderColor:'#07111b', borderWidth:2, titleColor:'#dbeaf5', bodyColor:'#dbeaf5', padding:10 }
|
|
361
|
+
},
|
|
362
|
+
scales:{
|
|
363
|
+
x:{ ticks:{ color:'#93b3c9', maxTicksLimit:12, maxRotation:45, font:{size:10} }, grid:{ color:'rgba(147,179,201,0.2)' } },
|
|
364
|
+
y:{ ticks:{ color:'#93b3c9', font:{size:10} }, grid:{ color:'rgba(147,179,201,0.2)' }, beginAtZero: type==='bar' }
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
return deepMerge(base, overrides);
|
|
368
|
+
}
|
|
369
|
+
function deepMerge(target, source) {
|
|
370
|
+
const out = Object.assign({}, target);
|
|
371
|
+
for(const key of Object.keys(source)) {
|
|
372
|
+
if(source[key] && typeof source[key]==='object' && !Array.isArray(source[key])) out[key]=deepMerge(target[key]||{},source[key]);
|
|
373
|
+
else out[key]=source[key];
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
initTabs();
|
|
379
|
+
</script>
|
|
380
|
+
</body>
|
|
381
|
+
</html>
|