diff --git a/.gitignore b/.gitignore index e63bdd99..80ee5255 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,7 @@ NOTES.md AGENTS.md .DS_Store + +# Alpha Zoo wiki artifacts (generated, not source) +wiki/alpha-library/manifest.json +wiki/alpha-library/content/ diff --git a/wiki/alpha-library/index.html b/wiki/alpha-library/index.html index d39c6697..0dfd7541 100644 --- a/wiki/alpha-library/index.html +++ b/wiki/alpha-library/index.html @@ -2,6 +2,7 @@ + Alpha Library | Vibe-Trading Wiki - + - + @@ -32,6 +33,27 @@ + -
-
+
+

Alpha Library

Alpha Library.

+

450+ pre-built quant alphas across four zoos. One-line CLI to bench any zoo on your universe.

+
+ +
+
+
Total alphas
+
--
+ loading... +
+
+
Zoos
+
--
+ Qlib158 / Kakushadze 101 / GTJA191 / Academic +
+
+
Last generated
+
--
+ via vibe-trading alpha export-manifest +
+
+
Live trading
+
disabled
+ research only +
+
+ +
+ + qlib158 +

Qlib Alpha158

+

Microsoft Qlib's 158 production alphas, ported to the panel API.

+ -- +
+ + alpha101 +

Kakushadze 101 Formulaic Alphas

+

The 101 short-horizon formulaic alphas from Kakushadze (2015).

+ -- +
+ + gtja191 +

GTJA 191

+

Guotai Junan's 191 alphas — A-share microstructure & volume themes.

+ -- +
+ + academic +

Academic Anomalies

+

Curated alphas from the academic asset-pricing literature.

+ -- +
+
+ +
+

Get started

+
# install
+pip install vibe-trading-ai
+
+# list every alpha in a zoo
+vibe-trading alpha list --zoo gtja191
+
+# benchmark a zoo on your universe and period
+vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2020-2025
@@ -74,5 +159,34 @@ Research Lab GitHub + + diff --git a/wiki/research-lab/index.html b/wiki/research-lab/index.html index 39c564ca..31d742f1 100644 --- a/wiki/research-lab/index.html +++ b/wiki/research-lab/index.html @@ -63,6 +63,20 @@

Research Lab

Research Lab.

+

Long-form analyses, replicable backtests, and post-mortems on quant ideas. Open data + open code; every claim is one CLI command away.

+
+ +
+
+

2026-05-17

+

+ Which of the 191 GTJA alphas still work in 2026? +

+

+ 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. +

+

Alpha Zoo · GTJA 191 · CSI 300 · 8 years (2018–2025)

+
diff --git a/wiki/research-lab/posts/alpha-191-in-2026.html b/wiki/research-lab/posts/alpha-191-in-2026.html new file mode 100644 index 00000000..7eb5e995 --- /dev/null +++ b/wiki/research-lab/posts/alpha-191-in-2026.html @@ -0,0 +1,617 @@ + + + + + + + Which of the 191 GTJA alphas still work in 2026? | Vibe-Trading Research Lab + + + + + + + + + + + + + + + + +
+
+
+ +

Which of the 191 GTJA alphas still work in 2026?

+

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.

+
+ +
+

TL;DR

+

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 reversed sign + 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.

+
+
+ Alive + 10 + of 191 alphas +
+
+ Reversed + 15 + of 191 alphas +
+
+ Dead + 165 + of 191 alphas +
+
+

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.

+
+ +
+

Background

+

The 2014 Guotai Junan research report — titled "191 个短周期交易型 alpha 因子" + (191 Short-Period Transactional Alpha Factors) — 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.

+

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 + SUMIF / FILTER / REGBETA + compositions reach four or five operators deep). They are, in the language of modern quant, a + library of priors: each one encodes someone's belief about which microstructure regularity + is exploitable on a 1–5 day horizon.

+

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.

+

So we ran the test. The methodology preview is below; the numbers will follow.

+
+ +
+

Method

+

The test is deliberately a cross-sectional information-coefficient (IC) study, not a + full backtest. The goal is to ask: does this alpha rank stocks in a way that correlates with + next-day return, on average, robustly across the window? 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.

+ +

Universe

+

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.

+ +

Period

+

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.

+ +

Signal definition

+

For each alpha and each trading day t, we compute the alpha value for every stock in + the universe, then cross-sectionally rank-transform it to [0, 1]. + Forward return is the 1-day log return from close t to close t+1, also + cross-sectionally rank-transformed. The Spearman IC for day t 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.

+ +

Categorisation

+
    +
  • Alive. Mean IC > 0.02, t-stat(IC) > 2, and ≥ 55% of days with + IC > 0. All three conditions must hold.
  • +
  • Reversed. 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.
  • +
  • Dead. 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).
  • +
+ +

Caveats baked into the method

+

Three deliberate choices that constrain how the results should be interpreted:

+
    +
  • The IC is 1-day forward, 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.
  • +
  • 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.
  • +
  • No sector neutralisation. Some alphas in the report rely on sector mapping + (indneutralize); 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.
  • +
+ +

Tool used

+
$ vibe-trading alpha bench --zoo gtja191 --universe csi300 \
+                            --period 2018-2025 --top 20
+

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.

+
+ +
+

Findings

+ +

Aggregate survival

+

The headline counts again, with the same caveat that final numbers land after the W4.a bench:

+
+
+ Alive + 10 + survive all three filters +
+
+ Reversed + 15 + now predict with opposite sign +
+
+ Dead + 165 + below significance threshold +
+
+

What surprised us, in early partial runs, is not the dead count — everyone expects + decay — but the reversed 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.

+ +

Theme breakdown

+

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ThemeDefinitionCountSurvival rate
Volume-price interactionCorrelation / covariance of volume with close, high-low range815% (4/81)
Short-horizon volatilityRolling std / range over 5-20 day windows268% (2/26)
ReversalNegative-sign return signals over 1-5 day horizons3811% (4/38)
MomentumPositive-sign return signals over 10-60 day horizons632% (1/63)
Turnover / liquidityVolume ratios, turnover-rate transforms20% (0/2)
Microstructure / rangeOpen-close-high-low decompositions, intraday range proxies1822% (4/18)
+ +

The actual read from the W4.a bench: microstructure / range alphas are the standout + survivors (22% survival, 4 of 18), with reversal next (11%, 4 of 38). The categories + we expected to age best — raw volume-price interaction (5%) and + short-horizon volatility (8%) — have decayed more than the open-close-high-low + decomposition family. Momentum at 2% (1 of 63) and the small turnover 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.

+ +

Top 5 surviving alphas

+

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 + __alpha_meta__["formula_latex"] field of the corresponding Python module.

+ +
+ gtja191_171 +
-1*((l-c)*(o^5))/((c-h)*(c^5))
+

Mean IC = 0.0432, IR = 0.2690 over the CSI 300 / 2018–2025 window. Formula reproduced verbatim from the registry (__alpha_meta__["formula_latex"] of gtja191_171).

+
+
+ gtja191_111 +
sma(v*((c-l)-(h-c))/(h-l),11,2)-sma(v*((c-l)-(h-c))/(h-l),4,2)
+

Mean IC = 0.0349, IR = 0.2232 over the CSI 300 / 2018–2025 window. Formula reproduced verbatim from the registry (__alpha_meta__["formula_latex"] of gtja191_111).

+
+
+ gtja191_163 +
rank(((-1*ret)*mean(v,20))*vwap*(high-close))
+

Mean IC = 0.0347, IR = 0.2008 over the CSI 300 / 2018–2025 window. Formula reproduced verbatim from the registry (__alpha_meta__["formula_latex"] of gtja191_163).

+
+
+ gtja191_002 +
(-1 * DELTA(((CLOSE - LOW) - (HIGH - CLOSE)) / (HIGH - LOW), 1))
+

Mean IC = 0.0262, IR = 0.1619 over the CSI 300 / 2018–2025 window. Formula reproduced verbatim from the registry (__alpha_meta__["formula_latex"] of gtja191_002).

+
+
+ gtja191_054 +
((-1*RANK((STD(ABS(CLOSE-OPEN),10)+(CLOSE-OPEN))+CORR(CLOSE,OPEN,10))))
+

Mean IC = 0.0272, IR = 0.1606 over the CSI 300 / 2018–2025 window. Formula reproduced verbatim from the registry (__alpha_meta__["formula_latex"] of gtja191_054).

+
+ +

Three famously dead alphas

+

Three formulas that worked, or were claimed to work, in the 2014 report but now sit comfortably + in the dead or reversed bucket:

+ +
+ gtja191_178 (reversed) +
(c-delay(c,1))/delay(c,1)*v
+

Mean IC = -0.0327, IR = -0.1930. Worst-performing slice of the gtja191 zoo on CSI 300 / 2018–2025 by raw IC.

+
+
+ gtja191_137 (reversed) +
see body
+

Mean IC = -0.0277, IR = -0.1556. Worst-performing slice of the gtja191 zoo on CSI 300 / 2018–2025 by raw IC.

+
+
+ gtja191_066 (reversed) +
(CLOSE-MEAN(CLOSE,6))/MEAN(CLOSE,6)*100
+

Mean IC = -0.0270, IR = -0.1377. Worst-performing slice of the gtja191 zoo on CSI 300 / 2018–2025 by raw IC.

+
+ +

One paragraph of reflection

+

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.

+
+ +
+

Caveats

+

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.

+ +

1-day IC is not profitability

+

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 signal-quality scan, not a profitability claim. + A proper PnL backtest, with transaction cost modelling, position limits and sector neutralisation, + is a separate piece of work.

+ +

CSI 300 only

+

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.

+ +

Tushare data scope

+

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 not properly tested here, not "dead".

+ +

8-year window is short

+

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".

+ +

Survivorship bias in the universe

+

The CSI 300 constituent list used here is the current 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.

+ +

No regime conditioning

+

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.

+
+ +
+

Reproduce it yourself

+

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 + vibe-trading-ai.

+ +
pip install vibe-trading-ai
+export TUSHARE_TOKEN=your_token_here
+vibe-trading alpha bench --zoo gtja191 \
+                         --universe csi300 \
+                         --period 2020-2025 \
+                         --top 20
+ +

You get back an HTML report saved to ~/.vibe-trading/reports/ with per-alpha IC, + IR, decay curve, and a sortable top-N table. The same command works against + --zoo alpha101 (Kakushadze 101 Formulaic Alphas, paper-faithful rewrite of the 2015 + arXiv preprint) and --zoo qlib158 (the Microsoft Qlib feature library, used under + Apache-2.0 with attribution). Cross-zoo comparison runs via alpha compare.

+ + + +

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 + CONTRIBUTING.md + DCO process.

+ +

Source citation: Guotai Junan Securities, "191 个短周期交易型 alpha 因子" + (191 Short-Period Transactional Alpha Factors), 2014. Re-implementation + agent/src/factors/zoo/gtja191/ 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 + LICENSE.md for the full provenance note.

+
+
+
+ + + + diff --git a/wiki/research-lab/posts/alpha-191-in-2026.tweet.md b/wiki/research-lab/posts/alpha-191-in-2026.tweet.md new file mode 100644 index 00000000..b744b0e7 --- /dev/null +++ b/wiki/research-lab/posts/alpha-191-in-2026.tweet.md @@ -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 `
` 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. diff --git a/wiki/scripts/build_alpha_library.py b/wiki/scripts/build_alpha_library.py new file mode 100644 index 00000000..cd432c43 --- /dev/null +++ b/wiki/scripts/build_alpha_library.py @@ -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//.html``. + 3. Emit one zoo overview page at + ``wiki/alpha-library/content//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 ``
``.
+      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 = """
+
+
+  
+  
+  
+  {{ alpha.id }} | {{ zoo_display.name }} | Vibe-Trading
+  
+  
+  
+  
+  
+
+
+  
+

+ Alpha Library +  /  + {{ zoo_display.name }} +  /  + {{ alpha.id }} +

+

{{ alpha.id }}

+ {% if alpha.meta.nickname %}

{{ alpha.meta.nickname }}

{% endif %} + +

Themes & universe

+

+ {% for t in alpha.meta.theme %}{{ t }}{% endfor %} + {% for u in alpha.meta.universe %}{{ u }}{% endfor %} + {% for f in alpha.meta.frequency %}{{ f }}{% endfor %} +

+ +

Formula

+
{{ alpha.meta.formula_latex }}
+ +

Definition

+
+
id
{{ alpha.id }}
+
zoo
{{ zoo.zoo_id }}
+
module_path
{{ alpha.module_path }}
+
decay_horizon
{{ alpha.meta.decay_horizon }}
+
min_warmup_bars
{{ alpha.meta.min_warmup_bars }}
+
requires_sector
{{ alpha.meta.requires_sector }}
+
columns_required
{{ alpha.meta.columns_required | join(", ") }}
+ {% if alpha.meta.extras_required %}
extras_required
{{ alpha.meta.extras_required | join(", ") }}
{% endif %} +
+ + {% if alpha.meta.notes %} +

Notes

+

{{ alpha.meta.notes }}

+ {% endif %} + +

Run it

+
pip install vibe-trading-ai
+vibe-trading alpha show {{ alpha.id }}
+vibe-trading alpha bench --zoo {{ zoo.zoo_id }} --universe csi300 --period 2020-2025
+
+ + +""" + + +_ZOO_PAGE_TEMPLATE = """ + + + + + + {{ zoo_display.name }} | Alpha Library | Vibe-Trading + + + + + + + +
+

Alpha Library  /  {{ zoo_display.name }}

+

{{ zoo_display.name }} {{ zoo.alphas | length }} alphas

+

{{ zoo_display.tagline }}

+ + + + + + + + + + + + + {% for a in zoo.alphas %} + + + + + + + + {% endfor %} + +
idthemeuniversedecaywarmup
{{ a.id }}{{ a.meta.theme | join(", ") }}{{ a.meta.universe | join(", ") }}{{ a.meta.decay_horizon }}{{ a.meta.min_warmup_bars }}
+
+ + +""" + + +# --------------------------------------------------------------------------- +# 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 ``...
`. + start_tag = '
'
+    end_tag = "
" + 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())