wiki: Chinese alpha notes, and the GTJA 191 post re-measured

Alpha Library: the 191 notes, 117 nicknames and 10 theme tags now switch
to Chinese with the rest of the site. wiki/alpha-library/i18n.zh.json keeps
each translation next to the English it came from; the generator puts the
Chinese in data-text-zh only while that English still matches the
registry, and tests/factors/test_alpha_library_translations.py fails on a
missing, stale or orphaned translation or an unnamed theme.

GTJA 191 post: the 2026-05-17 numbers came from Tushare prices that were
not split-adjusted and from today's CSI 300 roster applied to the whole
window, both fixed on 2026-08-04 (d52199ff). Re-ran the bench on the
current code (point-in-time membership, qfq prices): 8 alive, 14 reversed,
168 dead, gtja191_138 untested (99% NaN), where the first version said
10 / 15 / 165 and never accounted for the 191st. Both languages now carry
a revision note and the new counts, theme table, top 5 by IR and three
most-negative alphas, and fix what the translation surfaced or the code
contradicts: the top 5 are ranked by IR, not IC; the theme table counts
every tag an alpha carries; the reflection matches the table; the Method
universe matches the Caveats; Kakushadze's preprint came a year after the
report; forward returns are simple, not log; no GTJA formula needs sector
tags; the bench measures no turnover and draws no IC plot; five alphas use
a proxy input (named); the reproduce command covers 2018-2025. A new
caveat reports the bench's multiple-testing deflation: 3 of the 8 alive
alphas clear the IR expected from 190 null formulas.
This commit is contained in:
Haozhe Wu committed 2026-09-29 18:36:52 +08:00
1 parent eb883c36d6
commit 36f1698e1f
19 files changed
+1599 -211

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
// Fills the Alpha Library landing stats from content/index.json, which the
// deploy workflow generates (wiki/scripts/build_alpha_library.py).
import { storedLang } from "/site.js?v=20260929d";
import { storedLang } from "/site.js?v=20260929e";
const TEXT = {
en: { across: (n) => `across ${n} zoos`, alphas: (n) => `${n} alphas`, missing: "manifest not generated yet" },
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -24,11 +24,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/alpha-library/alpha.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
<script type="module" src="/alpha-library/alpha.js?v=20260929e"></script>
<style>
.alpha-hero { width: min(1180px, calc(100% - 32px)); margin: 56px auto 32px; }
.alpha-hero h1 { font-family: var(--serif); font-size: clamp(2.2rem, 4vw, 3.4rem); margin: 8px 0 14px; letter-spacing: -0.01em; }
+2 -2
View File
@@ -1,5 +1,5 @@
import { DOCS_STRUCTURE as EN_STRUCTURE } from "/docs/content.en.js?v=20260929d";
import { DOCS_STRUCTURE as ZH_STRUCTURE } from "/docs/content.zh.js?v=20260929d";
import { DOCS_STRUCTURE as EN_STRUCTURE } from "/docs/content.en.js?v=20260929e";
import { DOCS_STRUCTURE as ZH_STRUCTURE } from "/docs/content.zh.js?v=20260929e";
// One content set per language; every version alias renders the latest.
export const DOCS_DEFAULT_VERSION = "0.1.16";
+4 -4
View File
@@ -21,11 +21,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<link rel="stylesheet" href="/docs/styles.css?v=20260929d">
<script type="module" src="/docs/main.js?v=20260929d"></script>
<link rel="stylesheet" href="/docs/styles.css?v=20260929e">
<script type="module" src="/docs/main.js?v=20260929e"></script>
</head>
<body class="docs-body" data-section="docs">
<!-- site-header:start -->
+2 -2
View File
@@ -1,11 +1,11 @@
import { initSite, setPageLang, storedLang } from "/site.js?v=20260929d";
import { initSite, setPageLang, storedLang } from "/site.js?v=20260929e";
import {
DOCS_DEFAULT_LANG,
DOCS_DEFAULT_PAGE,
DOCS_LANGUAGES,
DOCS_UI,
DOCS_VERSIONS
} from "/docs/content.js?v=20260929d";
} from "/docs/content.js?v=20260929e";
const SITE = "https://vibetrading.wiki";
+3 -3
View File
@@ -23,10 +23,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
</head>
<body data-section="home">
<!-- site-header:start -->
+2 -1
View File
@@ -105,5 +105,6 @@
"home.term.route": "路由:加密行情 -> 策略 -> 回测 -> 运行卡",
"home.term.result": "<span class=\"ok\">收益</span> +18.6% <span class=\"warn\">最大回撤</span> -7.4%",
"home.term.workers": "成员:宏观、量化、风控、催化剂",
"home.term.report": "<span class=\"ok\">报告</span>已保存,附证据、指标和注意事项"
"home.term.report": "<span class=\"ok\">报告</span>已保存,附证据、指标和注意事项",
"lab.gtja.date": "2026-05-17 · 2026-09-29 修订"
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { initSite } from "/site.js?v=20260929d";
import { initSite } from "/site.js?v=20260929e";
// Home page install tabs; a no-op on pages without them.
function initInstallTabs() {
+2 -2
View File
@@ -2,5 +2,5 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
+4 -4
View File
@@ -18,10 +18,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
</head>
<body data-section="lab">
<!-- site-header:start -->
@@ -62,7 +62,7 @@
<section class="posts-grid" aria-label="Latest posts" data-i18n-attr="aria-label=lab.postsLabel">
<article class="post-card">
<p class="post-card__date">2026-05-17</p>
<p class="post-card__date" data-i18n="lab.gtja.date">2026-05-17 · revised 2026-09-29</p>
<h2 class="post-card__title">
<a href="/research-lab/posts/alpha-191-in-2026.html" data-href-zh="/research-lab/posts/alpha-191-in-2026-zh.html" data-i18n="lab.gtja.title">Which of the 191 GTJA alphas still work in 2026?</a>
</h2>
@@ -22,10 +22,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
<style>
.post {
width: min(760px, calc(100% - 32px));
@@ -255,37 +255,39 @@
<p class="lede">2014 年,国泰君安证券发布了一份研究报告,系统整理了面向 A 股市场的 191 个短周期 alpha 因子。十二年过去,T+1 交收制度依旧,行业轮动格局已两度翻转,散户资金流处于数十年来的高位&mdash;&mdash;这些公式里还有多少能产生可靠的信号?我们在沪深300 上、以 2018&ndash;2025 年为区间,对全部 191 个因子做了基准测试,得到的答案远比一个数字更耐人寻味。</p>
</header>
<p class="callout" id="revision"><strong>2026-09-29 修订。</strong>本文第一版(2026-05-17)使用的 Tushare 价格未做除权除息复权&mdash;&mdash;跨越除权日的收益率读到的是机械性的价格缺口,而且总是偏向下跌&mdash;&mdash;并且把最新的沪深300 成分名单套用到了整个窗口。这两个问题已于 2026-08-04 修复;自 2026-09 起,某根 K 线上只要因子的任一输入缺失,因子就不输出值,而不是用缺失数据生成信号。第一版报告的是存活 10 个、反转 15 个、失效 165 个;下文所有数字均来自在当前代码上的重新运行。</p>
<section id="tldr">
<h2>要点速览</h2>
<p>国泰君安 2014 年发布的 191 个短周期 alpha 因子中,在沪深300、2018&ndash;2025 年区间上,只有 10 个(5%)仍能通过我们的存活筛选&mdash;&mdash;平均 IC 为正且高于 0.02、t 统计量高于 2、且至少 55% 的交易日 IC 为正。另有 15 个(8%)<em>方向反转</em>且具有统计显著性,如今成了反向信号。其余 165 个(87%)要么已衰减到不再显著,要么因缺少我们没有的行业标签而无法计算,要么触发了 NaN 占比 &gt;95% 的预热期保护。这 10 个存活因子集中在微观结构 / K 线形态一族,而不是我们原先预期的原始量价交互一族。具体数量见下文;方法刻意保守,局限性也不容忽视。</p>
<p>国泰君安 2014 年发布的 191 个短周期 alpha 因子中,在沪深300、2018&ndash;2025 年区间上,只有 8 个(4%)仍能通过我们的存活筛选&mdash;&mdash;平均 IC 为正且高于 0.02、t 统计量高于 2、且至少 55% 的交易日 IC 为正。另有 14 个(7%)<em>方向反转</em>且具有统计显著性,如今成了反向信号。另外 168 个(88%)在两个方向上都已不再显著;还有 1 个(<code>gtja191_138</code>)完全无法检验:它的输出 99% 是 NaN。8 个存活因子中有一半带有微观结构 / K 线形态标签,这是各主题中最高的存活率,而不是我们原先预期的原始量价交互一族。具体数量见下文;方法刻意保守,局限性也不容忽视。</p>
<div class="findings-stats" aria-label="因子存活数量汇总">
<div class="stat alive">
<span class="label">存活</span>
<span class="num"><span class="num-alive">10</span></span>
<span class="num"><span class="num-alive">8</span></span>
<span>191 个因子中</span>
</div>
<div class="stat reversed">
<span class="label">反转</span>
<span class="num"><span class="num-reversed">15</span></span>
<span class="num"><span class="num-reversed">14</span></span>
<span>191 个因子中</span>
</div>
<div class="stat dead">
<span class="label">失效</span>
<span class="num"><span class="num-dead">165</span></span>
<span class="num"><span class="num-dead">168</span></span>
<span>191 个因子中</span>
</div>
</div>
<p class="callout">下文数字均为对内置 gtja191 因子集实际运行 W4.a bench 的输出(沪深300,2018&ndash;2025),可用文末的 CLI 命令复现。</p>
<p class="callout">下文数字来自 2026-09-29 的一次 bench 运行(Vibe-Trading 0.1.16:沪深300 时点成分、复权价格,2018&ndash;2025),可用文末的 CLI 命令复现。</p>
</section>
<section id="background">
<h2>背景</h2>
<p>国泰君安 2014 年的这份研究报告&mdash;&mdash;题为《191 个短周期交易型 alpha 因子》(英文译名 <em>191 Short-Period Transactional Alpha Factors</em>)&mdash;&mdash;问世于中国量化发展的一个特殊时点。当时市场刚走出多年的横盘震荡,散户参与度正在回升,而 Kakushadze 式的“公式化 alpha”模板在此前几年已借由他 2015 年的 arXiv 预印本在国际上流行开来。回头看,国泰君安团队拿出的是迄今针对 A 股公开发布的最系统的短周期因子目录之一:191 个编号公式,每个都只是基于日频 OHLCV 与换手率的寥寥几行算子运算。</p>
<p>国泰君安 2014 年的这份研究报告&mdash;&mdash;题为《191 个短周期交易型 alpha 因子》(英文译名 <em>191 Short-Period Transactional Alpha Factors</em>)&mdash;&mdash;问世于中国量化发展的一个特殊时点。当时市场刚走出多年的横盘震荡,散户参与度正在回升,而“公式化 alpha”模板要到一年后,才借由 Kakushadze 2015 年的 arXiv 预印本 <em>101 Formulaic Alphas</em> 在国际上流行开来。回头看,国泰君安团队拿出的是迄今针对 A 股公开发布的最系统的短周期因子目录之一:191 个编号公式,每个都只是基于日频 OHLCV 与换手率的寥寥几行算子运算。</p>
<p>报告中的公式读起来像是关于市场微观结构的第一性原理假设,被翻译成了便于用 pandas 实现的算术。有些事后看来显而易见(基于排名的五日反转;成交量与收盘价的相关性);另一些则相当奇特(报告中
<code class="formula">SUMIF</code> / <code class="formula">FILTER</code> / <code class="formula">REGBETA</code>
的组合嵌套深达四到五层算子)。用现代量化的话说,它们是一个<em>先验库</em>:每个公式都编码了某个人的判断&mdash;&mdash;哪种微观结构规律可以在 1&ndash;5 天的周期上被利用。</p>
<p>这件事在 2026 年之所以重要,原因很简单:短周期 alpha 的衰减速度比任何其他类别都快。长周期的价值或质量因子不做任何调整也可能存活十年;而建立在量价交互上的 1&ndash;5 天公式大概做不到。在因子研究的尺度上,十二年的样本外几乎等同于永恒。A 股市场本身也变了:T+1 交收仍在,但机构与散户的结构已经翻转,科创板与创业板注册制改革重新定价了小盘股风险,公募基金带有高频色彩的交易执行也压缩了许多明显的均值回归窗口。</p>
<p>于是我们做了这次检验。下面先介绍方法,数字随后给出。</p>
<p>于是我们做了这次检验:先讲方法,再给数字。</p>
</section>
<section id="method">
@@ -293,61 +295,61 @@
<p>这次检验刻意做成<em>截面信息系数(IC)研究</em>,而不是完整回测。要回答的问题是:<em>这个 alpha 对股票的排序,是否在整个窗口内平均而言、稳健地与次日收益相关?</em>更高保真度的策略回测(含交易成本、仓位限制、行业中性化、按衰减系数加权的组合)是另一个问题,不在本文讨论范围内。</p>
<h3>股票池</h3>
<p>沪深300。流动性最好的 300 只 A 股,由指数公司每半年调整一次成分。我们在每个调仓日使用指数的现行成分股,因此股票池随指数漂移。相对于固定不变的股票池,这会带来偏差(被调出沪深300 的恰恰是流动性已经衰退的个股,这可能使 IC 估计产生偏差),但它与实际部署时使用该指数的方式一致。</p>
<p>沪深300:流动性最好的 300 只 A 股,由指数公司每半年调整一次成分。每个交易日的截面,取 Tushare 月末成分名单中当日有效的指数成分,因此一只股票只在它属于指数的期间计入&mdash;&mdash;窗口期内共有 348 只股票曾是成分股。价格已做除权除息前复权。</p>
<h3>区间</h3>
<p>2018-01-02 至 2025-12-31。八个日历年,约 1,940 个交易日,相对 2014 年的报告完全处于样本外。窗口刻意覆盖了 2018 年的熊市、2020&ndash;2021 年流动性驱动的上涨、2022 年的回撤以及 2024&ndash;2025 年的震荡市&mdash;&mdash;因此平均 IC 是跨市场状态平均的结果,而不是挑选有利市场状态得出的。</p>
<h3>信号定义</h3>
<p>对每个 alpha 和每个交易日 <em>t</em>,我们计算股票池内每只股票的因子值,再在截面上做排名变换,映射到 <code class="formula">[0, 1]</code>。远期收益为从 <em>t</em> 日收盘到 <em>t+1</em> 日收盘的 1 日对数收益率,同样做截面排名变换。第 <em>t</em> 日的 Spearman IC 即两条排名序列的 Pearson 相关系数。我们报告所有有效交易日的平均 IC、IC 序列的 t 统计量,以及 IC 为正的交易日占比。</p>
<p>对每个 alpha 和每个交易日 <em>t</em>,我们计算股票池内每只股票的因子值,再在截面上做排名变换,映射到 <code class="formula">[0, 1]</code>。远期收益为从 <em>t</em> 日收盘到 <em>t+1</em> 日收盘的 1 日简单收益率,同样做截面排名变换。第 <em>t</em> 日的 Spearman IC 即两条排名序列的 Pearson 相关系数。我们报告所有有效交易日的平均 IC、IC 序列的 t 统计量,以及 IC 为正的交易日占比。</p>
<h3>分类标准</h3>
<ul>
<li><strong>存活。</strong>平均 IC &gt; 0.02、t-stat(IC) &gt; 2,且 IC &gt; 0 的交易日占比 &ge; 55%。三个条件须同时满足。</li>
<li><strong>反转。</strong>平均 IC &lt; &minus;0.02 且 t-stat(IC) &lt; &minus;2(即上述条件的符号翻转版)。原报告设定该因子以某一方向预测收益;我们观察到它以相反方向预测,且具有统计显著性。</li>
<li><strong>失效。</strong>其余所有情况:|平均 IC| &le; 0.02,或 |t-stat| &le; 2,或该因子无法在窗口内干净地计算(例如需要我们没有的日内逐笔数据)。</li>
<li><strong>失效。</strong>其余所有能够计算的情况:|平均 IC| &le; 0.02,或 |t-stat| &le; 2,或 IC 为正的交易日不足 55%。输出中 NaN 占比超过 95% 的因子完全不参与检验,单独列出。</li>
</ul>
<h3>方法本身带来的局限</h3>
<p>以下三个刻意的选择限定了结果应当如何解读:</p>
<ul>
<li>IC 为<em>向前 1 日</em>,不做衰减平滑。decay-3 或 decay-5 IC(原报告偏好的周期)会得出略有不同的数字;我们会在后续文章中发布。</li>
<li>不做交易成本调整。一个高 IC、每日调仓换手率接近 100% 的因子,在扣除 5&ndash;10 bps 的现实双边成本后,实际上无法盈利。我们在 bench 产出物中把换手率与 IC 一并报告,但这里的存活分类没有考虑它。</li>
<li>不做行业中性化。报告中部分因子依赖行业映射(<code class="formula">indneutralize</code>);对这些因子,我们退而用每日截面均值替代,这是一种严格更弱的中性化。受影响的因子在逐因子输出中有标注。</li>
<li>不做交易成本调整。一个高 IC、每日调仓换手率接近 100% 的因子,在扣除 5&ndash;10 bps 的现实双边成本后,实际上无法盈利。bench 不测量换手率,这里的存活分类也没有考虑它。</li>
<li>不做行业中性化。国泰君安 191 因子没有一个需要行业标签,因此这里所有 IC 都是在原始截面上测得的。</li>
</ul>
<h3>所用工具</h3>
<pre><code>$ vibe-trading alpha bench --zoo gtja191 --universe csi300 \
--period 2018-2025 --top 20</code></pre>
<p>一条命令即可。输出为一份 HTML 报告,包含逐因子的 IC、t-stat、换手率和 IC 时间序列图。复现方法见文末。</p>
<p>一条命令即可。它会在 <code>~/.vibe-trading/reports/</code> 下写一份 HTML 报告,列出按 IR 排名靠前的因子(平均 IC、IC 标准差、IR、IC 为正的交易日占比、交易日数),以及无法计算的因子。复现方法见文末。</p>
</section>
<section id="findings">
<h2>结果</h2>
<h3>总体存活情况</h3>
<p>再列一次核心数字,附带同样的说明:最终数字在 W4.a bench 跑完后落定:</p>
<p>再列一次核心数字:</p>
<div class="findings-stats" aria-label="因子存活数量汇总(结果部分重复展示)">
<div class="stat alive">
<span class="label">存活</span>
<span class="num"><span class="num-alive">10</span></span>
<span class="num"><span class="num-alive">8</span></span>
<span>通过全部三项筛选</span>
</div>
<div class="stat reversed">
<span class="label">反转</span>
<span class="num"><span class="num-reversed">15</span></span>
<span class="num"><span class="num-reversed">14</span></span>
<span>如今以相反方向预测</span>
</div>
<div class="stat dead">
<span class="label">失效</span>
<span class="num"><span class="num-dead">165</span></span>
<span class="num"><span class="num-dead">168</span></span>
<span>低于显著性阈值</span>
</div>
</div>
<p>在早期的部分运行中,让我们意外的不是失效数量&mdash;&mdash;衰减人人都预料得到&mdash;&mdash;而是<em>反转</em>数量。相当一部分在 2014 年有效的因子,如今成了幅度具有统计显著性的反向指标。最简单的解读是:公式原本捕捉的行为异象(小盘股均值回归、散户的尾盘资金流)已被恰恰是这类公式所代表的系统化交易挤出,剩下的就是反向交易。</p>
<p>让我们意外的不是失效数量&mdash;&mdash;衰减人人都预料得到&mdash;&mdash;而是<em>反转</em>数量。相当一部分在 2014 年有效的因子,如今成了幅度具有统计显著性的反向指标。最简单的解读是:公式原本捕捉的行为异象(小盘股均值回归、散户的尾盘资金流)已被恰恰是这类公式所代表的系统化交易挤出,剩下的就是反向交易。</p>
<h3>分主题拆解</h3>
<p>我们按主导主题&mdash;&mdash;即因子最倚重的那组算子&mdash;&mdash;给每个 alpha 手工打了标签。这是一种有损的分类(许多因子融合了两个主题),但总体规律对标签选择是稳健的。下表数字为各主题内的存活率:</p>
<p>每个因子在注册表元数据里带有一到两个主题标签&mdash;&mdash;即它最倚重的那组算子。带两个标签的因子在两行中都会计入,因此各行数量之和超过 190。下表数字为各主题内的存活率:</p>
<table class="theme-table" aria-label="各主题存活率">
<thead>
@@ -363,7 +365,7 @@
<td>量价交互</td>
<td>成交量与收盘价、高低价振幅的相关性 / 协方差</td>
<td>81</td>
<td>5% (4/81)</td>
<td>4% (3/81)</td>
</tr>
<tr>
<td>短周期波动率</td>
@@ -375,13 +377,13 @@
<td>反转</td>
<td>1-5 日周期上取负号的收益信号</td>
<td>38</td>
<td>11% (4/38)</td>
<td>8% (3/38)</td>
</tr>
<tr>
<td>动量</td>
<td>10-60 日周期上取正号的收益信号</td>
<td>63</td>
<td>2% (1/63)</td>
<td>0% (0/63)</td>
</tr>
<tr>
<td>换手 / 流动性</td>
@@ -395,61 +397,67 @@
<td>18</td>
<td>22% (4/18)</td>
</tr>
<tr>
<td>情绪</td>
<td>市场下跌日中个股上涨的占比(仅 <code>gtja191_075</code> 一个因子,基准为代理变量)</td>
<td>1</td>
<td>0% (0/1)</td>
</tr>
</tbody>
</table>
<p>W4.a bench 的实际结果是:<em>微观结构 / 振幅</em>类因子是最突出的存活者(存活率 22%,18 个中 4 个),<em>反转</em>类次之(11%,38 个中 4 个)。我们原本预期最经得起时间考验的类别&mdash;&mdash;原始<em>量价交互</em>(5%)和<em>短周期波动率</em>(8%)&mdash;&mdash;衰减程度反而超过开收高低价分解一族。<em>动量</em>类 2%(63 个中 1 个)和规模很小的<em>换手</em>类 0%,是被套利得最干净的两组。这一规律符合如下情形下的预期:十年的系统化资金压缩了最容易的反转 / 动量交易,却没有触动那些捕捉 K 线形态效应、基于日线结构几何的因子。</p>
<p>重新运行的结果是:<em>微观结构 / 振幅</em>类因子是最突出的存活者(22%,18 个中 4 个)。<em>反转</em>和<em>短周期波动率</em>其次,都是 8%(38 个中 3 个、26 个中 2 个),原始<em>量价交互</em>为 4%(81 个中 3 个)。<em>动量</em>类 63 个无一存活&mdash;&mdash;其中 5 个如今以相反方向预测&mdash;&mdash;规模很小的<em>换手</em>类 2 个也无一存活。这一规律符合如下情形下的预期:十年的系统化资金压缩了最容易的反转 / 动量交易,却没有触动那些捕捉 K 线形态效应、基于日线结构几何的因子。</p>
<h3>存活因子 Top 5</h3>
<p>窗口内平均 IC 最高的五个因子,按降序排列。每个因子以其 zoo id 和公式的转述标识;完整的算子级定义见对应 Python 模块中的 <code>__alpha_meta__["formula_latex"]</code> 字段。</p>
<p>窗口内 IR(平均 IC 除以其标准差)最高的五个因子,按降序排列,公式均为注册表中的原样(对应 Python 模块的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
<article class="alpha-card">
<article class="alpha-card">
<span class="alpha-id">gtja191_114</span>
<pre class="formula-block"><code>((RANK(DELAY(((HIGH-LOW)/(SUM(CLOSE,5)/5)),2)) * RANK(RANK(VOLUME))) / (((HIGH-LOW)/(SUM(CLOSE,5)/5))/(VWAP-CLOSE)))</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0606,IR = 0.2926。公式逐字取自注册表(<code>gtja191_114</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_171</span>
<pre class="formula-block"><code>-1*((l-c)*(o^5))/((c-h)*(c^5))</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0432,IR = 0.2690。公式逐字取自注册表(<code>gtja191_171</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0435,IR = 0.2299。公式逐字取自注册表(<code>gtja191_171</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_120</span>
<pre class="formula-block"><code>rank(vwap-close)/rank(vwap+close)</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0452,IR = 0.2119。公式逐字取自注册表(<code>gtja191_120</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_111</span>
<pre class="formula-block"><code>sma(v*((c-l)-(h-c))/(h-l),11,2)-sma(v*((c-l)-(h-c))/(h-l),4,2)</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0349,IR = 0.2232。公式逐字取自注册表(<code>gtja191_111</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0355,IR = 0.1888。公式逐字取自注册表(<code>gtja191_111</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_163</span>
<pre class="formula-block"><code>rank(((-1*ret)*mean(v,20))*vwap*(high-close))</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0347,IR = 0.2008。公式逐字取自注册表(<code>gtja191_163</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_002</span>
<pre class="formula-block"><code>(-1 * DELTA(((CLOSE - LOW) - (HIGH - CLOSE)) / (HIGH - LOW), 1))</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0262,IR = 0.1619。公式逐字取自注册表(<code>gtja191_002</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_054</span>
<pre class="formula-block"><code>((-1*RANK((STD(ABS(CLOSE-OPEN),10)+(CLOSE-OPEN))+CORR(CLOSE,OPEN,10))))</code></pre>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0272,IR = 0.1606。公式逐字取自注册表(<code>gtja191_054</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
<p class="alpha-note">在沪深300 / 2018&ndash;2025 窗口上,平均 IC = 0.0342,IR = 0.1674。公式逐字取自注册表(<code>gtja191_163</code> 的 <code>__alpha_meta__["formula_latex"]</code>)。</p>
</article>
<h3>三个著名的失效因子</h3>
<p>以下三个公式在 2014 年的报告中有效(或被声称有效),如今却稳稳落在失效或反转一栏:</p>
<h3>三个如今方向相反的因子</h3>
<p>平均 IC 最负的三个因子。三者都在反转一栏:它们如今以与报告设定相反的方向预测收益,且具有统计显著性。</p>
<article class="alpha-card">
<article class="alpha-card">
<span class="alpha-id">gtja191_124(反转)</span>
<pre class="formula-block"><code>(close-vwap)/decay_linear(rank(tsmax(close,30)),2)</code></pre>
<p class="alpha-note">平均 IC = -0.0518,IR = -0.2516。在沪深300 / 2018&ndash;2025 上,是 gtja191 因子集中平均 IC 最负的因子。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_178(反转)</span>
<pre class="formula-block"><code>(c-delay(c,1))/delay(c,1)*v</code></pre>
<p class="alpha-note">平均 IC = -0.0327,IR = -0.1930。按原始 IC 计,属于 gtja191 因子集在沪深300 / 2018&ndash;2025 上表现最差的一批。</p>
<p class="alpha-note">平均 IC = -0.0329,IR = -0.1644。平均 IC 第二负。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_137(反转)</span>
<pre class="formula-block"><code>见正文</code></pre>
<p class="alpha-note">平均 IC = -0.0277,IR = -0.1556。按原始 IC 计,属于 gtja191 因子集在沪深300 / 2018&ndash;2025 上表现最差的一批。</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_066(反转)</span>
<pre class="formula-block"><code>(CLOSE-MEAN(CLOSE,6))/MEAN(CLOSE,6)*100</code></pre>
<p class="alpha-note">平均 IC = -0.0270,IR = -0.1377。按原始 IC 计,属于 gtja191 因子集在沪深300 / 2018&ndash;2025 上表现最差的一批。</p>
<pre class="formula-block"><code>16*((c-dc1+(c-o)/2+dc1-do1)/MAX_term) * MAX(abs(h-dc1), abs(l-dc1))</code></pre>
<p class="alpha-note">平均 IC = -0.0300,IR = -0.1464。平均 IC 第三负。<code>MAX_term</code> 是报告中分三段取值的分母,由 |h &minus; dc1|、|l &minus; dc1|、|h &minus; dl1| 和 |dc1 &minus; do1| 构成(dc1、do1、dl1 分别为前一日的收盘价、开盘价和最低价);完整定义见模块代码。</p>
</article>
<h3>一段反思</h3>
<p>做完这类存活研究,人们容易过度概括:“衰减不可避免;公式化 alpha 已死。”我们认为这不是正确的解读。这次研究给出的结论更窄、也更有用:一份 12 年前的短周期因子目录中,仍有相当一部分能产生信号;存活者集中在可解释的主题里(量价交互、短周期波动率),失效者也同样集中在可解释的主题里(朴素反转、简单换手率变换)。至于 Kakushadze 101 公式或 Qlib 158 特征集是否会以同样的速度衰减,它几乎说明不了什么&mdash;&mdash;这两个因子集的算子词汇不同,面向的股票池也不同(分别是美股和多周期)。这留待后续工作,另行跑 bench。</p>
<p>做完这类存活研究,人们容易过度概括:“衰减不可避免;公式化 alpha 已死。”我们认为这不是正确的解读。这次研究给出的结论更窄、也更有用:一份 12 年前的短周期因子目录中,仍有一小部分&mdash;&mdash;190 个中的 8 个&mdash;&mdash;能产生信号;存活者集中在一个可解释的主题里(K 线几何:开收高低价分解),失效者也同样集中在可解释的主题里(动量类无一存活、5 个方向翻转,以及简单的换手率变换)。至于 Kakushadze 101 公式或 Qlib 158 特征集是否会以同样的速度衰减,它几乎说明不了什么&mdash;&mdash;这两个因子集的算子词汇不同,面向的股票池也不同(分别是美股和多周期)。这留待后续工作,另行跑 bench。</p>
</section>
<section id="caveats">
@@ -463,13 +471,16 @@
<p>我们的基准测试只覆盖流动性最好的 300 只 A 股。为全 A 股票池设计的因子&mdash;&mdash;全 A 约有 5,000 只股票,流动性特征差异很大&mdash;&mdash;表现会有所不同。尤其是小盘股均值回归类因子,在沪深300 上往往比在全市场上表现更差(因为沪深300 以机构资金为主导,短周期的散户反转效应较弱),这里一些“失效”的因子在中证1000 上或许会复活。</p>
<h3>Tushare 数据范围</h3>
<p>我们的数据源(Tushare 日终 OHLCV 与换手率,外加由成交额推算的 VWAP)不含日内逐笔或订单簿信息。少数国泰君安 191 因子在原始表述中引用了日内数据(深度加权 VWAP、level-1 盘口失衡);对这些因子,我们以日频 VWAP 替代,并在逐因子输出中标注这一替换。这些因子在这里应视为<em>未经充分检验</em>,而不是“失效”。</p>
<p>我们的数据源是 Tushare 日终 OHLCV 与成交额,已做除权除息前复权,VWAP 由成交额推算;不含日内逐笔或订单簿数据。有 5 个国泰君安 191 因子引用了面板中没有的输入&mdash;&mdash;基准指数收盘价(<code>gtja191_075</code>、<code>_149</code>、<code>_181</code>、<code>_182</code>)或多因子回归残差(<code>gtja191_030</code>)&mdash;&mdash;改用了代理变量:截面平均收盘价,或日收益率平方的加权平均,各因子的说明中均有写明。这 5 个因子在这里应视为<em>未经充分检验</em>,而不是“失效”。</p>
<h3>8 年窗口偏短</h3>
<p>按学术标准,8 年窗口偏短。经典的 Fama-French 论文使用 30&ndash;90 年的窗口。一些被我们标为“失效”的因子,在更长或所处时段不同的样本上可能复活&mdash;&mdash;例如,若 2026&ndash;2030 年的市场状态重新回到散户占比更高的结构,均值回归类因子可能恢复。这个标签的含义是“在本窗口内失效”,而非“永久失效”。</p>
<h3>股票池中的幸存者偏差</h3>
<p>本文使用的沪深300 成分股名单,是把<em>当前</em>的指数成分套用到整个 2018&ndash;2025 窗口,而不是按每次调仓时点重建的指数成分(point-in-time)。窗口期内退市、降档或被调出的股票不在其中;后期才纳入的股票则被回填了完整的历史价格。这是一种标准的幸存者偏差,会使 IC 估计在纯多头方向上偏高。对于以同样方式构建美股股票池的任何跨因子集比较,这一提醒更加适用&mdash;&mdash;例如,用今天的成分股名单跑标普 500,相对于真正的时点股票池会高估存活数量;那里若得出 0% 的存活比例,应理解为“衰减加上幸存者偏差把信号抵消了”,而不是该因子家族的干净失败。时点指数成分是计划中的升级。</p>
<p>本文第一版把最新的沪深300 成分名单套用到整个 2018&ndash;2025 窗口,每只股票之所以在列,都是因为它存活到了最后&mdash;&mdash;这是一个事后挑选的集合,会使 IC 偏高。重新运行时,每个交易日的截面都按当日有效的成分名单筛选(窗口期内共有 348 只股票曾是成分股)。<code>sp500</code> 股票池仍使用今天的成分名单,因此在它上面做的任何跨因子集比较都适用同样的提醒&mdash;&mdash;它的 bench 报告会写明这一点&mdash;&mdash;那里较低的存活数应理解为“衰减加上幸存者偏差”,而不是该因子家族的干净失败。</p>
<h3>190 个公式,各测一次</h3>
<p>在同一窗口上检验 190 个公式,总会有一些仅凭运气显得不错。bench 会报告:如果所有公式都不含任何信号,预期的最高 IR 有多大&mdash;&mdash;这里是 0.204。实际最高的 IR 为 0.293(<code>gtja191_114</code>),通过了这一校正(概率 0.9999)。另有两个存活因子高于 0.204&mdash;&mdash;<code>gtja191_171</code>(0.230)和 <code>gtja191_120</code>(0.212);其余五个的 IR 在 0.130 到 0.189 之间,低于这一水平,应视为候选信号而非已确认的信号。</p>
<h3>未按市场状态分组</h3>
<p>我们在整个 2018&ndash;2025 窗口上对 IC 取平均。这些因子中很多几乎肯定依赖市场状态&mdash;&mdash;在趋势市有效、在震荡市失灵,或者反过来。后续文章会按市场状态(牛市 / 熊市 / 震荡市,以 20 日指数动量划分)拆分存活数量,但本文的汇总数字是跨状态平均的。一个考虑市场状态的部署方案,或许能把一半的失效因子保留为条件信号。</p>
@@ -483,10 +494,10 @@
export TUSHARE_TOKEN=your_token_here
vibe-trading alpha bench --zoo gtja191 \
--universe csi300 \
--period 2020-2025 \
--period 2018-2025 \
--top 20</code></pre>
<p>你会得到一份保存在 <code>~/.vibe-trading/reports/</code> 的 HTML 报告,包含逐因子的 IC、IR、衰减曲线和一张可排序的 top-N 表。同一命令也适用于 <code>--zoo alpha101</code>(Kakushadze 的 101 Formulaic Alphas,忠实于论文、据 2015 年 arXiv 预印本重写)和 <code>--zoo qlib158</code>(微软 Qlib 特征库,依 Apache-2.0 许可并注明出处使用)。跨因子集比较通过 <code>alpha compare</code> 运行。</p>
<p>你会得到一份保存在 <code>~/.vibe-trading/reports/</code> 的 HTML 报告,列出按 IR 排名靠前的因子,以及无法计算的因子。同一命令也适用于 <code>--zoo alpha101</code>(Kakushadze 的 101 Formulaic Alphas,忠实于论文、据 2015 年 arXiv 预印本重写)和 <code>--zoo qlib158</code>(微软 Qlib 特征库,依 Apache-2.0 许可并注明出处使用)。跨因子集比较通过 <code>alpha compare</code> 运行。</p>
<div class="cta-row">
<a class="primary" href="https://github.com/HKUDS/Vibe-Trading">在 GitHub 查看源码</a>
@@ -494,7 +505,7 @@ vibe-trading alpha bench --zoo gtja191 \
<a href="/docs/">阅读文档</a>
</div>
<p>如果你发现某个因子的存活分类出乎意料&mdash;&mdash;尤其是我们没有列入前 5 名的存活者,或我们放进了存活一栏的著名失效因子&mdash;&mdash;欢迎附上因子 id 和你的理由提交 issue。社区也欢迎通过 pull request 贡献新的因子集、新的股票池(中证1000、纳斯达克 100、主流加密货币)或新的验证工具,流程遵循
<p>如果你发现某个因子的存活分类出乎意料&mdash;&mdash;尤其是我们没有列入前 5 名的存活者,或你原本以为会存活却落入反转一栏的因子&mdash;&mdash;欢迎附上因子 id 和你的理由提交 issue。社区也欢迎通过 pull request 贡献新的因子集、新的股票池(中证1000、纳斯达克 100、主流加密货币)或新的验证工具,流程遵循
<a href="https://github.com/HKUDS/Vibe-Trading/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a>
中的 DCO 规范。</p>
+124 -110
View File
@@ -22,10 +22,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
<style>
.post {
width: min(760px, calc(100% - 32px));
@@ -259,34 +259,37 @@
answer turns out to be more interesting than a single number.</p>
</header>
<p class="callout" id="revision"><strong>Revised 2026-09-29.</strong> The first version of this post (2026-05-17) ran on Tushare prices that were not adjusted for splits and dividends &mdash; a return across an ex-date read the mechanical price gap, always downward &mdash; and applied today's CSI 300 roster to the whole window. Both were fixed on 2026-08-04, and since 2026-09 an alpha emits no value on a bar where one of its inputs is missing, instead of a signal built on it. That version reported 10 alive, 15 reversed and 165 dead; every number below comes from a re-run on the current code.</p>
<section id="tldr">
<h2>TL;DR</h2>
<p>Of the 191 short-horizon alphas published by GTJA in 2014, only 10 (5%) still pass our
<p>Of the 191 short-horizon alphas published by GTJA in 2014, only 8 (4%) still pass our
alive filter &mdash; positive mean IC above 0.02, t-stat above 2, and at least 55% of days with
positive IC &mdash; on CSI 300 over 2018&ndash;2025. A further 15 (8%) have <em>reversed sign</em>
with statistical significance and now act as contrarian signals. The remaining 165 (87%) have
decayed below significance, could not be computed without sector tags we do not have, or hit
the &gt;95% NaN warmup guard. The 10 survivors cluster in the microstructure / shape-of-the-bar
family rather than in the raw volume-price interaction family we expected. Exact counts below;
the methodology is deliberately conservative and the caveats are non-trivial.</p>
positive IC &mdash; on CSI 300 over 2018&ndash;2025. A further 14 (7%) have <em>reversed sign</em>
with statistical significance and now act as contrarian signals. Another 168 (88%) no longer clear
the significance filter in either direction, and one (<code>gtja191_138</code>) could not be tested
at all: 99% of its output is NaN. Half of the 8 survivors carry the microstructure /
shape-of-the-bar tag, the best survival rate of any theme, rather than the raw volume-price
interaction family we expected. Exact counts below; the methodology is deliberately conservative
and the caveats are non-trivial.</p>
<div class="findings-stats" aria-label="Aggregate alpha survival counts">
<div class="stat alive">
<span class="label">Alive</span>
<span class="num"><span class="num-alive">10</span></span>
<span class="num"><span class="num-alive">8</span></span>
<span>of 191 alphas</span>
</div>
<div class="stat reversed">
<span class="label">Reversed</span>
<span class="num"><span class="num-reversed">15</span></span>
<span class="num"><span class="num-reversed">14</span></span>
<span>of 191 alphas</span>
</div>
<div class="stat dead">
<span class="label">Dead</span>
<span class="num"><span class="num-dead">165</span></span>
<span class="num"><span class="num-dead">168</span></span>
<span>of 191 alphas</span>
</div>
</div>
<p class="callout">Numbers below are the live W4.a bench output on the bundled gtja191 zoo (CSI 300, 2018&ndash;2025) &mdash; reproducible via the CLI snippet at the end of the post.</p>
<p class="callout">Numbers below are from a bench run on 2026-09-29 (Vibe-Trading 0.1.16: CSI 300 with point-in-time membership and split-adjusted prices, 2018&ndash;2025) &mdash; reproducible via the CLI snippet at the end of the post.</p>
</section>
<section id="background">
@@ -294,8 +297,8 @@
<p>The 2014 Guotai Junan research report &mdash; titled "191 个短周期交易型 alpha 因子"
(<em>191 Short-Period Transactional Alpha Factors</em>) &mdash; landed at a particular moment in
Chinese quant. The market had just emerged from a multi-year sideways grind, retail participation
was climbing back, and the Kakushadze-style "formulaic alpha" template had been popularised
internationally a few years earlier by his 2015 arXiv preprint. The GTJA team produced what is,
was climbing back, and the "formulaic alpha" template would be popularised internationally a
year later by Kakushadze's 2015 arXiv preprint, <em>101 Formulaic Alphas</em>. The GTJA team produced what is,
in retrospect, one of the most systematic public catalogues of short-horizon factors ever published
for the A-share universe: 191 numbered formulas, each a few lines of operator algebra over daily
OHLCV plus turnover.</p>
@@ -313,7 +316,7 @@
changed: T+1 settlement remains, but the institutional/retail mix has flipped, the STAR Board and
ChiNext registration-system reform have re-priced small-cap risk, and high-frequency-flavoured
execution by mutual funds has compressed many obvious mean-reversion windows.</p>
<p>So we ran the test. The methodology preview is below; the numbers will follow.</p>
<p>So we ran the test: method first, then the numbers.</p>
</section>
<section id="method">
@@ -325,11 +328,10 @@
question and is out of scope for this post.</p>
<h3>Universe</h3>
<p>CSI 300. The 300 most liquid A-shares, rebalanced semi-annually by the index provider. We use
the current index constituents on each rebalance date, so the universe drifts with the index. This
is biased relative to a frozen universe (the dropouts from CSI 300 are precisely the names whose
liquidity has decayed, which can bias the IC estimate), but it is consistent with how an actual
deployment would use the index.</p>
<p>CSI 300: the 300 most liquid A-shares, rebalanced semi-annually by the index provider. Each
day's cross-section is the index membership in force on that day, from Tushare's month-end
rosters, so a stock counts only while it is in the index &mdash; 348 names were members at some
point in the window. Prices are forward-adjusted for splits and dividends.</p>
<h3>Period</h3>
<p>2018-01-02 through 2025-12-31. Eight calendar years, ~1,940 trading days, fully out-of-sample
@@ -340,7 +342,7 @@
<h3>Signal definition</h3>
<p>For each alpha and each trading day <em>t</em>, we compute the alpha value for every stock in
the universe, then cross-sectionally rank-transform it to <code class="formula">[0, 1]</code>.
Forward return is the 1-day log return from close <em>t</em> to close <em>t+1</em>, also
Forward return is the 1-day simple return from close <em>t</em> to close <em>t+1</em>, also
cross-sectionally rank-transformed. The Spearman IC for day <em>t</em> is the Pearson correlation
of the two ranked series. We report the mean IC across all valid days, the t-statistic of the IC
series, and the fraction of days with positive IC.</p>
@@ -352,8 +354,9 @@
<li><strong>Reversed.</strong> Mean IC &lt; &minus;0.02 and t-stat(IC) &lt; &minus;2 (sign-flipped
version of the above). The original report intended the alpha to predict with one sign; we
observe it predicting with the opposite sign at statistical significance.</li>
<li><strong>Dead.</strong> Everything else: |mean IC| &le; 0.02, or |t-stat| &le; 2, or the alpha
could not be computed cleanly over the window (e.g. requires intraday tick data we do not have).</li>
<li><strong>Dead.</strong> Everything else that could be computed: |mean IC| &le; 0.02, or
|t-stat| &le; 2, or a positive IC on fewer than 55% of days. An alpha whose output is more than
95% NaN is not tested at all and is reported separately.</li>
</ul>
<h3>Caveats baked into the method</h3>
@@ -363,54 +366,53 @@
report's preferred horizon) will produce slightly different numbers; we'll publish those as a
follow-up.</li>
<li>No t-cost adjustment. A high-IC alpha with daily-rebalance turnover near 100% is, in
practice, unprofitable after a realistic 5&ndash;10 bps round-trip. We report turnover alongside
IC in the artefact bench output, but the survival classification here ignores it.</li>
<li>No sector neutralisation. Some alphas in the report rely on sector mapping
(<code class="formula">indneutralize</code>); for those we substitute the per-day
cross-sectional mean as a fallback, which is a strictly weaker neutralisation. Affected alphas
are flagged in the per-alpha output.</li>
practice, unprofitable after a realistic 5&ndash;10 bps round-trip. The bench does not
measure turnover, and the survival classification here ignores it.</li>
<li>No sector neutralisation. None of the GTJA 191 formulas needs sector tags, so every IC here
is measured on the raw cross-section.</li>
</ul>
<h3>Tool used</h3>
<pre><code>$ vibe-trading alpha bench --zoo gtja191 --universe csi300 \
--period 2018-2025 --top 20</code></pre>
<p>One command. Output is an HTML report with per-alpha IC, t-stat, turnover, and IC time-series
plot. Reproducibility recipe at the end of the post.</p>
<p>One command. It writes an HTML report to <code>~/.vibe-trading/reports/</code> with the top
alphas by IR (mean IC, IC standard deviation, IR, share of positive-IC days, number of days) and
the alphas it could not compute. Reproducibility recipe at the end of the post.</p>
</section>
<section id="findings">
<h2>Findings</h2>
<h3>Aggregate survival</h3>
<p>The headline counts again, with the same caveat that final numbers land after the W4.a bench:</p>
<p>The headline counts again:</p>
<div class="findings-stats" aria-label="Aggregate alpha survival counts, repeated for findings section">
<div class="stat alive">
<span class="label">Alive</span>
<span class="num"><span class="num-alive">10</span></span>
<span class="num"><span class="num-alive">8</span></span>
<span>survive all three filters</span>
</div>
<div class="stat reversed">
<span class="label">Reversed</span>
<span class="num"><span class="num-reversed">15</span></span>
<span class="num"><span class="num-reversed">14</span></span>
<span>now predict with opposite sign</span>
</div>
<div class="stat dead">
<span class="label">Dead</span>
<span class="num"><span class="num-dead">165</span></span>
<span class="num"><span class="num-dead">168</span></span>
<span>below significance threshold</span>
</div>
</div>
<p>What surprised us, in early partial runs, is not the dead count &mdash; everyone expects
decay &mdash; but the <em>reversed</em> count. A meaningful slice of alphas that worked in 2014
now act as contrarian indicators with statistically significant magnitude. The simplest reading is
that a behavioural anomaly the formula was capturing (small-cap mean reversion, end-of-day flow
from retail) has been crowded out by precisely the kind of systematic trading the formula
represents, and what remains is the opposite trade.</p>
<p>What surprised us is not the dead count &mdash; everyone expects decay &mdash; but the
<em>reversed</em> count. A meaningful slice of alphas that worked in 2014 now act as contrarian
indicators with statistically significant magnitude. The simplest reading is that a behavioural
anomaly the formula was capturing (small-cap mean reversion, end-of-day flow from retail) has been
crowded out by precisely the kind of systematic trading the formula represents, and what remains
is the opposite trade.</p>
<h3>Theme breakdown</h3>
<p>We hand-tagged each alpha by its dominant theme &mdash; the operator vocabulary it leans on
most heavily. This is a lossy categorisation (many alphas blend two themes), but the aggregate
pattern is robust to the labelling choice. Numbers below are survival rates within each theme:</p>
<p>Each alpha carries one or two theme tags in its registry metadata &mdash; the operator
vocabulary it leans on most heavily. An alpha with two tags counts in both rows, so the counts add
up to more than 190. Numbers below are survival rates within each theme:</p>
<table class="theme-table" aria-label="Theme-by-theme survival rate">
<thead>
@@ -426,7 +428,7 @@
<td>Volume-price interaction</td>
<td>Correlation / covariance of volume with close, high-low range</td>
<td>81</td>
<td>5% (4/81)</td>
<td>4% (3/81)</td>
</tr>
<tr>
<td>Short-horizon volatility</td>
@@ -438,13 +440,13 @@
<td>Reversal</td>
<td>Negative-sign return signals over 1-5 day horizons</td>
<td>38</td>
<td>11% (4/38)</td>
<td>8% (3/38)</td>
</tr>
<tr>
<td>Momentum</td>
<td>Positive-sign return signals over 10-60 day horizons</td>
<td>63</td>
<td>2% (1/63)</td>
<td>0% (0/63)</td>
</tr>
<tr>
<td>Turnover / liquidity</td>
@@ -458,79 +460,84 @@
<td>18</td>
<td>22% (4/18)</td>
</tr>
<tr>
<td>Sentiment</td>
<td>Share of up days on days the market fell (one alpha, <code>gtja191_075</code>, on a proxy benchmark)</td>
<td>1</td>
<td>0% (0/1)</td>
</tr>
</tbody>
</table>
<p>The actual read from the W4.a bench: <em>microstructure / range</em> alphas are the standout
survivors (22% survival, 4 of 18), with <em>reversal</em> next (11%, 4 of 38). The categories
we expected to age best &mdash; raw <em>volume-price interaction</em> (5%) and
<em>short-horizon volatility</em> (8%) &mdash; have decayed more than the open-close-high-low
decomposition family. <em>Momentum</em> at 2% (1 of 63) and the small <em>turnover</em> bucket
at 0% are the cleanly-arbitraged groups. The pattern is consistent with what you would expect
<p>The read from the re-run: <em>microstructure / range</em> alphas are the standout survivors
(22%, 4 of 18). <em>Reversal</em> and <em>short-horizon volatility</em> follow at 8% (3 of 38 and
2 of 26), raw <em>volume-price interaction</em> at 4% (3 of 81). <em>Momentum</em> keeps none of
its 63 &mdash; five of them now predict with the opposite sign &mdash; and the small
<em>turnover</em> bucket keeps none of its 2. The pattern is consistent with what you would expect
if a decade of systematic capital has compressed the easiest reversal/momentum trades but left
intact the structural daily-bar geometry alphas that key on shape-of-the-bar effects.</p>
<h3>Top 5 surviving alphas</h3>
<p>Five alphas with the highest mean IC across the window, in descending order. Each is identified
by its zoo id and a paraphrase of its formula; full operator-level definition is in the
<code>__alpha_meta__["formula_latex"]</code> field of the corresponding Python module.</p>
<p>The five alphas with the highest IR (mean IC divided by its standard deviation) across the
window, in descending order, each with its formula as the registry stores it
(<code>__alpha_meta__["formula_latex"]</code> of the corresponding Python module).</p>
<article class="alpha-card">
<article class="alpha-card">
<span class="alpha-id">gtja191_114</span>
<pre class="formula-block"><code>((RANK(DELAY(((HIGH-LOW)/(SUM(CLOSE,5)/5)),2)) * RANK(RANK(VOLUME))) / (((HIGH-LOW)/(SUM(CLOSE,5)/5))/(VWAP-CLOSE)))</code></pre>
<p class="alpha-note">Mean IC = 0.0606, IR = 0.2926 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_114</code>).</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_171</span>
<pre class="formula-block"><code>-1*((l-c)*(o^5))/((c-h)*(c^5))</code></pre>
<p class="alpha-note">Mean IC = 0.0432, IR = 0.2690 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_171</code>).</p>
<p class="alpha-note">Mean IC = 0.0435, IR = 0.2299 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_171</code>).</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_120</span>
<pre class="formula-block"><code>rank(vwap-close)/rank(vwap+close)</code></pre>
<p class="alpha-note">Mean IC = 0.0452, IR = 0.2119 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_120</code>).</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_111</span>
<pre class="formula-block"><code>sma(v*((c-l)-(h-c))/(h-l),11,2)-sma(v*((c-l)-(h-c))/(h-l),4,2)</code></pre>
<p class="alpha-note">Mean IC = 0.0349, IR = 0.2232 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_111</code>).</p>
<p class="alpha-note">Mean IC = 0.0355, IR = 0.1888 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_111</code>).</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_163</span>
<pre class="formula-block"><code>rank(((-1*ret)*mean(v,20))*vwap*(high-close))</code></pre>
<p class="alpha-note">Mean IC = 0.0347, IR = 0.2008 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_163</code>).</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_002</span>
<pre class="formula-block"><code>(-1 * DELTA(((CLOSE - LOW) - (HIGH - CLOSE)) / (HIGH - LOW), 1))</code></pre>
<p class="alpha-note">Mean IC = 0.0262, IR = 0.1619 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_002</code>).</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_054</span>
<pre class="formula-block"><code>((-1*RANK((STD(ABS(CLOSE-OPEN),10)+(CLOSE-OPEN))+CORR(CLOSE,OPEN,10))))</code></pre>
<p class="alpha-note">Mean IC = 0.0272, IR = 0.1606 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_054</code>).</p>
<p class="alpha-note">Mean IC = 0.0342, IR = 0.1674 over the CSI 300 / 2018&ndash;2025 window. Formula reproduced verbatim from the registry (<code>__alpha_meta__["formula_latex"]</code> of <code>gtja191_163</code>).</p>
</article>
<h3>Three famously dead alphas</h3>
<p>Three formulas that worked, or were claimed to work, in the 2014 report but now sit comfortably
in the dead or reversed bucket:</p>
<h3>Three alphas that now point the other way</h3>
<p>The three alphas with the most negative mean IC. All three sit in the reversed bucket: they now
predict, with statistical significance, in the direction opposite to the report's sign.</p>
<article class="alpha-card">
<article class="alpha-card">
<span class="alpha-id">gtja191_124 (reversed)</span>
<pre class="formula-block"><code>(close-vwap)/decay_linear(rank(tsmax(close,30)),2)</code></pre>
<p class="alpha-note">Mean IC = -0.0518, IR = -0.2516. The most negative mean IC in the gtja191 zoo on CSI 300 / 2018&ndash;2025.</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_178 (reversed)</span>
<pre class="formula-block"><code>(c-delay(c,1))/delay(c,1)*v</code></pre>
<p class="alpha-note">Mean IC = -0.0327, IR = -0.1930. Worst-performing slice of the gtja191 zoo on CSI 300 / 2018&ndash;2025 by raw IC.</p>
<p class="alpha-note">Mean IC = -0.0329, IR = -0.1644. The second most negative.</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_137 (reversed)</span>
<pre class="formula-block"><code>see body</code></pre>
<p class="alpha-note">Mean IC = -0.0277, IR = -0.1556. Worst-performing slice of the gtja191 zoo on CSI 300 / 2018&ndash;2025 by raw IC.</p>
</article>
<article class="alpha-card">
<span class="alpha-id">gtja191_066 (reversed)</span>
<pre class="formula-block"><code>(CLOSE-MEAN(CLOSE,6))/MEAN(CLOSE,6)*100</code></pre>
<p class="alpha-note">Mean IC = -0.0270, IR = -0.1377. Worst-performing slice of the gtja191 zoo on CSI 300 / 2018&ndash;2025 by raw IC.</p>
<pre class="formula-block"><code>16*((c-dc1+(c-o)/2+dc1-do1)/MAX_term) * MAX(abs(h-dc1), abs(l-dc1))</code></pre>
<p class="alpha-note">Mean IC = -0.0300, IR = -0.1464. The third most negative. <code>MAX_term</code> is the report's three-way piecewise denominator, built from |h &minus; dc1|, |l &minus; dc1|, |h &minus; dl1| and |dc1 &minus; do1| (dc1, do1, dl1: the previous day's close, open and low); the module carries the full definition.</p>
</article>
<h3>One paragraph of reflection</h3>
<p>The temptation, after a survival study like this, is to over-generalise: "decay is inevitable;
formulaic alphas are dead." We don't think that's the right read. What this exercise teaches is
narrower and more useful: a meaningful fraction of a 12-year-old short-horizon catalogue still
produces signal, the survivors cluster in interpretable themes (volume-price interaction,
short-horizon volatility), and the dead ones cluster in equally interpretable themes (naive
reversal, simple turnover transforms). It tells us very little about whether the Kakushadze 101
formulas, or the Qlib 158 feature set, will decay at the same rate &mdash; those zoos have
different operator vocabularies and different intended universes (US equities, multi-horizon
respectively). Future work, separate bench runs.</p>
narrower and more useful: a small part of a 12-year-old short-horizon catalogue &mdash; 8 of 190
&mdash; still produces signal; the survivors cluster in an interpretable theme (bar geometry:
open-close-high-low decompositions), and the losers cluster in equally interpretable ones
(momentum, where nothing survives and five alphas flipped sign, and simple turnover transforms).
It tells us very little about whether the Kakushadze 101 formulas, or the Qlib 158 feature set,
will decay at the same rate &mdash; those zoos have different operator vocabularies and different
intended universes (US equities, multi-horizon respectively). Future work, separate bench runs.</p>
</section>
<section id="caveats">
@@ -554,11 +561,13 @@
muted), and some "dead" alphas here might revive on a CSI 1000 cut.</p>
<h3>Tushare data scope</h3>
<p>Our data feed (Tushare end-of-day OHLCV plus turnover, plus VWAP derived from amount) does not
include intraday tick or order-book information. A small number of GTJA 191 alphas in their
original formulation reference intraday quantities (depth-weighted VWAP, level-1 book imbalance);
for those we substitute the daily VWAP and flag the substitution in the per-alpha output. Those
alphas should be considered <em>not properly tested</em> here, not "dead".</p>
<p>Our data feed is Tushare end-of-day OHLCV and traded amount, forward-adjusted for splits and
dividends, with VWAP derived from amount; it has no intraday tick or order-book data. Five GTJA 191
formulas reference an input the panel does not carry &mdash; the benchmark index close
(<code>gtja191_075</code>, <code>_149</code>, <code>_181</code>, <code>_182</code>) or a
multi-factor regression residual (<code>gtja191_030</code>) &mdash; and use a proxy instead: the
cross-sectional mean close, or a weighted average of squared daily returns, as each alpha's notes
say. Treat those five as <em>not properly tested</em> here, not as "dead".</p>
<h3>8-year window is short</h3>
<p>By academic standards an 8-year window is on the short side. The classic Fama-French papers
@@ -568,16 +577,21 @@
window", not "dead forever".</p>
<h3>Survivorship bias in the universe</h3>
<p>The CSI 300 constituent list used here is the <em>current</em> index membership applied across
the full 2018&ndash;2025 window, not a point-in-time reconstruction of the index on each rebalance.
Stocks that were delisted, demoted, or removed during the window are absent; stocks added late get
their full price history backfilled. This is a standard form of survivorship bias and it
biases IC estimates upward in the long-only direction. The same caveat applies even more strongly
to any cross-zoo comparison against a US universe constructed the same way &mdash; for example, a
SP 500 run using today's constituent list will overstate alive counts relative to a true
point-in-time universe, and a 0% alive count there should be read as "decay plus survivorship
cancelled out the signal", not as a clean failure of the alpha family. Point-in-time index
membership is a planned upgrade.</p>
<p>The first version of this post applied the latest CSI 300 roster to the whole 2018&ndash;2025
window, so every name was present because it survived to the end &mdash; a hindsight-selected set,
which biases IC upward. The re-run masks each day's cross-section to the roster in force that day
(348 names were members at some point in the window). The <code>sp500</code> universe still uses
today's constituent list, so the same caveat applies to any cross-zoo comparison run on it &mdash;
its bench report says so &mdash; and a low alive count there should be read as "decay plus
survivorship", not as a clean failure of the alpha family.</p>
<h3>190 formulas, one test each</h3>
<p>Testing 190 formulas on the same window makes some look good by chance. The bench reports how
large the best IR would be expected to be if none of them carried any signal: 0.204 here. The best
observed IR, 0.293 (<code>gtja191_114</code>), survives that deflation (probability 0.9999). Two
more alive alphas are above 0.204 &mdash; <code>gtja191_171</code> (0.230) and
<code>gtja191_120</code> (0.212); the other five, with IRs between 0.130 and 0.189, are below it,
so read them as candidates rather than confirmed signals.</p>
<h3>No regime conditioning</h3>
<p>We average IC over the full 2018&ndash;2025 window. Many of these alphas are almost certainly
@@ -597,11 +611,11 @@
export TUSHARE_TOKEN=your_token_here
vibe-trading alpha bench --zoo gtja191 \
--universe csi300 \
--period 2020-2025 \
--period 2018-2025 \
--top 20</code></pre>
<p>You get back an HTML report saved to <code>~/.vibe-trading/reports/</code> with per-alpha IC,
IR, decay curve, and a sortable top-N table. The same command works against
<p>You get back an HTML report saved to <code>~/.vibe-trading/reports/</code> with the top alphas
by IR and the alphas that could not be computed. The same command works against
<code>--zoo alpha101</code> (Kakushadze 101 Formulaic Alphas, paper-faithful rewrite of the 2015
arXiv preprint) and <code>--zoo qlib158</code> (the Microsoft Qlib feature library, used under
Apache-2.0 with attribution). Cross-zoo comparison runs via <code>alpha compare</code>.</p>
@@ -613,7 +627,7 @@ vibe-trading alpha bench --zoo gtja191 \
</div>
<p>If you find an alpha whose survival classification surprises you &mdash; especially a survivor
we did not flag in the top-5 or a famously-dead one we kept in the alive bucket &mdash; please
we did not flag in the top 5, or a reversed one you expected to survive &mdash; please
open an issue with the alpha id and your reasoning. Community pull requests adding new zoos, new
universes (CSI 1000, NASDAQ 100, crypto majors) or new validation tooling are welcome under the
<a href="https://github.com/HKUDS/Vibe-Trading/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a>
+60 -6
View File
@@ -23,6 +23,9 @@ Security posture (v0.1.8):
* KaTeX is intentionally **not** loaded. ``formula_latex`` is shown as
raw escaped text inside ``<pre>``. A future revision can swap in a
server-side LaTeX→MathML pass that keeps CSP intact.
* Notes, nicknames and theme tags carry their Chinese in ``data-text-zh``
from ``wiki/alpha-library/i18n.zh.json`` — only when the stored English
still matches the manifest, so a stale translation is never shown.
* The header, footer and head block come from ``wiki/partials/`` (the
same markup ``sync_site_chrome.py`` writes into the hand-written
pages); labels carry ``data-i18n`` keys from ``wiki/locales/zh.json``.
@@ -68,6 +71,11 @@ except ImportError as exc: # pragma: no cover — Jinja2 is a project dep
_REPO_ROOT = Path(__file__).resolve().parents[2]
_DEFAULT_MANIFEST = _REPO_ROOT / "wiki" / "alpha-library" / "manifest.json"
_DEFAULT_OUTPUT = _REPO_ROOT / "wiki" / "alpha-library" / "content"
# Chinese notes, nicknames and theme names. Each note and nickname keeps the
# English it was translated from, so an edited English text is shown in
# English until its translation is updated (agent/tests/factors/
# test_alpha_library_translations.py fails in the meantime).
_I18N_PATH = _REPO_ROOT / "wiki" / "alpha-library" / "i18n.zh.json"
# Strict CSP for static content pages: same-origin scripts only, no inline.
_CSP_CONTENT = (
@@ -80,7 +88,7 @@ _CSP_CONTENT = (
)
# Cache-busting version of the site shell modules; matches the hand-written pages.
_SHELL_VERSION = "20260929d"
_SHELL_VERSION = "20260929e"
# Display names for zoos. Source of truth — do not depend on prose elsewhere.
_ZOO_DISPLAY: dict[str, dict[str, str]] = {
@@ -199,11 +207,11 @@ _ALPHA_PAGE_TEMPLATE = """<!doctype html>
<span>{{ alpha.id }}</span>
</p>
<h1>{{ alpha.id }}</h1>
{% if alpha.meta.nickname %}<p class="nickname">{{ alpha.meta.nickname }}</p>{% endif %}
{% if alpha.meta.nickname %}<p class="nickname"{% if zh.nickname %} data-text-zh="{{ zh.nickname }}"{% endif %}>{{ alpha.meta.nickname }}</p>{% endif %}
<h2 data-i18n="alpha.page.themes">Themes &amp; universe</h2>
<p>
{% for t in alpha.meta.theme %}<span class="tag">{{ t }}</span>{% endfor %}
{% for t in alpha.meta.theme %}<span class="tag" data-text-zh="{{ t | zh_theme }}">{{ t }}</span>{% endfor %}
{% for u in alpha.meta.universe %}<span class="tag">{{ u }}</span>{% endfor %}
{% for f in alpha.meta.frequency %}<span class="tag">{{ f }}</span>{% endfor %}
</p>
@@ -225,7 +233,7 @@ _ALPHA_PAGE_TEMPLATE = """<!doctype html>
{% if alpha.meta.notes %}
<h2 data-i18n="alpha.page.notes">Notes</h2>
<p class="notes">{{ alpha.meta.notes }}</p>
<p class="notes"{% if zh.notes %} data-text-zh="{{ zh.notes }}"{% endif %}>{{ alpha.meta.notes }}</p>
{% endif %}
<h2 data-i18n="alpha.page.run">Run it</h2>
@@ -290,7 +298,7 @@ _ZOO_PAGE_TEMPLATE = """<!doctype html>
{% for a in zoo.alphas %}
<tr>
<td><a href="{{ a.id }}.html">{{ a.id }}</a></td>
<td class="theme-cell">{{ a.meta.theme | join(", ") }}</td>
<td class="theme-cell" data-text-zh="{{ a.meta.theme | map('zh_theme') | join('、') }}">{{ a.meta.theme | join(", ") }}</td>
<td class="theme-cell">{{ a.meta.universe | join(", ") }}</td>
<td>{{ a.meta.decay_horizon }}</td>
<td>{{ a.meta.min_warmup_bars }}</td>
@@ -311,13 +319,47 @@ _ZOO_PAGE_TEMPLATE = """<!doctype html>
# ---------------------------------------------------------------------------
def _load_i18n() -> dict[str, Any]:
"""Load the Chinese notes / nicknames / theme names (empty when absent).
Returns:
``{"notes": {id: {"en", "zh"}}, "nicknames": {...}, "themes": {tag: zh}}``.
"""
if not _I18N_PATH.is_file():
return {"notes": {}, "nicknames": {}, "themes": {}}
return json.loads(_I18N_PATH.read_text(encoding="utf-8"))
_I18N = _load_i18n()
def _zh_texts(alpha: dict) -> dict[str, str | None]:
"""Chinese note and nickname for one alpha, or None where stale / missing.
Args:
alpha: One manifest alpha entry.
Returns:
``{"notes": ..., "nickname": ...}``.
"""
meta = alpha.get("meta", {})
out: dict[str, str | None] = {}
for field, bucket in (("notes", "notes"), ("nickname", "nicknames")):
entry = _I18N.get(bucket, {}).get(alpha.get("id"))
text = meta.get(field)
out[field] = entry["zh"] if entry and text and entry.get("en") == text else None
return out
def _build_env() -> Environment:
"""Create a Jinja2 environment with mandatory autoescape on."""
return Environment(
env = Environment(
autoescape=select_autoescape(["html", "xml"]),
trim_blocks=True,
lstrip_blocks=True,
)
env.filters["zh_theme"] = lambda tag: _I18N.get("themes", {}).get(tag, tag)
return env
def _chrome() -> dict[str, Any]:
@@ -351,6 +393,7 @@ def render_alpha_page(env: Environment, alpha: dict, zoo: dict) -> str:
zoo=zoo,
zoo_display=zoo_display,
csp=_CSP_CONTENT,
zh=_zh_texts(alpha),
**_chrome(),
)
@@ -472,6 +515,7 @@ def main(argv: list[str] | None = None) -> int:
zoo_count = 0
sample_alpha: dict | None = None
sample_html: str | None = None
untranslated: list[str] = []
for zoo in manifest.get("zoos", []):
zoo_id = zoo.get("zoo_id")
@@ -492,6 +536,10 @@ def main(argv: list[str] | None = None) -> int:
# regex, but never trust a JSON file from disk.
continue
html = render_alpha_page(env, alpha, zoo)
zh = _zh_texts(alpha)
meta = alpha.get("meta", {})
if (meta.get("notes") and not zh["notes"]) or (meta.get("nickname") and not zh["nickname"]):
untranslated.append(alpha_id)
_write(zoo_dir / f"{alpha_id}.html", html)
alpha_count += 1
if sample_alpha is None:
@@ -513,6 +561,12 @@ def main(argv: list[str] | None = None) -> int:
print("html escape FAIL", file=sys.stderr)
return 1
if untranslated:
print(
f"warning: {len(untranslated)} alphas show English notes/nicknames in Chinese "
f"(missing or stale in {_I18N_PATH.name}): {', '.join(untranslated[:10])}",
file=sys.stderr,
)
print(
f"built: {alpha_count} alphas across {zoo_count} zoos -> {out_dir}"
)
+10 -4
View File
@@ -4,8 +4,9 @@
// Language model: English is the HTML itself. Chinese comes from
// /locales/zh.json through data-i18n (text), data-i18n-html (markup),
// data-i18n-attr ("attr=key,attr=key") and data-href-zh (a link that has a
// Chinese counterpart). One stored choice, default English, never guessed
// from the browser.
// Chinese counterpart); generated pages carry their own Chinese inline in
// data-text-zh (the Alpha Library's per-alpha notes). One stored choice,
// default English, never guessed from the browser.
//
// A page whose URL is language-neutral (<html data-lang-neutral>) renders the
// stored language. A page whose URL is language-specific (<html
@@ -14,7 +15,7 @@
// site Chinese; its toggle goes to the counterpart page instead.
const LANG_KEY = "vibetrading-lang";
const LOCALE_VERSION = "20260929d";
const LOCALE_VERSION = "20260929e";
const THEME_KEY = "vibetrading-theme";
const STARS_CACHE_KEY = "vibetrading-github-stars";
const STARS_TTL_MS = 12 * 60 * 60 * 1000;
@@ -79,6 +80,11 @@ function translateElement(el, messages) {
el.setAttribute(attr, messages?.[key] ?? original);
}
}
const textZh = el.getAttribute("data-text-zh");
if (textZh) {
const original = remember(el, "i18nEnText", el.textContent);
el.textContent = messages ? textZh : original;
}
const hrefZh = el.getAttribute("data-href-zh");
if (hrefZh) {
const original = remember(el, "i18nEnHref", el.getAttribute("href") ?? "");
@@ -98,7 +104,7 @@ export async function translate(lang, root = document) {
messages = null;
}
}
root.querySelectorAll("[data-i18n], [data-i18n-html], [data-i18n-attr], [data-href-zh]").forEach((el) => {
root.querySelectorAll("[data-i18n], [data-i18n-html], [data-i18n-attr], [data-href-zh], [data-text-zh]").forEach((el) => {
translateElement(el, messages);
});
if (root === document) {
+3 -3
View File
@@ -18,10 +18,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
</head>
<body data-section="tutorials">
<!-- site-header:start -->
+3 -3
View File
@@ -21,10 +21,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
<style>
.tutorial {
width: min(940px, calc(100% - 32px));
+3 -3
View File
@@ -21,10 +21,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<link rel="stylesheet" href="/styles.css?v=20260929e">
<script src="/theme-init.js?v=20260929e"></script>
<!-- site-head:end -->
<script type="module" src="/main.js?v=20260929d"></script>
<script type="module" src="/main.js?v=20260929e"></script>
<style>
.tutorial {
width: min(940px, calc(100% - 32px));