Skip to content

Commit ccd1177

Browse files
committed
Initial release
CrossSectionCN: a China A-share companion to the Open Source Asset Pricing project. Reimplements the documented cross-sectional return predictor literature against A-share data under China-specific timing, disclosure, and listing-board constraints. - Data pipeline (ingest, normalize, intermediate, factor compute) - Factor registry in factor_doc.csv - Quarto site with per-factor detail pages, family rollups, return leaderboard, and monthly heatmap - GitHub Actions workflow that rerenders and publishes the site from the committed JSON snapshots on every push
0 parents  commit ccd1177

100 files changed

Lines changed: 154438 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
TUSHARE_TOKEN=
2+
CNCS_DATA_SOURCE=tushare
3+
CNCS_MAX_STOCKS=0
4+
CNCS_OHLCV_BATCH=25

.github/workflows/publish.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Publish Quarto site to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "quarto/**"
8+
- "reports/build_quarto_data.py"
9+
- ".github/workflows/publish.yml"
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
concurrency:
18+
group: pages
19+
cancel-in-progress: true
20+
21+
jobs:
22+
build:
23+
name: Render Quarto site
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
29+
- name: Set up Quarto
30+
uses: quarto-dev/quarto-actions/setup@v2
31+
with:
32+
version: 1.9.37
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v5
36+
with:
37+
python-version: "3.11"
38+
39+
# build_quarto_data.py is intentionally self-contained: it depends only on
40+
# the Python standard library and reads JSON files committed under
41+
# quarto/_data/, so we don't need polars / pyarrow / tushare in CI.
42+
- name: Generate factor pages and snippets
43+
run: python -m reports.build_quarto_data
44+
45+
- name: Render Quarto site
46+
working-directory: quarto
47+
run: quarto render
48+
49+
- name: Upload Pages artifact
50+
uses: actions/upload-pages-artifact@v3
51+
with:
52+
path: quarto/_site
53+
54+
deploy:
55+
name: Deploy to GitHub Pages
56+
needs: build
57+
runs-on: ubuntu-latest
58+
environment:
59+
name: github-pages
60+
url: ${{ steps.deployment.outputs.page_url }}
61+
steps:
62+
- name: Deploy
63+
id: deployment
64+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
pyData/
2+
*.parquet
3+
*.csv
4+
!factor_doc.csv
5+
__pycache__/
6+
*.pyc
7+
.pytest_cache/
8+
.env
9+
.venv/
10+
docs/
11+
*.egg-info/
12+
node_modules/
13+
quarto/_site/
14+
quarto/.quarto/
15+
quarto/_generated/
16+
quarto/factors/
17+
quarto/families/
18+
reference/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Yanpeng Wang
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# CrossSectionCN
2+
3+
A companion to the Open Source Asset Pricing project ([Chen and Zimmermann, 2022](https://www.openassetpricing.com/)) for the China A-share market.
4+
5+
Site: <https://yanpgwang.github.io/CrossSectionCN/>
6+
7+
This repository re-tests documented cross-sectional return predictors against China A-share data, following the documentation discipline of [OpenSourceAP/CrossSection](https://github.com/OpenSourceAP/CrossSection/): each signal carries a definition, literature attribution, expected return direction, portfolio construction rule, and a record of known data limitations. None of the original U.S.-market results are republished; every predictor is rebuilt under China-specific timing, disclosure, and listing-board constraints. The project is descriptive rather than prescriptive.
8+
9+
Most readers will want the [site](https://yanpgwang.github.io/CrossSectionCN/); the repository is provided for inspection, citation, and contribution. The site is regenerated and published from the snapshots under `quarto/_data/` by the workflow in `.github/workflows/publish.yml`. Refreshing the underlying panel is a deliberate local operation, orchestrated by `master.py` and `reports/build_site_data.py`.
10+
11+
## Acknowledgments
12+
13+
This project follows the documentation structure of:
14+
15+
- Chen, A. Y. and Zimmermann, T. (2022). Open source cross-sectional asset pricing. *Critical Finance Review*, 11 (2): 207–264. <https://www.openassetpricing.com/>
16+
- OpenSourceAP/CrossSection — reference implementation for the U.S. market. <https://github.com/OpenSourceAP/CrossSection/>
17+
18+
No code or data from the source projects is redistributed.
19+
20+
## License
21+
22+
[MIT](LICENSE).

config.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""ABOUTME: Global config for CrossSectionCN.
2+
ABOUTME: Paths, date ranges, universe filters."""
3+
import os
4+
from datetime import date
5+
from pathlib import Path
6+
7+
from dotenv import load_dotenv
8+
9+
load_dotenv()
10+
11+
ROOT = Path(__file__).parent
12+
DATA_RAW_VENDOR = ROOT / "pyData" / "raw_vendor"
13+
DATA_CANONICAL = ROOT / "pyData" / "canonical"
14+
# Backward-compatible alias: existing intermediate/factor code reads canonical tables here.
15+
DATA_RAW = DATA_CANONICAL
16+
DATA_INTERMEDIATE = ROOT / "pyData" / "intermediate"
17+
DATA_FACTORS = ROOT / "pyData" / "factors"
18+
19+
START_DATE = date(2010, 1, 1)
20+
END_DATE = date.today()
21+
22+
UNIVERSE_EXCHANGES = ["SH", "SZ"]
23+
UNIVERSE_BOARDS = ["MAIN", "GEM", "STAR"]
24+
25+
FACTOR_DOC_PATH = ROOT / "factor_doc.csv"
26+
27+
DATA_SOURCE = os.environ.get("CNCS_DATA_SOURCE", "tushare").lower()
28+
TUSHARE_TOKEN = os.environ.get("TUSHARE_TOKEN") or os.environ.get("TS_TOKEN")
29+
30+
# Dry-run / smoke: limit universe size (0 = no limit)
31+
MAX_STOCKS = int(os.environ.get("CNCS_MAX_STOCKS", "0") or "0")
32+
33+
# Parallel-ish batch size for OHLCV downloads (symbols per HTTP batch call)
34+
OHLCV_BATCH = int(os.environ.get("CNCS_OHLCV_BATCH", "25") or "25")
35+
36+
# Tushare official limits vary by endpoint/account; default leaves room below 500/min.
37+
TUSHARE_MAX_REQUESTS_PER_MINUTE = int(os.environ.get("CNCS_TUSHARE_RPM", "450") or "450")
38+
39+
for d in (DATA_RAW_VENDOR, DATA_CANONICAL, DATA_INTERMEDIATE, DATA_FACTORS):
40+
d.mkdir(parents=True, exist_ok=True)

data/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Raw + intermediate data pipeline."""

data/capabilities/tushare.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
vendor: tushare
2+
capabilities:
3+
daily_ohlcv: true
4+
adjusted_prices: true
5+
daily_valuation: true
6+
pit_financials: partial
7+
financial_actual_disclosure_date: true
8+
historical_st: true
9+
suspension_daily: true
10+
index_membership: true
11+
index_daily: true
12+
snapshot_industry: true
13+
pit_industry: false
14+
intraday_quotes: false
15+
tick_quotes: false
16+
analyst_forecasts: partial
17+
northbound_holdings: true
18+
margin_trading: true
19+
money_flow: true
20+
notes:
21+
- "Tushare is the bootstrap open-source vendor, not the canonical architecture boundary."
22+
- "Capabilities marked partial require factor-level documentation before use."

data/contracts/raw_tables.yaml

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
tables:
2+
- name: stock_basic
3+
primary_key: [ts_code, list_status]
4+
date_columns: [list_date, delist_date]
5+
partition_keys: [snapshot_date]
6+
unit_notes: "snapshot table; include listed, delisted, and paused names"
7+
pit_available: false
8+
incremental_key: snapshot_date
9+
used_by: [universe, signal_master]
10+
known_limitations: "Market/industry metadata may be current snapshot rather than PIT history."
11+
12+
- name: daily
13+
primary_key: [ts_code, trade_date]
14+
date_columns: [trade_date]
15+
partition_keys: [trade_year, trade_month]
16+
unit_notes: "amount is yuan in canonical raw."
17+
pit_available: true
18+
incremental_key: trade_date
19+
used_by: [signal_master, price_predictors]
20+
known_limitations: "Rows can be absent on non-trading or fully suspended days depending on source."
21+
22+
- name: adj_factor
23+
primary_key: [ts_code, trade_date]
24+
date_columns: [trade_date]
25+
partition_keys: [trade_year, trade_month]
26+
unit_notes: "close_adj = close * adj_factor."
27+
pit_available: true
28+
incremental_key: trade_date
29+
used_by: [signal_master, returns]
30+
known_limitations: "Adjustment factors can be restated after corporate actions."
31+
32+
- name: daily_basic
33+
primary_key: [ts_code, trade_date]
34+
date_columns: [trade_date]
35+
partition_keys: [trade_year, trade_month]
36+
unit_notes: "total_mv and circ_mv are 10k yuan in canonical raw unless normalized later."
37+
pit_available: true
38+
incremental_key: trade_date
39+
used_by: [signal_master, valuation, liquidity]
40+
known_limitations: "Some fields can be null for early or special securities."
41+
42+
- name: income
43+
primary_key: [ts_code, report_period, notice_date, report_type]
44+
date_columns: [report_period, notice_date, final_notice_date]
45+
partition_keys: [report_year]
46+
unit_notes: "statement original monetary units from source; keep source raw values for audit."
47+
pit_available: true
48+
incremental_key: notice_date
49+
used_by: [pit_financials, profitability, valuation]
50+
known_limitations: "Restatements and update flags need audit in later versions."
51+
52+
- name: balancesheet
53+
primary_key: [ts_code, report_period, notice_date, report_type]
54+
date_columns: [report_period, notice_date, final_notice_date]
55+
partition_keys: [report_year]
56+
unit_notes: "statement original monetary units from source; keep source raw values for audit."
57+
pit_available: true
58+
incremental_key: notice_date
59+
used_by: [pit_financials, valuation, investment, quality]
60+
known_limitations: "Equity and share-cap fields differ by source and availability."
61+
62+
- name: cashflow
63+
primary_key: [ts_code, report_period, notice_date, report_type]
64+
date_columns: [report_period, notice_date, final_notice_date]
65+
partition_keys: [report_year]
66+
unit_notes: "statement original monetary units from source; keep source raw values for audit."
67+
pit_available: true
68+
incremental_key: notice_date
69+
used_by: [pit_financials, accruals, cashflow_predictors]
70+
known_limitations: "Operating cashflow can be null for some report types."
71+
72+
- name: fina_indicator
73+
primary_key: [ts_code, report_period, notice_date]
74+
date_columns: [report_period, notice_date]
75+
partition_keys: [report_year]
76+
unit_notes: "derived financial ratios; definitions must be documented before predictor use."
77+
pit_available: true
78+
incremental_key: notice_date
79+
used_by: [profitability, gross_profitability]
80+
known_limitations: "Derived definitions and null patterns differ across sources."
81+
82+
- name: disclosure_date
83+
primary_key: [ts_code, report_period]
84+
date_columns: [report_period, notice_date]
85+
partition_keys: [report_year]
86+
unit_notes: "notice_date is the best available actual disclosure date."
87+
pit_available: true
88+
incremental_key: notice_date
89+
used_by: [pit_financials]
90+
known_limitations: "Appointment dates can be revised; actual disclosure date is preferred."
91+
92+
- name: stock_st
93+
primary_key: [ts_code, trade_date, type]
94+
date_columns: [trade_date]
95+
partition_keys: [trade_year, trade_month]
96+
unit_notes: "Historical ST flags."
97+
pit_available: true
98+
incremental_key: trade_date
99+
used_by: [signal_master, universe_filters]
100+
known_limitations: "Coverage should be verified on older years."
101+
102+
- name: suspend_d
103+
primary_key: [ts_code, trade_date]
104+
date_columns: [trade_date]
105+
partition_keys: [trade_year, trade_month]
106+
unit_notes: "Daily suspension flag."
107+
pit_available: true
108+
incremental_key: trade_date
109+
used_by: [signal_master, tradability_flags]
110+
known_limitations: "Intraday suspension timing is not modeled in v0.1."
111+
112+
- name: index_daily
113+
primary_key: [index_symbol, trade_date]
114+
date_columns: [trade_date]
115+
partition_keys: [index_symbol, trade_year, trade_month]
116+
unit_notes: "index ret is decimal return."
117+
pit_available: true
118+
incremental_key: trade_date
119+
used_by: [signal_master, market_model]
120+
known_limitations: "Default broad market index is 000300.SH unless configured."
121+
122+
- name: index_weight
123+
primary_key: [index_code, ts_code, trade_date]
124+
date_columns: [trade_date]
125+
partition_keys: [index_code]
126+
unit_notes: "Use latest rebalance before as-of date."
127+
pit_available: true
128+
incremental_key: trade_date
129+
used_by: [universe_membership]
130+
known_limitations: "Membership PIT requires as-of join by rebalance date."

data/downloads/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Scripts to populate ``pyData/raw`` from a :class:`data.sources.base.DataSource`."""

0 commit comments

Comments
 (0)