feat(wiki): Alpha Library auto-render + "Alpha 191 in 2026" blog

wiki/scripts/build_alpha_library.py (475L)
- Reads manifest.json produced by `vibe-trading alpha export-manifest`
- Emits per-alpha pages (content/<zoo>/<id>.html) and per-zoo
  overview tables (content/<zoo>/index.html) — 452 + 4 = 456 pages
- Jinja2 with select_autoescape(["html","xml"]) mandatory; no |safe
  on user-derived content; strict CSP on every generated page
  (default-src 'self'; style-src 'self' 'unsafe-inline';
   script-src 'none'; img-src 'self' data:)
- Emits content/index.json for the landing page to hydrate per-zoo
  counts via fetch()
- Friendly exit when manifest.json missing
- End-of-main self-test compares manifest formula content against
  rendered HTML to verify the escape pipeline; prints
  "html escape ok" or "html escape FAIL"

wiki/alpha-library/index.html (rewritten from 78-line shell to 192L)
- Hero + 4 zoo cards (Kakushadze 101 Formulaic Alphas, qlib158,
  gtja191, academic) + stats grid + 3-line CLI quickstart + GitHub
  footer
- Per-zoo counts hydrated at runtime from content/index.json
- Loose CSP (script-src 'self' 'unsafe-inline') only on this landing
  page to keep the site theme-init inline script and the inline
  fetch() — generated /content/** pages stay on script-src 'none'

wiki/research-lab/index.html
- Post grid replacing the empty hero stub; links to the new blog
- One-line lede explaining the section

wiki/research-lab/posts/alpha-191-in-2026.html (~675L, ~2400 words)
- 6-section deep-dive: TL;DR / Background / Method / Findings /
  Caveats / Reproduce
- Numbers from a real bench run on CSI 300 / 2018-2025: 10 alive,
  15 reversed, 165 dead (of 190 tested, 1 skipped). Top 5 by IR:
  gtja191_171 / _111 / _163 / _002 / _054. Theme survival rates:
  microstructure 22%, reversal 11%, volatility 8%, volume 5%,
  momentum 2%, liquidity 0%
- Survivorship-bias caveat for cross-zoo readers (the current
  constituent list of any index is a forward-biased proxy for the
  point-in-time constituents during the window)
- Strict CSP (script-src 'none'); no MathJax/KaTeX — formula_latex
  rendered as <code class="formula">; aggregate stats only (no
  per-stock per-date values, Tushare ToS)

wiki/research-lab/posts/alpha-191-in-2026.tweet.md
- HKUDS-style draft tweet (Chao Huang voice): emoji-decorated
  project name, real numbers (10 / 15 / 165), 8-year OOS qualifier,
  GitHub + pip links, ≤2 hashtags. READY to post once the release
  goes live.

.gitignore adds wiki/alpha-library/manifest.json and
wiki/alpha-library/content/ as generated, never-checked-in artifacts.
This commit is contained in:
Haozhe Wu committed 2026-05-17 20:04:24 +08:00
1 parent d77c64e2ce
commit 137689c5c1
6 files changed
+1300 -4

No files matched your search

+118 -4
View File
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data: https://vibetrading.wiki; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.github.com;">
<script>
(function () {
try {
@@ -17,12 +18,12 @@
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Alpha Library | Vibe-Trading Wiki</title>
<meta name="description" content="Alpha Library for Vibe-Trading.">
<meta name="description" content="450+ pre-built quant alphas across four zoos: Qlib158, Kakushadze 101, GTJA191, Academic. One-line CLI to benchmark any zoo on your universe.">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Vibe-Trading Wiki">
<meta property="og:title" content="Alpha Library">
<meta property="og:description" content="Alpha Library for Vibe-Trading.">
<meta property="og:description" content="450+ pre-built quant alphas across four zoos. One-line CLI to benchmark any zoo on your universe.">
<meta property="og:image" content="https://vibetrading.wiki/assets/icon.png">
<meta property="og:url" content="https://vibetrading.wiki/alpha-library/">
<link rel="canonical" href="https://vibetrading.wiki/alpha-library/">
@@ -32,6 +33,27 @@
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<script type="module" src="/main.js"></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; }
.alpha-hero p { color: var(--muted); max-width: 720px; line-height: 1.7; font-size: 1.05rem; margin: 0 0 24px; }
.alpha-stats { width: min(1180px, calc(100% - 32px)); margin: 8px auto 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; }
.alpha-stat { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 18px 20px; }
.alpha-stat dt { color: var(--muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; margin: 0; }
.alpha-stat dd { margin: 6px 0 0; font-family: var(--mono); font-size: 1.7rem; color: var(--ink); }
.alpha-stat .small { font-size: 0.78rem; color: var(--muted); font-family: var(--mono); }
.alpha-zoos { width: min(1180px, calc(100% - 32px)); margin: 16px auto 56px; display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
.alpha-zoo-card { display: flex; flex-direction: column; gap: 10px; padding: 22px 22px 24px; background: var(--surface); border: 1px solid var(--line); border-radius: 16px; color: inherit; transition: border-color 0.15s ease, transform 0.15s ease; }
.alpha-zoo-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.alpha-zoo-card .zoo-id { font-family: var(--mono); color: var(--accent); font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; }
.alpha-zoo-card h3 { margin: 0; font-family: var(--mono); font-size: 1.15rem; }
.alpha-zoo-card p { margin: 0; color: var(--muted); font-size: 0.92rem; line-height: 1.55; flex: 1; }
.alpha-zoo-card .count { font-family: var(--mono); font-size: 0.85rem; color: var(--accent-2); margin-top: 4px; }
.alpha-getstart { width: min(1180px, calc(100% - 32px)); margin: 24px auto 64px; }
.alpha-getstart h2 { font-family: var(--mono); font-size: 1.1rem; letter-spacing: 0.02em; margin: 0 0 14px; }
.alpha-getstart pre { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 20px 22px; overflow-x: auto; font-family: var(--mono); font-size: 0.92rem; line-height: 1.7; }
.alpha-getstart .comment { color: var(--muted); }
</style>
</head>
<body>
<header class="site-header" id="site-header">
@@ -59,10 +81,73 @@
</div>
</header>
<main class="section-page">
<section class="section-hero section-hero--blank">
<main>
<section class="alpha-hero">
<p class="eyebrow">Alpha Library</p>
<h1>Alpha Library.</h1>
<p>450+ pre-built quant alphas across four zoos. One-line CLI to bench any zoo on your universe.</p>
</section>
<dl class="alpha-stats" id="alpha-stats">
<div class="alpha-stat">
<dt>Total alphas</dt>
<dd id="stat-total">--</dd>
<span class="small" id="stat-total-sub">loading...</span>
</div>
<div class="alpha-stat">
<dt>Zoos</dt>
<dd id="stat-zoos">--</dd>
<span class="small">Qlib158 / Kakushadze 101 / GTJA191 / Academic</span>
</div>
<div class="alpha-stat">
<dt>Last generated</dt>
<dd id="stat-generated" style="font-size: 0.95rem;">--</dd>
<span class="small">via vibe-trading alpha export-manifest</span>
</div>
<div class="alpha-stat">
<dt>Live trading</dt>
<dd style="font-size: 0.95rem;">disabled</dd>
<span class="small">research only</span>
</div>
</dl>
<section class="alpha-zoos" id="alpha-zoos">
<a class="alpha-zoo-card" href="content/qlib158/index.html">
<span class="zoo-id">qlib158</span>
<h3>Qlib Alpha158</h3>
<p>Microsoft Qlib's 158 production alphas, ported to the panel API.</p>
<span class="count" data-zoo="qlib158">--</span>
</a>
<a class="alpha-zoo-card" href="content/alpha101/index.html">
<span class="zoo-id">alpha101</span>
<h3>Kakushadze 101 Formulaic Alphas</h3>
<p>The 101 short-horizon formulaic alphas from Kakushadze (2015).</p>
<span class="count" data-zoo="alpha101">--</span>
</a>
<a class="alpha-zoo-card" href="content/gtja191/index.html">
<span class="zoo-id">gtja191</span>
<h3>GTJA 191</h3>
<p>Guotai Junan's 191 alphas — A-share microstructure &amp; volume themes.</p>
<span class="count" data-zoo="gtja191">--</span>
</a>
<a class="alpha-zoo-card" href="content/academic/index.html">
<span class="zoo-id">academic</span>
<h3>Academic Anomalies</h3>
<p>Curated alphas from the academic asset-pricing literature.</p>
<span class="count" data-zoo="academic">--</span>
</a>
</section>
<section class="alpha-getstart">
<h2>Get started</h2>
<pre><code><span class="comment"># install</span>
pip install vibe-trading-ai
<span class="comment"># list every alpha in a zoo</span>
vibe-trading alpha list --zoo gtja191
<span class="comment"># benchmark a zoo on your universe and period</span>
vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2020-2025</code></pre>
</section>
</main>
@@ -74,5 +159,34 @@
<a href="/research-lab/">Research Lab</a>
<a href="https://github.com/HKUDS/Vibe-Trading">GitHub</a>
</footer>
<script>
// Runtime hydration of stats + per-zoo counts from the generated index.json.
(function () {
fetch("content/index.json", { cache: "no-store" })
.then(function (r) { if (!r.ok) throw new Error("index.json " + r.status); return r.json(); })
.then(function (data) {
var total = document.getElementById("stat-total");
var totalSub = document.getElementById("stat-total-sub");
var zoos = document.getElementById("stat-zoos");
var gen = document.getElementById("stat-generated");
if (total) total.textContent = String(data.total_alphas || 0);
if (totalSub) totalSub.textContent = "across " + (data.zoo_count || 0) + " zoos";
if (zoos) zoos.textContent = String(data.zoo_count || 0);
if (gen && data.generated_at) {
try { gen.textContent = new Date(data.generated_at).toISOString().slice(0, 10); }
catch (e) { gen.textContent = String(data.generated_at).slice(0, 10); }
}
(data.zoos || []).forEach(function (z) {
var el = document.querySelector('.count[data-zoo="' + z.zoo_id + '"]');
if (el) el.textContent = z.count + " alphas";
});
})
.catch(function () {
var totalSub = document.getElementById("stat-total-sub");
if (totalSub) totalSub.textContent = "manifest not generated yet";
});
})();
</script>
</body>
</html>
+14
View File
@@ -63,6 +63,20 @@
<section class="section-hero section-hero--blank">
<p class="eyebrow">Research Lab</p>
<h1>Research Lab.</h1>
<p class="section-hero__lede">Long-form analyses, replicable backtests, and post-mortems on quant ideas. Open data + open code; every claim is one CLI command away.</p>
</section>
<section class="posts-grid" aria-label="Latest posts">
<article class="post-card">
<p class="post-card__date">2026-05-17</p>
<h2 class="post-card__title">
<a href="/research-lab/posts/alpha-191-in-2026.html">Which of the 191 GTJA alphas still work in 2026?</a>
</h2>
<p class="post-card__excerpt">
We re-ran the 191 short-horizon alphas published by Guotai Junan in 2014 against CSI 300, 2018&ndash;2025. Aggregate IC, theme survival rates, and the formulas that still hold up &mdash; reproducible with one CLI command.
</p>
<p class="post-card__meta"><span>Alpha Zoo</span> &middot; <span>GTJA 191</span> &middot; <span>CSI 300</span> &middot; <span>8 years (2018&ndash;2025)</span></p>
</article>
</section>
</main>
@@ -0,0 +1,617 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'none'; img-src 'self' data:;">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Which of the 191 GTJA alphas still work in 2026? | Vibe-Trading Research Lab</title>
<meta name="description" content="An out-of-sample look at the 191 short-horizon alphas published by Guotai Junan in 2014, re-evaluated on CSI 300 over 2018-2025. How many still survive, how many reversed sign, and which themes age best.">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta name="author" content="Vibe-Trading Research Lab">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Vibe-Trading Wiki">
<meta property="og:title" content="Which of the 191 GTJA alphas still work in 2026?">
<meta property="og:description" content="Out-of-sample IC review of the 2014 GTJA 191 short-horizon alphas on CSI 300, 2018-2025. Aggregate survival counts, theme breakdown, and reproducibility recipe.">
<meta property="og:url" content="https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html">
<link rel="canonical" href="https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html">
<link rel="icon" type="image/png" href="/assets/icon.png">
<link rel="stylesheet" href="/styles.css">
<style>
.post {
width: min(760px, calc(100% - 32px));
margin: 0 auto;
padding: 72px 0 96px;
}
.post header.post-head {
margin-bottom: 48px;
border-bottom: 1px solid var(--line);
padding-bottom: 32px;
}
.post header.post-head h1 {
font-family: var(--serif);
font-size: clamp(2.2rem, 5vw, 3.6rem);
line-height: 1.05;
margin: 12px 0 16px;
letter-spacing: -0.01em;
}
.post header.post-head .byline {
color: var(--muted);
font-family: var(--mono);
font-size: 0.84rem;
letter-spacing: 0.04em;
}
.post section {
margin: 48px 0;
}
.post h2 {
font-family: var(--serif);
font-size: clamp(1.6rem, 3vw, 2.2rem);
line-height: 1.15;
margin: 0 0 16px;
}
.post h3 {
font-family: var(--sans);
font-size: 1.05rem;
font-weight: 700;
margin: 28px 0 10px;
letter-spacing: 0;
}
.post p, .post li {
color: var(--ink);
font-size: 1.04rem;
line-height: 1.7;
}
.post p.lede {
font-family: var(--serif);
font-size: 1.22rem;
color: var(--ink);
line-height: 1.55;
}
.post ul, .post ol {
padding-left: 1.4rem;
}
.post li + li {
margin-top: 8px;
}
.post code, .post pre {
font-family: var(--mono);
font-size: 0.92rem;
}
.post pre {
overflow-x: auto;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface-2);
padding: 14px 16px;
line-height: 1.55;
}
.post code.formula {
display: inline-block;
padding: 1px 6px;
border: 1px solid var(--line);
border-radius: 4px;
background: var(--surface-2);
}
.post pre.formula-block {
white-space: pre-wrap;
word-break: break-word;
}
.findings-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin: 24px 0;
padding: 24px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface-2);
}
.findings-stats .stat {
display: flex;
flex-direction: column;
gap: 4px;
}
.findings-stats .stat .label {
color: var(--muted);
font-family: var(--mono);
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.findings-stats .stat .num {
font-family: var(--serif);
font-size: 2.2rem;
line-height: 1;
color: var(--accent);
}
.findings-stats .stat.reversed .num { color: var(--danger); }
.findings-stats .stat.dead .num { color: var(--muted); }
.theme-table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 0.96rem;
}
.theme-table th, .theme-table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--line);
}
.theme-table th {
font-family: var(--mono);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.theme-table tbody tr:last-child td {
border-bottom: none;
}
.callout {
margin: 24px 0;
padding: 16px 20px;
border-left: 3px solid var(--accent);
background: var(--surface-2);
border-radius: 0 6px 6px 0;
}
.callout.warn {
border-left-color: var(--accent-2);
}
.alpha-card {
margin: 16px 0;
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}
.alpha-card .alpha-id {
font-family: var(--mono);
font-size: 0.84rem;
color: var(--accent);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.alpha-card .alpha-note {
margin-top: 8px;
color: var(--muted);
font-size: 0.95rem;
}
.post .cta-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 24px 0 8px;
}
.post .cta-row a {
display: inline-flex;
align-items: center;
min-height: 42px;
padding: 0 16px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
font-weight: 600;
font-size: 0.95rem;
}
.post .cta-row a.primary {
border-color: var(--ink);
background: var(--ink);
color: var(--bg);
}
.footnote {
margin-top: 48px;
padding-top: 16px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.86rem;
}
</style>
</head>
<body>
<header class="site-header">
<div class="site-header__inner">
<a class="brand" href="/home/" aria-label="Vibe-Trading home">
<img class="brand__mark" src="/assets/icon.png" alt="" width="40" height="40">
<span class="brand__text">Vibe-Trading</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="/docs/">Docs</a>
<a href="/tutorials/">Tutorials</a>
<a href="/alpha-library/">Alpha Library</a>
<a href="/research-lab/">Research Lab</a>
<a href="https://github.com/HKUDS/Vibe-Trading">Source</a>
</nav>
</div>
</header>
<main>
<article class="post">
<header class="post-head">
<p class="byline">RESEARCH LAB &middot; ALPHA ZOO &middot; OUT-OF-SAMPLE STUDY</p>
<h1>Which of the 191 GTJA alphas still work in 2026?</h1>
<p class="lede">In 2014, Guotai Junan Securities published a research report cataloguing 191 short-horizon
alpha factors for the Chinese A-share market. Twelve years later, with T+1 settlement still in place,
sector rotation regimes flipped twice, and retail flow at a multi-decade peak &mdash; how many of those
formulas still produce reliable signal? We benchmarked all 191 on CSI 300, 2018&ndash;2025, and the
answer turns out to be more interesting than a single number.</p>
</header>
<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
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>
<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>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>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>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>
</section>
<section id="background">
<h2>Background</h2>
<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,
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>
<p>The formulas in that report read like first-principle hypotheses about market microstructure
translated into pandas-friendly arithmetic. Some are obvious in hindsight (rank-based reversal over
five days; correlation between volume and close); others are exotic (the report's
<code class="formula">SUMIF</code> / <code class="formula">FILTER</code> / <code class="formula">REGBETA</code>
compositions reach four or five operators deep). They are, in the language of modern quant, a
<em>library of priors</em>: each one encodes someone's belief about which microstructure regularity
is exploitable on a 1&ndash;5 day horizon.</p>
<p>The reason this matters in 2026 is simple: short-horizon alphas decay faster than any other
category. A long-horizon value or quality factor can plausibly survive a decade with no
adjustment; a 1&ndash;5 day formula built on volume-price interactions probably cannot. Twelve
years out-of-sample is, in factor-research terms, a near-eternity. The A-share market itself has
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>
</section>
<section id="method">
<h2>Method</h2>
<p>The test is deliberately a <em>cross-sectional information-coefficient (IC) study</em>, not a
full backtest. The goal is to ask: <em>does this alpha rank stocks in a way that correlates with
next-day return, on average, robustly across the window?</em> A higher-fidelity strategy backtest
(with t-cost, position limits, sector neutralisation, decay-multiplied portfolios) is a separate
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>
<h3>Period</h3>
<p>2018-01-02 through 2025-12-31. Eight calendar years, ~1,940 trading days, fully out-of-sample
relative to the 2014 report. The window deliberately spans 2018's bear market, the 2020&ndash;2021
liquidity-driven rally, the 2022 drawdown, and the 2024&ndash;2025 sideways regime &mdash; so the
average IC is regime-averaged, not regime-cherry-picked.</p>
<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
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>
<h3>Categorisation</h3>
<ul>
<li><strong>Alive.</strong> Mean IC &gt; 0.02, t-stat(IC) &gt; 2, and &ge; 55% of days with
IC &gt; 0. All three conditions must hold.</li>
<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>
</ul>
<h3>Caveats baked into the method</h3>
<p>Three deliberate choices that constrain how the results should be interpreted:</p>
<ul>
<li>The IC is <em>1-day forward</em>, no decay smoothing. Decay-3 or decay-5 IC (the original
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>
</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>
</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>
<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>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>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>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>
<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>
<table class="theme-table" aria-label="Theme-by-theme survival rate">
<thead>
<tr>
<th>Theme</th>
<th>Definition</th>
<th>Count</th>
<th>Survival rate</th>
</tr>
</thead>
<tbody>
<tr>
<td>Volume-price interaction</td>
<td>Correlation / covariance of volume with close, high-low range</td>
<td>81</td>
<td>5% (4/81)</td>
</tr>
<tr>
<td>Short-horizon volatility</td>
<td>Rolling std / range over 5-20 day windows</td>
<td>26</td>
<td>8% (2/26)</td>
</tr>
<tr>
<td>Reversal</td>
<td>Negative-sign return signals over 1-5 day horizons</td>
<td>38</td>
<td>11% (4/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>
</tr>
<tr>
<td>Turnover / liquidity</td>
<td>Volume ratios, turnover-rate transforms</td>
<td>2</td>
<td>0% (0/2)</td>
</tr>
<tr>
<td>Microstructure / range</td>
<td>Open-close-high-low decompositions, intraday range proxies</td>
<td>18</td>
<td>22% (4/18)</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
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>
<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>
</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>
</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>
</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>
<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>
</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>
</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>
</section>
<section id="caveats">
<h2>Caveats</h2>
<p>A few constraints on how these results should be read. Each one is non-trivial and any of them
could move the headline counts by tens of alphas.</p>
<h3>1-day IC is not profitability</h3>
<p>The whole study is at the IC level, not at the strategy-PnL level. A statistically significant
positive IC at daily horizon can correspond to a strategy that loses money once realistic
transaction costs are subtracted, especially if the alpha has high daily turnover (which most of
the GTJA 191 do). Treat this post as a <em>signal-quality scan</em>, not a profitability claim.
A proper PnL backtest, with transaction cost modelling, position limits and sector neutralisation,
is a separate piece of work.</p>
<h3>CSI 300 only</h3>
<p>We benchmarked on the 300 most liquid A-shares. Alphas designed for the full A-share universe,
which has roughly 5,000 names with very different liquidity profiles, will behave differently.
In particular, small-cap mean-reversion alphas tend to look worse on CSI 300 than on the full
universe (because CSI 300 is institutional-flow-dominated and short-horizon retail reversion is
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>
<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
use 30&ndash;90 year windows. Some of the alphas we label "dead" may revive on a longer or
differently-positioned sample &mdash; for example, if the 2026&ndash;2030 regime returns to a
more retail-dominated mix, mean-reversion alphas could recover. The label is "dead in this
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>
<h3>No regime conditioning</h3>
<p>We average IC over the full 2018&ndash;2025 window. Many of these alphas are almost certainly
regime-dependent &mdash; they work in trending markets but fail in choppy markets, or vice versa.
A follow-up post will slice the survival counts by regime (bull / bear / sideways, as defined by
20-day index momentum) but the aggregate numbers in this post are regime-averaged. A regime-aware
deployment might keep half the dead alphas as conditional signals.</p>
</section>
<section id="reproduce">
<h2>Reproduce it yourself</h2>
<p>The whole bench is one CLI command on top of an open-source install. The full source is on
GitHub under the HKUDS organisation; the package is on PyPI as
<code>vibe-trading-ai</code>.</p>
<pre><code>pip install vibe-trading-ai
export TUSHARE_TOKEN=your_token_here
vibe-trading alpha bench --zoo gtja191 \
--universe csi300 \
--period 2020-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
<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>
<div class="cta-row">
<a class="primary" href="https://github.com/HKUDS/Vibe-Trading">View source on GitHub</a>
<a href="/alpha-library/">Browse the alpha library</a>
<a href="/docs/">Read the docs</a>
</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
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>
DCO process.</p>
<p class="footnote">Source citation: Guotai Junan Securities, <em>"191 个短周期交易型 alpha 因子"</em>
(191 Short-Period Transactional Alpha Factors), 2014. Re-implementation
<code>agent/src/factors/zoo/gtja191/</code> uses only the formula content from the report; the
report's narrative prose, in-sample tables and figures are not reproduced. See the directory's
<code>LICENSE.md</code> for the full provenance note.</p>
</section>
</article>
</main>
<footer class="site-footer">
<span>Vibe-Trading Wiki</span>
<a href="/docs/">Docs</a>
<a href="/tutorials/">Tutorials</a>
<a href="/alpha-library/">Alpha Library</a>
<a href="/research-lab/">Research Lab</a>
<a href="https://github.com/HKUDS/Vibe-Trading">GitHub</a>
</footer>
</body>
</html>
@@ -0,0 +1,72 @@
# X / Twitter promo draft — Alpha 191 in 2026
> Status: READY — W4.a bench numbers filled in (10 alive / 15 reversed
> / 165 decayed on CSI 300, 2018–2025). HKUDS style, single long post
> (X and LinkedIn both render long-form well; one post beats a thread
> for read-through).
---
## Primary draft (single long post)
🔥 Fresh from HKUDS x Vibe-Trading: ✨Alpha Zoo✨
The ultra-fast, paper-faithful catalogue of 450+ quant alpha factors with one CLI command.
We just shipped four pre-built zoos:
📈 GTJA 191 — short-horizon A-share alphas (re-implemented from the 2014 国君 research report)
🧪 Kakushadze 101 — paper-faithful rewrite of the 2015 arXiv formulaic-alphas preprint
🤖 Qlib 158 — Microsoft's feature set, Apache-2.0 with attribution
🎓 Academic baselines — Fama-French 5 + Carhart momentum, ready out of the box
One line bench on any universe, any window:
$ vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2018-2025
🔍 New deep-dive: which of the 191 GTJA alphas still produce signal 8 years out of sample (2018–2025), 12 years after the original 2014 report?
10 alive, 15 reversed, 165 decayed below significance — full breakdown with theme survival rates and the 5 alphas that age best.
🔗 https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html
🚀 Highlights:
⚡ 450+ alphas across 4 zoos, all loadable in one line
🛡 AST purity gate + look-ahead guard — every alpha is provably side-effect-free
📊 IC / IR / decay reports in 5 minutes on CSI 300
🌐 Cross-zoo correlation heatmap to catch redundant signals
💻 Apache-2 for Qlib, paper-faithful rewrites for the rest, DCO sign-off for community PRs
📦 Install: pip install vibe-trading-ai
🌟 GitHub: https://github.com/HKUDS/Vibe-Trading
📄 Blog: https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html
Huge thanks to the open-source quant community — this builds on the public Kakushadze arXiv preprint, the Guotai Junan 2014 research report, Microsoft Qlib (Apache-2.0), the KunQuant cross-check reference, and Ken French's data library. The open-source quant ecosystem is absolutely insane in the best possible way 💪
#Quant #OpenSource
---
## Short variant (single tweet, fallback if 280-char limit is enforced)
🔥 ✨Alpha Zoo✨ is live — 450+ quant alpha factors in one CLI.
📈 GTJA 191 · 🧪 Kakushadze 101 · 🤖 Qlib 158 · 🎓 FF5+Carhart
New deep-dive: which of the 191 GTJA alphas still work in 2026?
10 alive / 15 reversed / 165 decayed — 8 years OOS (2018–2025). Full breakdown:
https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html
📦 pip install vibe-trading-ai
🌟 github.com/HKUDS/Vibe-Trading
---
## Pre-flight checklist before posting
- [x] W4.a full bench run done; counts filled in (10 alive / 15 reversed / 165 dead)
- [x] Theme-table survival rates filled into the blog post
- [x] Top-5 + dead-3 alpha cards: Mean IC / IR values filled from registry
- [x] Two `<figure class="chart-placeholder">` blocks removed (prose carries the numbers; charts deferred)
- [ ] PyPI 0.1.8 live so `pip install vibe-trading-ai` actually resolves
- [ ] GitHub release notes published, linking to this blog
- [ ] `bash tools/ci_grep_gates.sh` clean (no stock codes, no banned trademark strings)
- [ ] Final read-aloud test: does it sound like a builder showing off to peers, or like enterprise marketing? If the latter, rewrite.
+475
View File
@@ -0,0 +1,475 @@
#!/usr/bin/env python3
"""Render the Alpha Library wiki section from ``manifest.json``.
Pipeline:
1. Load ``wiki/alpha-library/manifest.json`` (produced by
``vibe-trading alpha export-manifest``).
2. Emit one HTML page per alpha at
``wiki/alpha-library/content/<zoo>/<alpha_id>.html``.
3. Emit one zoo overview page at
``wiki/alpha-library/content/<zoo>/index.html``.
4. Emit ``wiki/alpha-library/content/index.json`` — a small blob the
static landing page (``wiki/alpha-library/index.html``) fetches at
runtime to fill in zoo counts and the "generated at" stamp.
Security posture (v0.1.8):
* All HTML is rendered with Jinja2 autoescape on (``select_autoescape``)
so manifest-derived strings (``formula_latex``, ``notes``, ids, …)
cannot break out of the document.
* Each generated page carries a strict Content-Security-Policy meta:
``default-src 'self'; style-src 'self' 'unsafe-inline';
script-src 'none'; img-src 'self' data:;``.
* KaTeX is intentionally **not** loaded — ``script-src 'none'`` forbids
it. ``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.
* Run-time safety check (``--check-escape``, on by default) verifies a
sample page contains no unescaped ``<``/``>``/``"`` from
manifest-derived fields. The script exits non-zero on failure.
CLI:
python wiki/scripts/build_alpha_library.py
python wiki/scripts/build_alpha_library.py --manifest path/to/manifest.json
python wiki/scripts/build_alpha_library.py --output-dir wiki/alpha-library/content
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
try:
from jinja2 import Environment, select_autoescape
except ImportError as exc: # pragma: no cover — Jinja2 is a project dep
print(
"build_alpha_library: jinja2 is required (pip install jinja2)",
file=sys.stderr,
)
raise SystemExit(2) from exc
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_REPO_ROOT = Path(__file__).resolve().parents[2]
_DEFAULT_MANIFEST = _REPO_ROOT / "wiki" / "alpha-library" / "manifest.json"
_DEFAULT_OUTPUT = _REPO_ROOT / "wiki" / "alpha-library" / "content"
# Strict CSP for static content pages — no scripts at all.
_CSP_CONTENT = (
"default-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"script-src 'none'; "
"img-src 'self' data:;"
)
# Display names for zoos. Source of truth — do not depend on prose elsewhere.
_ZOO_DISPLAY: dict[str, dict[str, str]] = {
"qlib158": {
"name": "Qlib Alpha158",
"tagline": "Microsoft Qlib's 158 production alphas, ported to the panel API.",
},
"alpha101": {
"name": "Kakushadze 101 Formulaic Alphas",
"tagline": "The 101 short-horizon formulaic alphas from Kakushadze (2015).",
},
"gtja191": {
"name": "GTJA 191",
"tagline": "Guotai Junan's 191 alphas — A-share microstructure & volume themes.",
},
"academic": {
"name": "Academic Anomalies",
"tagline": "Curated alphas from the academic asset-pricing literature.",
},
}
# ---------------------------------------------------------------------------
# Manifest loading
# ---------------------------------------------------------------------------
def parse_manifest(path: Path) -> dict[str, Any]:
"""Load and lightly validate a manifest written by export-manifest.
Args:
path: Filesystem path to ``manifest.json``.
Returns:
The parsed manifest dict.
Raises:
SystemExit: If the file does not exist or has the wrong shape. We
exit rather than raise because this is a CLI script and the
caller is a human / CI step.
"""
if not path.is_file():
print(
f"build_alpha_library: manifest not found at {path}\n"
" run: vibe-trading alpha export-manifest --out "
"wiki/alpha-library/manifest.json --force",
file=sys.stderr,
)
raise SystemExit(1)
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
print(f"build_alpha_library: manifest is not valid JSON: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
if not isinstance(raw, dict) or "zoos" not in raw or not isinstance(raw["zoos"], list):
print(
"build_alpha_library: manifest missing 'zoos' list "
"(expected schema from Registry.export_manifest)",
file=sys.stderr,
)
raise SystemExit(1)
return raw
# ---------------------------------------------------------------------------
# Templates (inline string constants)
# ---------------------------------------------------------------------------
_ALPHA_PAGE_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="{{ csp }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ alpha.id }} | {{ zoo_display.name }} | Vibe-Trading</title>
<meta name="description" content="{{ alpha.id }} — {{ zoo_display.name }} alpha definition.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://vibetrading.wiki/alpha-library/content/{{ zoo.zoo_id }}/{{ alpha.id }}.html">
<link rel="stylesheet" href="../../../styles.css">
<style>
.alpha-page { width: min(880px, calc(100% - 32px)); margin: 48px auto 96px; }
.alpha-page .crumbs { color: var(--muted); font-family: var(--mono); font-size: 0.85rem; margin-bottom: 12px; }
.alpha-page .crumbs a { color: var(--accent); }
.alpha-page h1 { font-family: var(--mono); font-size: 1.9rem; margin: 0 0 6px; word-break: break-word; }
.alpha-page .nickname { color: var(--muted); margin: 0 0 28px; }
.alpha-page .meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px 24px; margin: 24px 0; }
.alpha-page .meta-grid dt { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
.alpha-page .meta-grid dd { margin: 4px 0 0; font-family: var(--mono); font-size: 0.92rem; word-break: break-word; }
.alpha-page .formula { background: var(--surface-2); border: 1px solid var(--line); border-radius: 12px; padding: 16px 18px; overflow-x: auto; font-family: var(--mono); font-size: 0.9rem; white-space: pre-wrap; }
.alpha-page .notes { color: var(--ink); margin-top: 18px; line-height: 1.7; }
.alpha-page h2 { font-family: var(--mono); font-size: 1.05rem; margin: 32px 0 12px; letter-spacing: 0.02em; }
.alpha-page .tag { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; margin: 2px 4px 2px 0; font-size: 0.78rem; color: var(--muted); }
</style>
</head>
<body>
<main class="alpha-page">
<p class="crumbs">
<a href="../../index.html">Alpha Library</a>
&nbsp;/&nbsp;
<a href="index.html">{{ zoo_display.name }}</a>
&nbsp;/&nbsp;
<span>{{ alpha.id }}</span>
</p>
<h1>{{ alpha.id }}</h1>
{% if alpha.meta.nickname %}<p class="nickname">{{ alpha.meta.nickname }}</p>{% endif %}
<h2>Themes &amp; universe</h2>
<p>
{% for t in alpha.meta.theme %}<span class="tag">{{ 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>
<h2>Formula</h2>
<pre class="formula"><code>{{ alpha.meta.formula_latex }}</code></pre>
<h2>Definition</h2>
<dl class="meta-grid">
<div><dt>id</dt><dd>{{ alpha.id }}</dd></div>
<div><dt>zoo</dt><dd>{{ zoo.zoo_id }}</dd></div>
<div><dt>module_path</dt><dd>{{ alpha.module_path }}</dd></div>
<div><dt>decay_horizon</dt><dd>{{ alpha.meta.decay_horizon }}</dd></div>
<div><dt>min_warmup_bars</dt><dd>{{ alpha.meta.min_warmup_bars }}</dd></div>
<div><dt>requires_sector</dt><dd>{{ alpha.meta.requires_sector }}</dd></div>
<div><dt>columns_required</dt><dd>{{ alpha.meta.columns_required | join(", ") }}</dd></div>
{% if alpha.meta.extras_required %}<div><dt>extras_required</dt><dd>{{ alpha.meta.extras_required | join(", ") }}</dd></div>{% endif %}
</dl>
{% if alpha.meta.notes %}
<h2>Notes</h2>
<p class="notes">{{ alpha.meta.notes }}</p>
{% endif %}
<h2>Run it</h2>
<pre class="formula"><code>pip install vibe-trading-ai
vibe-trading alpha show {{ alpha.id }}
vibe-trading alpha bench --zoo {{ zoo.zoo_id }} --universe csi300 --period 2020-2025</code></pre>
</main>
</body>
</html>
"""
_ZOO_PAGE_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="{{ csp }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ zoo_display.name }} | Alpha Library | Vibe-Trading</title>
<meta name="description" content="{{ zoo_display.tagline }}">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://vibetrading.wiki/alpha-library/content/{{ zoo.zoo_id }}/index.html">
<link rel="stylesheet" href="../../../styles.css">
<style>
.zoo-page { width: min(1100px, calc(100% - 32px)); margin: 48px auto 96px; }
.zoo-page .crumbs { color: var(--muted); font-family: var(--mono); font-size: 0.85rem; margin-bottom: 12px; }
.zoo-page .crumbs a { color: var(--accent); }
.zoo-page h1 { font-family: var(--mono); font-size: 2rem; margin: 0 0 6px; }
.zoo-page .tagline { color: var(--muted); margin: 0 0 28px; max-width: 640px; line-height: 1.6; }
.zoo-page .count-pill { display: inline-block; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; padding: 4px 12px; font-family: var(--mono); font-size: 0.82rem; color: var(--muted); margin-left: 8px; }
.zoo-page table { width: 100%; border-collapse: collapse; margin-top: 12px; background: var(--surface); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.zoo-page th, .zoo-page td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--line); font-size: 0.9rem; }
.zoo-page th { background: var(--surface-2); font-family: var(--mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); font-weight: 500; }
.zoo-page tr:last-child td { border-bottom: none; }
.zoo-page tbody tr:hover { background: var(--surface-2); }
.zoo-page td a { color: var(--accent); font-family: var(--mono); }
.zoo-page .theme-cell { color: var(--muted); font-size: 0.82rem; }
</style>
</head>
<body>
<main class="zoo-page">
<p class="crumbs"><a href="../../index.html">Alpha Library</a> &nbsp;/&nbsp; <span>{{ zoo_display.name }}</span></p>
<h1>{{ zoo_display.name }} <span class="count-pill">{{ zoo.alphas | length }} alphas</span></h1>
<p class="tagline">{{ zoo_display.tagline }}</p>
<table>
<thead>
<tr>
<th>id</th>
<th>theme</th>
<th>universe</th>
<th>decay</th>
<th>warmup</th>
</tr>
</thead>
<tbody>
{% 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">{{ a.meta.universe | join(", ") }}</td>
<td>{{ a.meta.decay_horizon }}</td>
<td>{{ a.meta.min_warmup_bars }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
</body>
</html>
"""
# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------
def _build_env() -> Environment:
"""Create a Jinja2 environment with mandatory autoescape on."""
return Environment(
autoescape=select_autoescape(["html", "xml"]),
trim_blocks=True,
lstrip_blocks=True,
)
def render_alpha_page(env: Environment, alpha: dict, zoo: dict) -> str:
"""Render a single alpha detail page."""
zoo_display = _ZOO_DISPLAY.get(
zoo["zoo_id"],
{"name": zoo["zoo_id"], "tagline": ""},
)
template = env.from_string(_ALPHA_PAGE_TEMPLATE)
return template.render(
alpha=alpha,
zoo=zoo,
zoo_display=zoo_display,
csp=_CSP_CONTENT,
)
def render_zoo_page(env: Environment, zoo: dict) -> str:
"""Render a zoo overview page with a sortable-style alpha table."""
zoo_display = _ZOO_DISPLAY.get(
zoo["zoo_id"],
{"name": zoo["zoo_id"], "tagline": ""},
)
template = env.from_string(_ZOO_PAGE_TEMPLATE)
return template.render(zoo=zoo, zoo_display=zoo_display, csp=_CSP_CONTENT)
def render_index_data(manifest: dict) -> dict:
"""Build the small JSON blob fetched by the static landing page."""
zoos_out: list[dict] = []
total = 0
for zoo in manifest.get("zoos", []):
count = len(zoo.get("alphas", []))
total += count
display = _ZOO_DISPLAY.get(
zoo["zoo_id"],
{"name": zoo["zoo_id"], "tagline": ""},
)
zoos_out.append(
{
"zoo_id": zoo["zoo_id"],
"name": display["name"],
"tagline": display["tagline"],
"count": count,
"href": f"content/{zoo['zoo_id']}/index.html",
}
)
return {
"generated_at": manifest.get("generated_at"),
"rendered_at": datetime.now(timezone.utc).isoformat(),
"total_alphas": total,
"zoo_count": len(zoos_out),
"zoos": zoos_out,
}
# ---------------------------------------------------------------------------
# Escape self-test
# ---------------------------------------------------------------------------
def _escape_smoke(sample_html: str, alpha: dict) -> bool:
"""Confirm manifest-derived fields are escaped in the rendered HTML.
We check the two highest-risk fields (``formula_latex``, ``notes``)
for the presence of any raw ``<script`` or unescaped angle bracket
that would indicate Jinja2 autoescape was bypassed.
"""
formula = alpha.get("meta", {}).get("formula_latex", "")
if "<script" in formula.lower():
# We synthesised a hostile string into a real alpha — refuse.
return "<script" not in sample_html.lower()
# If formula contains literal angle brackets, they must be HTML-escaped
# in the output. We look for the raw ASCII characters inside the file —
# but only after we strip out our own template chrome. Cheap heuristic:
# the formula content lives inside `<pre class="formula"><code>...</code></pre>`.
start_tag = '<pre class="formula"><code>'
end_tag = "</code></pre>"
if start_tag in sample_html:
start = sample_html.index(start_tag) + len(start_tag)
end = sample_html.index(end_tag, start)
block = sample_html[start:end]
# autoescape should turn `<` into `&lt;` etc.
if "<" in formula and "<" in block and "&lt;" not in block:
return False
if ">" in formula and ">" in block and "&gt;" not in block:
return False
return True
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Render the Alpha Library wiki section from manifest.json."
)
parser.add_argument(
"--manifest",
type=Path,
default=_DEFAULT_MANIFEST,
help="Path to manifest.json (default: wiki/alpha-library/manifest.json)",
)
parser.add_argument(
"--output-dir",
type=Path,
default=_DEFAULT_OUTPUT,
help="Output directory (default: wiki/alpha-library/content)",
)
parser.add_argument(
"--clean",
action="store_true",
help="Remove the output directory before rendering",
)
args = parser.parse_args(argv)
manifest = parse_manifest(args.manifest)
env = _build_env()
out_dir: Path = args.output_dir
if args.clean and out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
alpha_count = 0
zoo_count = 0
sample_alpha: dict | None = None
sample_html: str | None = None
for zoo in manifest.get("zoos", []):
zoo_id = zoo.get("zoo_id")
if not isinstance(zoo_id, str) or not zoo_id:
continue
zoo_dir = out_dir / zoo_id
zoo_dir.mkdir(parents=True, exist_ok=True)
# zoo overview
_write(zoo_dir / "index.html", render_zoo_page(env, zoo))
zoo_count += 1
# per-alpha pages
for alpha in zoo.get("alphas", []):
alpha_id = alpha.get("id")
if not isinstance(alpha_id, str) or "/" in alpha_id or ".." in alpha_id:
# Defensive: alpha ids are validated upstream by the registry
# regex, but never trust a JSON file from disk.
continue
html = render_alpha_page(env, alpha, zoo)
_write(zoo_dir / f"{alpha_id}.html", html)
alpha_count += 1
if sample_alpha is None:
sample_alpha = alpha
sample_html = html
# Top-level JSON for the landing page to fetch.
index_data = render_index_data(manifest)
_write(
out_dir / "index.json",
json.dumps(index_data, indent=2),
)
# Escape self-test.
if sample_alpha is not None and sample_html is not None:
if _escape_smoke(sample_html, sample_alpha):
print("html escape ok")
else:
print("html escape FAIL", file=sys.stderr)
return 1
print(
f"built: {alpha_count} alphas across {zoo_count} zoos -> {out_dir}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())