Files
sanguo_vnpy_v2/docs/factor_research/factor-tears-compare-mockup.html
T

544 lines
36 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">
<title>因子 tears 与对比设计稿</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap">
<style>
:root{
--bg:#05080d;--bg-card:#0a0f17;--bg-hover:#0e1622;--bg-overlay:#0d1320;--panel:#070b12;
--text:#d4e4f0;--text-2:#7a8a9a;--text-3:#4a5868;
--brand:#00e5ff;--cyan-dim:#00b8cc;--cyan-soft:rgba(0,229,255,0.14);
--purple-dim:#b48cff;--amber:#ffb000;--amber-soft:rgba(255,176,0,0.14);
--up:#ff4d5e;--down:#2ee68a;--danger:#ff4d5e;
--lamp-ok:#2ee68a;--lamp-warn:#ffb000;--lamp-idle:#4a5868;
--border:#16202e;--border-2:#0f1822;
--grid-line:rgba(0,229,255,0.035);--grid-size:32px;
--r-sm:2px;--r-md:3px;--r-lg:4px;
--mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',Roboto,sans-serif;
--ease:cubic-bezier(0.4,0,0.2,1);
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:13px;line-height:1.55;-webkit-font-smoothing:antialiased}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
background-image:linear-gradient(var(--grid-line) 1px,transparent 1px),linear-gradient(90deg,var(--grid-line) 1px,transparent 1px);
background-size:var(--grid-size) var(--grid-size)}
.wrap{position:relative;z-index:1;max-width:1240px;margin:0 auto;padding:18px 22px 90px}
.mono{font-family:var(--mono)}
button{font-family:inherit}
button:focus-visible{outline:1px solid var(--brand);outline-offset:2px}
/* ===== 顶栏 ===== */
.topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;border:1px solid var(--border);background:var(--bg-card);border-radius:var(--r-lg);padding:10px 14px;margin-bottom:14px;flex-wrap:wrap}
.topbar .t{font-family:var(--mono);font-size:12.5px;letter-spacing:.05em;font-weight:700}
.topbar .t .dim{color:var(--text-3);font-weight:400}
.tabs{display:flex;gap:6px;flex-wrap:wrap}
.tabs button{font-family:var(--mono);font-size:11.5px;padding:5px 14px;border-radius:var(--r-sm);border:1px solid var(--border);background:transparent;color:var(--text-2);cursor:pointer;transition:all .15s var(--ease)}
.tabs button:hover{color:var(--text);border-color:var(--text-3)}
.tabs button.on{color:var(--brand);background:var(--cyan-soft);border-color:rgba(0,229,255,.35)}
.tab{display:none}
.tab.on{display:block}
/* ===== 设计标注 ===== */
.dcap{display:flex;align-items:center;gap:10px;margin:16px 2px 10px;flex-wrap:wrap}
.dcap .no{font-family:var(--mono);font-size:11px;color:var(--amber);border:1px solid rgba(255,176,0,.4);background:var(--amber-soft);border-radius:var(--r-sm);padding:2px 8px;letter-spacing:.08em;white-space:nowrap}
.dcap .txt{color:var(--text-2);font-size:12px}
.dcap .txt b{color:var(--text)}
.mock{border:1px solid var(--border);background:var(--bg);border-radius:var(--r-lg);padding:20px 22px 26px;box-shadow:0 8px 32px rgba(0,0,0,.6)}
.mock.flash{box-shadow:0 0 0 1px rgba(0,229,255,.55),0 0 24px rgba(0,229,255,.15)}
.new-tag{font-family:var(--mono);font-size:9px;color:var(--amber);border:1px dashed rgba(255,176,0,.45);border-radius:var(--r-sm);padding:1px 6px;letter-spacing:.06em;margin-left:8px;vertical-align:1px}
/* ===== 页面骨架(复刻线上组件) ===== */
.backlink{color:var(--brand);font-size:12px;margin-bottom:12px;display:inline-block;font-family:var(--mono);letter-spacing:.03em}
.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;margin-bottom:14px;flex-wrap:wrap}
.page-title{font-size:18px;font-weight:700;letter-spacing:.04em;margin:0}
.page-sub{color:var(--text-2);font-size:12px;margin:2px 0 0;font-family:var(--mono);letter-spacing:.02em}
.fcard{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--r-lg);padding:14px 16px;margin-bottom:12px}
.fcard .hd{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.fname{font-family:var(--mono);font-size:17px;font-weight:700;color:var(--brand);letter-spacing:.03em}
.expr{font-family:var(--mono);font-size:11.5px;line-height:1.7;background:var(--panel);border:1px solid var(--border-2);border-left:2px solid #00b8cc;border-radius:var(--r-sm);padding:10px 14px;color:var(--text-2);overflow-x:auto;white-space:pre;margin-top:8px}
.cat-chip{display:inline-block;padding:1px 8px;border-radius:var(--r-sm);font-family:var(--mono);font-size:10.5px;font-weight:600;line-height:17px;border:1px solid transparent}
.cat-alpha101{color:var(--brand);background:rgba(0,229,255,.1);border-color:rgba(0,229,255,.3)}
.cat-alpha158{color:var(--purple-dim);background:rgba(180,140,255,.1);border-color:rgba(180,140,255,.3)}
.cat-builtin{color:var(--amber);background:rgba(255,176,0,.1);border-color:rgba(255,176,0,.3)}
.st{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em}
.lamp{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}
.st-ok .lamp{box-shadow:0 0 6px rgba(46,230,138,.7)}
.st-watch .lamp{box-shadow:0 0 6px rgba(255,176,0,.7)}
.pos{color:var(--up)}
.neg{color:var(--down)}
/* ===== 指标条 ===== */
.metric-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden;margin-bottom:12px}
.metric{background:var(--bg-card);padding:10px 12px;min-width:0}
.metric .k{color:var(--text-3);font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap}
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:17px;font-weight:600;margin-top:2px;white-space:nowrap}
@media(max-width:1080px){.metric-strip{grid-template-columns:repeat(4,1fr)}}
/* ===== 周期开关 / 图表卡 ===== */
.period-switch{display:flex;border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden}
.period-switch button{font-family:var(--mono);font-size:11px;padding:4px 12px;color:var(--text-2);background:transparent;border:none;cursor:pointer}
.period-switch button.on{background:rgba(0,229,255,.12);color:var(--brand)}
.charts{display:grid;grid-template-columns:3fr 2fr;gap:12px;margin-bottom:12px}
.two-half{grid-template-columns:1fr 1fr}
@media(max-width:980px){.charts{grid-template-columns:1fr}}
.chart-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--r-lg);padding:12px 14px;min-width:0}
.chart-card h3{font-size:12.5px;font-weight:600;letter-spacing:.04em;margin:0}
.chart-card .cs{color:var(--text-3);font-family:var(--mono);font-size:10.5px;margin:3px 0 8px}
.cbody{width:100%}
svg{display:block}
svg text{font-family:var(--mono)}
.actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{font-size:12.5px;font-weight:600;border-radius:var(--r-md);padding:7px 16px;border:1px solid var(--border);color:var(--text-2);background:var(--bg-card);transition:all .15s var(--ease);cursor:pointer}
.btn:hover{color:var(--text);border-color:var(--text-3)}
.btn.primary{color:var(--text);background:var(--brand);border-color:var(--brand);box-shadow:0 0 0 1px rgba(0,229,255,.22),0 0 18px rgba(0,229,255,.08)}
.hint-inline{font-family:var(--mono);font-size:10.5px;color:var(--text-3)}
.hint{color:var(--text-3);font-size:11.5px;margin:10px 2px 0}
/* ===== 表格 ===== */
.tblwrap{overflow-x:auto}
.tbl{width:100%;border-collapse:collapse;font-size:12px}
.tbl th{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-3);text-align:left;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg-hover);white-space:nowrap}
.tbl td{padding:8px 10px;border-bottom:1px solid var(--border-2);white-space:nowrap}
.tbl tr:hover td{background:var(--bg-hover)}
.dim{color:var(--text-3)}
.cy{color:var(--brand)}
.fname-s{color:var(--brand)}
.addbtn{font-family:var(--mono);font-size:10.5px;padding:2px 10px;border-radius:var(--r-sm);border:1px solid var(--border);background:transparent;color:var(--text-2);cursor:pointer;transition:all .15s var(--ease)}
.addbtn:hover{color:var(--text);border-color:var(--text-3)}
.addbtn.on{color:var(--brand);border-color:rgba(0,229,255,.4);background:var(--cyan-soft)}
/* ===== 对比托盘 ===== */
.tray{display:flex;align-items:center;gap:10px;margin-top:12px;border:1px dashed rgba(0,229,255,.35);background:rgba(0,229,255,.05);border-radius:var(--r-md);padding:10px 12px;flex-wrap:wrap}
.tray .tk{font-size:10.5px;color:var(--brand);letter-spacing:.1em}
.tray .chips{display:flex;gap:6px;flex-wrap:wrap;flex:1}
.tchip{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;border:1px solid var(--border);border-radius:var(--r-sm);padding:2px 8px;background:var(--panel)}
.tchip .x{cursor:pointer;color:var(--text-3)}
.tchip .x:hover{color:var(--danger)}
/* ===== 对比页 ===== */
.chipsrow{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.cmp-tbl{width:100%;border-collapse:collapse;font-size:12.5px}
.cmp-tbl th,.cmp-tbl td{padding:9px 12px;border-bottom:1px solid var(--border-2);text-align:right;font-variant-numeric:tabular-nums}
.cmp-tbl th:first-child,.cmp-tbl td:first-child{text-align:left}
.cmp-tbl thead th{font-family:var(--mono);font-size:11.5px;letter-spacing:.03em;background:var(--bg-hover);border-bottom:1px solid var(--border)}
.cmp-tbl td.rk{color:var(--text-3);font-family:var(--mono);font-size:11px}
.cmp-tbl td.best{color:var(--brand);font-weight:700}
.minis{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}
.chart-card.mini h3{font-family:var(--mono);font-size:12px}
.h3c{font-size:10px;margin-left:6px}
.cmx{display:grid;gap:3px;margin-top:6px}
.cmx .cl{font-family:var(--mono);font-size:10.5px;text-align:center;padding:4px 2px;color:var(--text-2)}
.cmx .cc{font-family:var(--mono);font-size:11px;text-align:center;padding:9px 2px;border-radius:var(--r-sm);border:1px solid var(--border-2);font-variant-numeric:tabular-nums}
/* ===== 说明页 ===== */
.nh{font-size:13.5px;font-weight:700;letter-spacing:.03em;margin:20px 0 8px;color:var(--text)}
.nh:first-child{margin-top:0}
.ask{margin:6px 0 0;padding-left:20px;color:var(--text-2)}
.ask li{margin:6px 0}
.foot{color:var(--text-3);font-family:var(--mono);font-size:10.5px;text-align:center;margin-top:26px}
</style>
<div class="wrap">
<div class="topbar">
<div class="t">三国量化研究台 <span class="dim">· 因子工作台 设计稿 v1 · 2026-08-28</span></div>
<nav class="tabs">
<button class="on" data-tab="tears">① tears 报告页</button>
<button data-tab="compare">② 加入对比</button>
<button data-tab="notes">③ 实现说明</button>
</nav>
</div>
<!-- ============ 设计① tears 报告页 ============ -->
<section id="tab-tears" class="tab on">
<div class="dcap"><span class="no">设计 ①</span><span class="txt"><b>完整 tears 报告页</b> · 终端风原生图表重构,替代当前 iframe 里的浅色 alphalens 报告 · 入口:因子详情「查看完整 tears 报告」/ 分析任务完成页直达 · 右上 1D/5D/10D 全页联动(可点)</span></div>
<div class="mock">
<span class="backlink">← 返回分析结果</span>
<div class="page-head">
<div><h1 class="page-title">因子 tears · alpha16</h1><p class="page-sub">task_8f3a2c · 全A抽样 800 标的 · 2022-01-01 ~ 2024-06-30 · 日频 RankIC</p></div>
<div class="period-switch"><button data-tp="1">1D</button><button class="on" data-tp="5">5D</button><button data-tp="10">10D</button></div>
</div>
<div class="fcard">
<div class="hd"><span class="fname">alpha16</span><span class="cat-chip cat-alpha101">alpha101</span><span class="st st-ok"><span class="lamp" style="background:var(--lamp-ok)"></span>有效</span></div>
<div class="expr">(-1 * ts_rank(rank(correlation(rank(high), rank(volume), 3)), 3))</div>
</div>
<div class="metric-strip" id="t-strip"></div>
<div class="charts">
<div class="chart-card"><h3>月度 IC 与累计 IC</h3><div class="cs">柱=月度 RankIC(红正绿负)· 线=累计 IC(右轴)</div><div class="cbody" id="t-ic"></div></div>
<div class="chart-card"><h3>月度 IC 热力图<span class="new-tag">新增</span></h3><div class="cs">红=正预测力 · 绿=负 · 看月度稳定性与失效期</div><div class="cbody" id="t-heat"></div></div>
</div>
<div class="charts">
<div class="chart-card"><h3>分组累计净值 Q1 / Q5 / Q10</h3><div class="cs">按因子值十分组回测净值 · 单调分层=因子有效</div><div class="cbody" id="t-ql"></div></div>
<div class="chart-card"><h3>多空净值 Q10 Q1<span class="new-tag">新增</span></h3><div class="cs">做多最高组做空最低组 · ○=最大回撤点</div><div class="cbody" id="t-ls"></div></div>
</div>
<div class="charts two-half">
<div class="chart-card"><h3 id="t-dec-title">十分组年化收益 (5D)</h3><div class="cs">Q1=因子值最低组 · 单调性=区分度</div><div class="cbody" id="t-dec"></div></div>
<div class="chart-card"><h3>IC 衰减<span class="new-tag">新增</span></h3><div class="cs">持有期拉长后的 IC 均值 · 衰减快=适合短持有</div><div class="cbody" id="t-decay"></div></div>
</div>
<div class="actions"><button class="btn">返回分析任务</button><button class="btn">导出报告 PNG</button><span class="hint-inline">指标条与十分组图随周期开关联动</span></div>
</div>
</section>
<!-- ============ 设计② 加入对比 ============ -->
<section id="tab-compare" class="tab">
<div class="dcap"><span class="no">设计 ②</span><span class="txt"><b>「加入对比」做实</b> · 排行榜行内多选 + 底部托盘 + 对比页 · 本段可交互:点行内「+对比 / ✓ 已选」试试,托盘随动,「对比 N 因子 →」跳到对比页</span></div>
<div class="mock">
<div class="page-head">
<div><h1 class="page-title">IC 排行榜 · 行内多选</h1><p class="page-sub">run ev_20260825_211248 · 全A 5,214 标的 · 2018-01 ~ 2026-06 · 5D</p></div>
</div>
<div class="tblwrap"><table class="tbl">
<thead><tr><th>#</th><th>因子</th><th>类别</th><th>IC 均值</th><th>ICIR</th><th>结论</th><th>对比</th></tr></thead>
<tbody id="lb-body"></tbody>
</table></div>
<div class="tray">
<span class="mono tk">对比托盘</span>
<div class="chips" id="tray-chips"></div>
<button class="btn primary" id="tray-go">对比 3 因子 →</button>
</div>
<p class="hint">详情页入口:按钮「加入对比」点击后变「已加入 ✓」;托盘全局悬浮于页面底部,2~6 个因子随时进对比页。</p>
</div>
<div class="mock" id="cmp-page" style="margin-top:14px">
<div class="page-head">
<div><h1 class="page-title">因子对比</h1><p class="page-sub">同批次同口径并排 · 批次 ev_20260825_211248 · 2018-01 ~ 2026-06</p></div>
<div class="period-switch"><button data-cp="1">1D</button><button class="on" data-cp="5">5D</button><button data-cp="10">10D</button></div>
</div>
<div class="chipsrow" id="cmp-chips"></div>
<div class="chart-card" style="margin-bottom:12px"><h3>指标并排<span class="new-tag">行最优青色高亮</span></h3><div class="tblwrap" id="cmp-tbl" style="margin-top:8px"></div></div>
<div class="charts">
<div class="chart-card"><h3>累计 IC 叠加</h3><div class="cs">各因子累计 RankIC 曲线 · 斜率=预测力</div><div class="cbody" id="cmp-overlay"></div></div>
<div class="chart-card"><h3>IC 序列相关性<span class="new-tag">新增</span></h3><div class="cs">月度 IC 序列 Pearson · 高相关=同质因子,为去重铺路</div><div class="cbody" id="cmp-cmx"></div></div>
</div>
<div class="minis" id="cmp-minis"></div>
</div>
</section>
<!-- ============ 实现说明 ============ -->
<section id="tab-notes" class="tab">
<div class="dcap"><span class="no">说明</span><span class="txt">两处设计的数据来源、实现路径与工作量 · <b>两项待拍板</b></span></div>
<div class="mock">
<h3 class="nh">tears 报告页 · 三种实现路径</h3>
<div class="tblwrap"><table class="tbl">
<thead><tr><th>方案</th><th>做法</th><th>视觉</th><th>交互</th><th>工作量</th></tr></thead>
<tbody>
<tr><td class="mono cy">A 原生重构(本稿①)</td><td>服务端把 alphalens 已算的分层序列(日度IC / 分组净值 / 多空净值 / 换手)序列化成 JSON 端点,前端 ECharts 暗色渲染</td><td>与全站完全统一</td><td>周期切换全页联动、tooltip、导出 PNG</td><td class="mono">后端端点 + 前端页 ≈ 2~3 天</td></tr>
<tr><td class="mono">B matplotlib 暗化</td><td>tears 生成端统一套暗色 rcParams,仍走 iframe 内嵌</td><td>暗底但仍是 matplotlib 风,字体/气质仍割裂</td><td>维持现状(无联动)</td><td class="mono">≈ 半天</td></tr>
<tr><td class="mono">C 终端壳包 iframe</td><td>iframe 外面包终端风卡片框,报告本体不动</td><td>壳统一、内里割裂</td><td></td><td class="mono">≈ 0.5 天</td></tr>
</tbody>
</table></div>
<p class="hint">推荐 <b class="cy">A</b>:要的是终态一致;B 只是把白底换黑底,图表气质仍是 matplotlib。B 也可作为先行止血项,随后 A 替换。</p>
<h3 class="nh">加入对比(设计②)</h3>
<div class="tblwrap"><table class="tbl">
<thead><tr><th style="width:110px"></th><th>说明</th></tr></thead>
<tbody>
<tr><td class="mono">数据来源</td><td>纯前端:现有 <span class="mono">GET /factor/eval/detail</span> × N 个因子(指标 / 月度IC / 十分组全在返回里);相关性 = 月度 IC 序列 Pearson,前端可算,零后端改动</td></tr>
<tr><td class="mono">入口</td><td>排行榜行内「+对比」/ 详情页「加入对比」按钮(加入后变「已加入 ✓」)/ 底部全局托盘(2~6 个)</td></tr>
<tr><td class="mono">工作量</td><td class="mono">≈ 1 天</td></tr>
<tr><td class="mono">衔接</td><td>相关性格子即后续「62 个 effective 因子相关性去重分析」的页面雏形,做了不白做</td></tr>
</tbody>
</table></div>
<h3 class="nh">待拍板</h3>
<ol class="ask">
<li>tears 报告走 <b class="cy">A(原生重构,2~3 天)</b>、B(半天暗化先行)还是 C?我的建议:A,一步到位。</li>
<li>对比页按本稿②范围确认即可开工(指标并排 + 累计 IC + 相关性 + 十分组小倍数)。</li>
</ol>
</div>
</section>
<p class="foot">图表数据为种子化生成的示意值(形态对齐 08-25 批次真实结果)· 线上以接口真实数据为准</p>
</div>
<script>
'use strict'
/* ---------- 工具 ---------- */
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
function makeGauss(rnd){var sp=null;return function(){if(sp!==null){var v=sp;sp=null;return v}var u=0,v=0;while(u===0)u=rnd();while(v===0)v=rnd();var m=Math.sqrt(-2*Math.log(u)),a=2*Math.PI*v;sp=m*Math.sin(a);return m*Math.cos(a)}}
function $(s){return document.querySelector(s)}
var AX='#4a5868',GRID='#16202e',UP='#ff4d5e',DN='#2ee68a',CY='#00e5ff'
function pct(v,d){if(d===undefined)d=1;return(v>=0?'+':'')+(v*100).toFixed(d)+'%'}
function num(v,d){return v.toFixed(d)}
function sgn(v){return v>=0?'pos':'neg'}
/* ---------- 示意数据(种子化生成,形态对齐真实批次) ---------- */
function buildMonths(){var o=[],y,m;for(y=2022;y<=2024;y++)for(m=1;m<=12;m++){if(y===2024&&m>6)break;o.push(y+'-'+(m<10?'0':'')+m)}return o}
var M=buildMonths()
var G1=makeGauss(mulberry32(20260828))
var icT=M.map(function(_,i){var v=0.055+0.085*G1();if(i>=8&&i<=11)v-=0.07;if(i>=26)v+=0.035;return Math.round(v*1000)/1000})
var cc=0,cumT=icT.map(function(v){return cc+=v})
var G2=makeGauss(mulberry32(777))
var c10=1,c5=1,c1=1,q10=[],q5=[],q1=[]
M.forEach(function(_,i){
var r10=0.020+0.013*G2(),r5=0.005+0.009*G2(),r1=-0.0065+0.011*G2()
if(i>=8&&i<=11){r10-=0.035;r1+=0.02}
c10*=1+r10;c5*=1+r5;c1*=1+r1;q10.push(c10);q5.push(c5);q1.push(c1)
})
var lsR=[],i2
for(i2=0;i2<M.length;i2++){var a2=i2===0?q10[0]:q10[i2]/q10[i2-1],b2=i2===0?q1[0]:q1[i2]/q1[i2-1];lsR.push(a2-b2+0.001)}
var cl=1,lsCum=lsR.map(function(r){return cl*=1+r})
var TEARS={metrics:{
'1':{ic:.048,icir:.58,t:21.3,win:.662,ls:.315,dd:-.087,ac:.94},
'5':{ic:.065,icir:.66,t:29.9,win:.684,ls:.423,dd:-.121,ac:.94},
'10':{ic:.071,icir:.61,t:25.6,win:.670,ls:.489,dd:-.149,ac:.94}},
dec:{'1':[-.078,-.059,-.041,-.024,-.009,.005,.019,.036,.054,.079],
'5':[-.096,-.072,-.050,-.031,-.014,.007,.028,.049,.070,.098],
'10':[-.104,-.080,-.058,-.038,-.019,.006,.031,.054,.078,.109]}}
var FCOL={alpha16:'#00e5ff',resi_5:'#b48cff',roc_20:'#ffb000',std_20:'#5f8fd9',KLEN:'#d98fd9'}
function genCum(seed,drift,sd){var g=makeGauss(mulberry32(seed)),c=0;return M.map(function(){return c+=drift+sd*g()})}
var CUMS={alpha16:cumT,resi_5:genCum(31,.05,.10),roc_20:genCum(32,.03,.11),std_20:genCum(33,-.04,.07),KLEN:genCum(34,-.01,.08)}
var CORR={'alpha16|resi_5':.42,'alpha16|roc_20':.18,'alpha16|std_20':.35,'alpha16|KLEN':.06,'resi_5|roc_20':.27,'resi_5|std_20':.31,'resi_5|KLEN':.11,'roc_20|std_20':.22,'roc_20|KLEN':.09,'std_20|KLEN':.13}
function corrOf(a,b){if(a===b)return 1;var v=CORR[a+'|'+b];if(v===undefined)v=CORR[b+'|'+a];return v===undefined?0:v}
var CMP=[
{name:'alpha16',cat:'alpha101',concl:'effective',dec5:[-.096,-.072,-.050,-.031,-.014,.007,.028,.049,.070,.098],
m:{'1':{ic:.048,icir:.58,t:21.3,win:.662,ls:.315,to:.081},'5':{ic:.065,icir:.66,t:29.9,win:.684,ls:.423,to:.081},'10':{ic:.071,icir:.61,t:25.6,win:.670,ls:.489,to:.081}}},
{name:'resi_5',cat:'alpha158',concl:'effective',dec5:[-.118,-.089,-.060,-.034,-.011,.012,.036,.064,.097,.129],
m:{'1':{ic:.041,icir:.47,t:18.9,win:.638,ls:.612,to:.124},'5':{ic:.058,icir:.52,t:23.1,win:.649,ls:.991,to:.124},'10':{ic:.062,icir:.48,t:20.3,win:.641,ls:.884,to:.124}}},
{name:'roc_20',cat:'alpha158',concl:'watch',dec5:[-.032,-.019,-.008,.001,.007,.010,.012,.014,.019,.030],
m:{'1':{ic:.022,icir:.19,t:8.4,win:.557,ls:.118,to:.158},'5':{ic:.036,icir:.31,t:13.8,win:.602,ls:.225,to:.158},'10':{ic:.039,icir:.28,t:11.9,win:.593,ls:.261,to:.158}}},
{name:'std_20',cat:'alpha158',concl:'effective',dec5:[.021,.016,.011,.007,.003,-.002,-.006,-.011,-.016,-.023],
m:{'1':{ic:-.031,icir:-.36,t:-15.7,win:.598,ls:.187,to:.062},'5':{ic:-.041,icir:-.38,t:-16.4,win:.601,ls:.243,to:.062},'10':{ic:-.044,icir:-.35,t:-14.8,win:.594,ls:.271,to:.062}}},
{name:'KLEN',cat:'builtin',concl:'watch',dec5:[-.006,.004,-.002,.007,.001,-.003,.005,-.001,.003,-.008],
m:{'1':{ic:-.008,icir:-.07,t:-3.1,win:.517,ls:.041,to:.141},'5':{ic:-.012,icir:-.09,t:-3.8,win:.521,ls:.064,to:.141},'10':{ic:-.014,icir:-.08,t:-3.4,win:.524,ls:.078,to:.141}}}
]
var LB_ROWS=[CMP[0],CMP[1],CMP[3],CMP[2],CMP[4]]
var STATE={tp:'5',cp:'5',sel:['alpha16','resi_5','roc_20']}
/* ---------- SVG 图表 ---------- */
function svgo(W,H){return '<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;height:auto" role="img">'}
function tx(x,y,s,fill,fs,an){return '<text x="'+x+'" y="'+y+'" fill="'+(fill||AX)+'" font-size="'+(fs||9)+'" text-anchor="'+(an||'middle')+'">'+s+'</text>'}
function chIC(ics){
var W=660,H=226,pl=36,pr=48,pt=18,pb=24,pw=W-pl-pr,ph=H-pt-pb,n=ics.length,i
var mx=Math.max(.12,Math.max.apply(null,ics.map(Math.abs)))*1.15
var cum=[],c=0;ics.forEach(function(v){c+=v;cum.push(c)})
var cmax=Math.max(.5,Math.max.apply(null,cum))*1.06
function y1(v){return pt+ph/2-v/mx*(ph/2)}
function y2(v){return pt+ph-Math.max(0,v)/cmax*ph}
function x(i){return pl+(i+.5)*pw/n}
var s=svgo(W,H)
;[-.08,0,.08].forEach(function(t){s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+y1(t).toFixed(1)+'" y2="'+y1(t).toFixed(1)+'" stroke="'+GRID+'"'+(t===0?'':' stroke-dasharray="3 4"')+'/>'})
s+=tx(pl-5,y1(.08)+3,'.08',AX,9,'end')+tx(pl-5,y1(0)+3,'0',AX,9,'end')+tx(pl-5,y1(-.08)+3,'-.08',AX,9,'end')
s+=tx(W-pr+5,y2(cmax)+3,num(cmax,1),CY,9,'start')+tx(W-pr+5,y2(0)+3,'0',CY,9,'start')
var bw=Math.max(3,pw/n-1.5)
ics.forEach(function(v,i){var y0=y1(0),y=y1(v)
s+='<rect x="'+(x(i)-bw/2).toFixed(1)+'" y="'+Math.min(y,y0).toFixed(1)+'" width="'+bw.toFixed(1)+'" height="'+Math.abs(y-y0).toFixed(1)+'" fill="'+(v>=0?UP:DN)+'" opacity=".85"/>'})
s+='<polyline points="'+cum.map(function(v,i){return x(i).toFixed(1)+','+y2(v).toFixed(1)}).join(' ')+'" fill="none" stroke="'+CY+'" stroke-width="1.8"/>'
M.forEach(function(m,i){if(i%4===0)s+=tx(x(i),H-8,m.slice(2),AX,9)})
s+='<rect x="'+pl+'" y="4" width="8" height="8" fill="'+UP+'"/>'+tx(pl+12,11,'月度IC',AX,9,'start')
s+='<line x1="'+(pl+58)+'" x2="'+(pl+70)+'" y1="8" y2="8" stroke="'+CY+'" stroke-width="1.8"/>'+tx(pl+74,11,'累计IC·右轴',AX,9,'start')
return s+'</svg>'
}
function chHeat(){
var yMap={'2022':icT.slice(0,12),'2023':icT.slice(12,24),'2024':icT.slice(24).concat([null,null,null,null,null,null])}
var cw=40,chh=24,g=2,lw=36,th=16,j
var W=lw+12*(cw+g)+g,H=th+3*(chh+g)+4
var s=svgo(W,H)
for(j=0;j<12;j++)s+=tx(lw+j*(cw+g)+cw/2,11,String(j+1),AX,8.5)
Object.keys(yMap).forEach(function(y,i){
s+=tx(lw-8,th+i*(chh+g)+chh/2+3,y,AX,9,'end')
yMap[y].forEach(function(v,j){
var X=lw+j*(cw+g),Y=th+i*(chh+g)
if(v===null){s+='<rect x="'+X+'" y="'+Y+'" width="'+cw+'" height="'+chh+'" rx="1.5" fill="#070b12" stroke="#0f1822"/>';return}
var a=Math.min(1,Math.abs(v)/.16)
s+='<rect x="'+X+'" y="'+Y+'" width="'+cw+'" height="'+chh+'" rx="1.5" fill="'+(v>=0?'rgba(255,77,94,':'rgba(46,230,138,')+(0.08+a*.72).toFixed(2)+')"/>'
var t2=(Math.round(v*100)/100).toFixed(2).replace('0.','.')
s+=tx(X+cw/2,Y+chh/2+3,v<0?'-'+t2.replace('-',''):t2,a>.55?'#05080d':'#7a8a9a',8.5)
})})
return s+'</svg>'
}
function chLines(series,yFmt){
var W=660,H=230,pl=44,pr=88,pt=14,pb=22,pw=W-pl-pr,ph=H-pt-pb,k,i
var all=[];series.forEach(function(sr){all=all.concat(sr.vals)})
var mn=Math.min.apply(null,all),mx=Math.max.apply(null,all),pd=(mx-mn)*.1||1;mn-=pd;mx+=pd
function X(i){return pl+i/(M.length-1)*pw}
function Y(v){return pt+(mx-v)/(mx-mn)*ph}
var s=svgo(W,H)
for(k=0;k<=3;k++){var v=mn+(mx-mn)*k/3,yy=Y(v).toFixed(1)
s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+yy+'" y2="'+yy+'" stroke="'+GRID+'"/>'+tx(pl-6,+yy+3,yFmt(v),AX,9,'end')}
M.forEach(function(m,i){if(i%4===0)s+=tx(X(i),H-7,m.slice(2),AX,9)})
series.forEach(function(sr){
s+='<polyline points="'+sr.vals.map(function(v,i){return X(i).toFixed(1)+','+Y(v).toFixed(1)}).join(' ')+'" fill="none" stroke="'+sr.color+'" stroke-width="1.8"/>'
var lv=sr.vals[sr.vals.length-1]
s+=tx(W-pr+6,Y(lv)+3,sr.name+' '+yFmt(lv),sr.color,9.5,'start')})
return s+'</svg>'
}
function chLS(){
var W=660,H=230,pl=44,pr=16,pt=14,pb=22,pw=W-pl-pr,ph=H-pt-pb,k
var mn=Math.min.apply(null,lsCum),mx=Math.max.apply(null,lsCum),pd=(mx-mn)*.12||1
mn=Math.min(mn-pd,0.985);mx+=pd
function X(i){return pl+i/(M.length-1)*pw}
function Y(v){return pt+(mx-v)/(mx-mn)*ph}
var peak=-1,pi=0,ti=0,ddi=0,tdd=0
lsCum.forEach(function(v,i){if(v>peak){peak=v;pi=i}var dd=v/peak-1;if(dd<tdd){tdd=dd;ddi=i;ti=pi}})
var s=svgo(W,H)
for(k=0;k<=3;k++){var v=mn+(mx-mn)*k/3,yy=Y(v).toFixed(1)
s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+yy+'" y2="'+yy+'" stroke="'+GRID+'"/>'+tx(pl-6,+yy+3,(v*100).toFixed(0)+'%',AX,9,'end')}
M.forEach(function(m,i){if(i%4===0)s+=tx(X(i),H-7,m.slice(2),AX,9)})
var pts=lsCum.map(function(v,i){return X(i).toFixed(1)+','+Y(v).toFixed(1)})
s+='<polygon points="'+pl+','+Y(mn).toFixed(1)+' '+pts.join(' ')+' '+(W-pr)+','+Y(mn).toFixed(1)+'" fill="rgba(255,176,0,0.10)"/>'
s+='<polyline points="'+pts.join(' ')+'" fill="none" stroke="#ffb000" stroke-width="1.8"/>'
s+='<circle cx="'+X(ddi).toFixed(1)+'" cy="'+Y(lsCum[ddi]).toFixed(1)+'" r="3" fill="none" stroke="'+DN+'" stroke-width="1.2"/>'
s+=tx(X(ddi),Y(lsCum[ddi])-8,'最大回撤 '+pct(tdd)+' · '+M[ti].slice(2)+'~'+M[ddi].slice(2),DN,9.5)
return s+'</svg>'
}
function chDec(vals,small){
var W=small?300:660,H=small?150:212,pl=small?26:40,pr=8,pt=small?12:16,pb=20,pw=W-pl-pr,ph=H-pt-pb,i
var mx=Math.max(.02,Math.max.apply(null,vals.map(Math.abs)))*1.18
var y0=pt+ph/2
function Y(v){return pt+ph/2-v/mx*(ph/2)}
var s=svgo(W,H)
s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+y0+'" y2="'+y0+'" stroke="'+GRID+'"/>'
if(!small){s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+Y(.05).toFixed(1)+'" y2="'+Y(.05).toFixed(1)+'" stroke="'+GRID+'" stroke-dasharray="3 4"/>'
s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+Y(-.05).toFixed(1)+'" y2="'+Y(-.05).toFixed(1)+'" stroke="'+GRID+'" stroke-dasharray="3 4"/>'
s+=tx(pl-5,Y(.05)+3,'+5%',AX,9,'end')+tx(pl-5,Y(-.05)+3,'-5%',AX,9,'end')}
var bw=pw/10*.55
vals.forEach(function(v,i){var cx=pl+(i+.5)*pw/10,y=Y(v)
s+='<rect x="'+(cx-bw/2).toFixed(1)+'" y="'+Math.min(y,y0).toFixed(1)+'" width="'+bw.toFixed(1)+'" height="'+Math.abs(y-y0).toFixed(1)+'" fill="'+(v>=0?UP:DN)+'"/>'
s+=tx(cx,H-7,'D'+(i+1),AX,small?8:9)
if(!small)s+=tx(cx,v>=0?y-4:y+11,pct(v),v>=0?UP:DN,8.5)})
return s+'</svg>'
}
function chDecay(){
var W=300,H=150,pl=34,pr=8,pt=16,pb=20,pw=W-pl-pr,ph=H-pt-pb
var items=[['1D',TEARS.metrics['1'].ic],['5D',TEARS.metrics['5'].ic],['10D',TEARS.metrics['10'].ic]]
var mx=.085,y0=pt+ph/2
function Y(v){return pt+ph/2-v/mx*(ph/2)}
var s=svgo(W,H)
s+='<line x1="'+pl+'" x2="'+(W-pr)+'" y1="'+y0+'" y2="'+y0+'" stroke="'+GRID+'"/>'
s+=tx(pl-5,Y(.06)+3,'.06',AX,9,'end')+tx(pl-5,y0+3,'0',AX,9,'end')
var bw=pw/3*.4
items.forEach(function(it,i){var cx=pl+(i+.5)*pw/3,y=Y(it[1])
s+='<rect x="'+(cx-bw/2).toFixed(1)+'" y="'+y.toFixed(1)+'" width="'+bw.toFixed(1)+'" height="'+(y0-y).toFixed(1)+'" fill="rgba(0,229,255,0.7)"/>'
s+=tx(cx,y-5,num(it[1],3),CY,9)+tx(cx,H-7,it[0],AX,9)})
return s+'</svg>'
}
/* ---------- 渲染 ---------- */
function chip(cat){return '<span class="cat-chip cat-'+cat+'">'+cat+'</span>'}
function lamp(c){var m={effective:['有效','st-ok','var(--lamp-ok)'],watch:['观察','st-watch','var(--lamp-warn)'],eliminated:['淘汰','st-elim','var(--lamp-idle)']},e=m[c]
return '<span class="st '+e[1]+'"><span class="lamp" style="background:'+e[2]+'"></span>'+e[0]+'</span>'}
function renderTears(){
var p=STATE.tp,m=TEARS.metrics[p]
$('#t-strip').innerHTML=[
['IC 均值 ('+p+'D)',num(m.ic,3),sgn(m.ic)],
['ICIR',num(m.icir,2),sgn(m.icir)],
['t 值',num(m.t,1),sgn(m.t)],
['IC 胜率',num(m.win*100,1)+'%',''],
['多空年化',pct(m.ls),sgn(m.ls)],
['多空最大回撤',pct(m.dd),'neg'],
['因子自相关',num(m.ac,2),'']
].map(function(x){return '<div class="metric"><div class="k">'+x[0]+'</div><div class="v '+x[2]+'">'+x[1]+'</div></div>'}).join('')
$('#t-ic').innerHTML=chIC(icT)
$('#t-heat').innerHTML=chHeat()
$('#t-ql').innerHTML=chLines([
{name:'Q10 最高组',color:UP,vals:q10},
{name:'Q5',color:'#7a8a9a',vals:q5},
{name:'Q1 最低组',color:DN,vals:q1}],function(v){return(v*100).toFixed(0)+'%'})
$('#t-ls').innerHTML=chLS()
$('#t-dec-title').textContent='十分组年化收益 ('+p+'D)'
$('#t-dec').innerHTML=chDec(TEARS.dec[p],false)
$('#t-decay').innerHTML=chDecay()
}
function renderLB(){
$('#lb-body').innerHTML=LB_ROWS.map(function(f,i){
var sel=STATE.sel.indexOf(f.name)>=0,m=f.m['5']
return '<tr><td class="mono dim">'+(i+1)+'</td><td class="mono fname-s">'+f.name+'</td><td>'+chip(f.cat)+
'</td><td class="mono '+sgn(m.ic)+'">'+num(m.ic,3)+'</td><td class="mono '+sgn(m.icir)+'">'+num(m.icir,2)+
'</td><td>'+lamp(f.concl)+'</td><td><button class="addbtn'+(sel?' on':'')+'" data-add="'+f.name+'">'+(sel?'✓ 已选':'+对比')+'</button></td></tr>'}).join('')
}
function renderTray(){
$('#tray-chips').innerHTML=STATE.sel.map(function(f){
return '<span class="tchip" style="border-color:'+FCOL[f]+'66"><span style="color:'+FCOL[f]+'">●</span>'+f.name+
(STATE.sel.length>2?'<span class="x" data-f="'+f+'" title="移除">✕</span>':'')+'</span>'}).join('')+
'<span class="hint-inline">2~6 个</span>'
$('#tray-go').textContent='对比 '+STATE.sel.length+' 因子 →'
}
function cmxHTML(names){
var h='<div class="cmx" style="grid-template-columns:64px repeat('+names.length+',1fr)">'
h+='<div></div>'+names.map(function(n){return '<div class="cl" style="color:'+FCOL[n]+'">'+n+'</div>'}).join('')
names.forEach(function(a){
h+='<div class="cl" style="color:'+FCOL[a]+'">'+a+'</div>'
names.forEach(function(b){var c=corrOf(a,b),al=Math.min(1,Math.abs(c))
h+='<div class="cc" style="background:rgba(0,229,255,'+(0.04+al*.4).toFixed(2)+')">'+(a===b?'—':num(c,2))+'</div>'})})
return h+'</div>'
}
function renderCmp(){
var p=STATE.cp
var fs=CMP.filter(function(f){return STATE.sel.indexOf(f.name)>=0})
$('#cmp-chips').innerHTML=fs.map(function(f){
return '<span class="tchip" style="border-color:'+FCOL[f.name]+'66"><span style="color:'+FCOL[f.name]+'">●</span>'+f.name+' '+chip(f.cat)+'</span>'}).join('')+
'<span class="hint-inline">在排行榜行内 + 或详情页「加入对比」增删</span>'
var rows=[
{k:'结论',h:function(f){return lamp(f.concl)}},
{k:'IC 均值',h:function(f){return '<span class="'+sgn(f.m[p].ic)+'">'+num(f.m[p].ic,3)+'</span>'},g:function(f){return f.m[p].ic},b:'max'},
{k:'ICIR',h:function(f){return '<span class="'+sgn(f.m[p].icir)+'">'+num(f.m[p].icir,2)+'</span>'},g:function(f){return f.m[p].icir},b:'max'},
{k:'t 值',h:function(f){return '<span class="'+sgn(f.m[p].t)+'">'+num(f.m[p].t,1)+'</span>'},g:function(f){return Math.abs(f.m[p].t)},b:'max'},
{k:'IC 胜率',h:function(f){return num(f.m[p].win*100,1)+'%'},g:function(f){return f.m[p].win},b:'max'},
{k:'多空年化',h:function(f){return '<span class="'+sgn(f.m[p].ls)+'">'+pct(f.m[p].ls)+'</span>'},g:function(f){return f.m[p].ls},b:'max'},
{k:'日均换手',h:function(f){return num(f.m[p].to*100,1)+'%'},g:function(f){return f.m[p].to},b:'min'}]
var h='<table class="cmp-tbl"><thead><tr><th>指标('+p+'D</th>'+
fs.map(function(f){return '<th class="mcol" style="color:'+FCOL[f.name]+'">'+f.name+'</th>'}).join('')+'</tr></thead><tbody>'
rows.forEach(function(r){
var bi=-1,bv=null
if(r.g){bv=r.b==='min'?Infinity:-Infinity
fs.forEach(function(f,i){var v=r.g(f);if(r.b==='min'?v<bv:v>bv){bv=v;bi=i}})}
h+='<tr><td class="rk">'+r.k+'</td>'+fs.map(function(f,i){
return '<td class="'+(i===bi?'best':'')+'">'+r.h(f)+'</td>'}).join('')+'</tr>'})
h+='</tbody></table>'
$('#cmp-tbl').innerHTML=h
$('#cmp-overlay').innerHTML=chLines(fs.map(function(f){
return {name:f.name,color:FCOL[f.name],vals:CUMS[f.name]}}),function(v){return v.toFixed(1)})
$('#cmp-cmx').innerHTML=cmxHTML(fs.map(function(f){return f.name}))
var k=p==='1'?.78:p==='10'?1.12:1
$('#cmp-minis').innerHTML=fs.map(function(f){
return '<div class="chart-card mini"><h3>'+f.name+'<span class="h3c" style="color:'+FCOL[f.name]+'">'+p+'D</span></h3>'+
'<div class="cs">十分组年化 · 越单调区分度越好</div><div class="cbody">'+chDec(f.dec5.map(function(v){return v*k}),true)+'</div></div>'}).join('')
}
/* ---------- 交互 ---------- */
document.querySelectorAll('.tabs button').forEach(function(b){
b.addEventListener('click',function(){
document.querySelectorAll('.tabs button').forEach(function(x){x.classList.toggle('on',x===b)})
document.querySelectorAll('.tab').forEach(function(t){t.classList.toggle('on',t.id==='tab-'+b.dataset.tab)})
})})
document.querySelectorAll('[data-tp]').forEach(function(b){
b.addEventListener('click',function(){
STATE.tp=b.dataset.tp
document.querySelectorAll('[data-tp]').forEach(function(x){x.classList.toggle('on',x===b)})
renderTears()})})
document.querySelectorAll('[data-cp]').forEach(function(b){
b.addEventListener('click',function(){
STATE.cp=b.dataset.cp
document.querySelectorAll('[data-cp]').forEach(function(x){x.classList.toggle('on',x===b)})
renderCmp()})})
$('#lb-body').addEventListener('click',function(e){
var b=e.target.closest('button[data-add]');if(!b)return
var f=b.dataset.add,i=STATE.sel.indexOf(f)
if(i>=0)STATE.sel.splice(i,1)
else if(STATE.sel.length<6)STATE.sel.push(f)
renderLB();renderTray();renderCmp()})
$('#tray-chips').addEventListener('click',function(e){
var x=e.target.closest('.x');if(!x||STATE.sel.length<=2)return
STATE.sel=STATE.sel.filter(function(v){return v!==x.dataset.f})
renderLB();renderTray();renderCmp()})
$('#tray-go').addEventListener('click',function(){
var el=$('#cmp-page')
el.scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth'})
el.classList.add('flash');setTimeout(function(){el.classList.remove('flash')},1200)})
renderTears();renderLB();renderTray();renderCmp()
</script>
</body>
</html>