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:
1 parent
d77c64e2ce
commit
137689c5c1
6 files changed
+1300
-4
No files matched your search
@@ -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 & 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>
|
||||
@@ -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–2025. Aggregate IC, theme survival rates, and the formulas that still hold up — reproducible with one CLI command.
|
||||
</p>
|
||||
<p class="post-card__meta"><span>Alpha Zoo</span> · <span>GTJA 191</span> · <span>CSI 300</span> · <span>8 years (2018–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 · ALPHA ZOO · 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 — how many of those
|
||||
formulas still produce reliable signal? We benchmarked all 191 on CSI 300, 2018–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 — positive mean IC above 0.02, t-stat above 2, and at least 55% of days with
|
||||
positive IC — on CSI 300 over 2018–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 >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–2025) — 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 — titled "191 个短周期交易型 alpha 因子"
|
||||
(<em>191 Short-Period Transactional Alpha Factors</em>) — 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–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–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–2021
|
||||
liquidity-driven rally, the 2022 drawdown, and the 2024–2025 sideways regime — 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 > 0.02, t-stat(IC) > 2, and ≥ 55% of days with
|
||||
IC > 0. All three conditions must hold.</li>
|
||||
<li><strong>Reversed.</strong> Mean IC < −0.02 and t-stat(IC) < −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| ≤ 0.02, or |t-stat| ≤ 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–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 — everyone expects
|
||||
decay — 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 — 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 — raw <em>volume-price interaction</em> (5%) and
|
||||
<em>short-horizon volatility</em> (8%) — 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–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–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–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–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–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–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–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–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 — 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–90 year windows. Some of the alphas we label "dead" may revive on a longer or
|
||||
differently-positioned sample — for example, if the 2026–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–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 — 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–2025 window. Many of these alphas are almost certainly
|
||||
regime-dependent — 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 — especially a survivor
|
||||
we did not flag in the top-5 or a famously-dead one we kept in the alive bucket — 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.
|
||||
@@ -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>
|
||||
/
|
||||
<a href="index.html">{{ zoo_display.name }}</a>
|
||||
/
|
||||
<span>{{ alpha.id }}</span>
|
||||
</p>
|
||||
<h1>{{ alpha.id }}</h1>
|
||||
{% if alpha.meta.nickname %}<p class="nickname">{{ alpha.meta.nickname }}</p>{% endif %}
|
||||
|
||||
<h2>Themes & 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> / <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 `<` etc.
|
||||
if "<" in formula and "<" in block and "<" not in block:
|
||||
return False
|
||||
if ">" in formula and ">" in block and ">" 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())
|
||||
Reference in new issue
Block a user