refactor: class-based alpha factory + month-partitioned data pipeline
Replace the old signal/strategy/backtest modules with a decoupled
data → alpha → combo pipeline (parquet between phases, .pq extension).
Alphas:
- BaseAlpha + @register_alpha factory/plugin registry; one file per
built-in (reversal, reversal_vol, momentum); external alphas via
--alpha-module. Alphas are z-scored position weights, not predictors.
Data:
- baostock primary / akshare fallback, treated consistently.
- New --universe all (~5000 A-shares via query_all_stock, filtered).
- login-once batch downloader; empty-string OHLCV coerced to NaN.
- Month-partitioned dataset {output_dir}/{universe}/month=YYYY-MM/*.pq
with chunked durability flushes; --data-path is the dataset dir.
CLI logs at INFO by default (--log-level) so progress is visible.
Docs (README, CLAUDE.md) updated incl. pipeline diagram and roadmap
TODOs for portfolio construction / backtest / paper trading.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+47
-26
@@ -1,36 +1,16 @@
|
||||
"""CSI 300 (HS300) and CSI 500 (ZZ500) universe helpers."""
|
||||
"""CSI 300 (HS300), CSI 500 (ZZ500), and full A-share universe helpers."""
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
import baostock as bs
|
||||
import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# First 30 HS300 constituents (large caps) in 'shXXXXXX' / 'szXXXXXX' format.
|
||||
# Hardcoded for fast, deterministic smoke tests. Use get_hs300_stocks() for the
|
||||
# live, full list — downloading daily bars for all ~300 takes roughly 10 minutes.
|
||||
SYMBOLS = [
|
||||
"sh600000", "sh600009", "sh600010", "sh600028", "sh600030",
|
||||
"sh600036", "sh600048", "sh600050", "sh600104", "sh600276",
|
||||
"sh600309", "sh600519", "sh600585", "sh600887", "sh600900",
|
||||
"sh601012", "sh601166", "sh601288", "sh601318", "sh601398",
|
||||
"sh601628", "sh601668", "sh601857", "sh601888", "sh601988",
|
||||
"sz000001", "sz000002", "sz000333", "sz000651", "sz000858",
|
||||
]
|
||||
|
||||
|
||||
# First 30 CSI 500 (ZZ500) constituents (mid/small caps) in 'shXXXXXX' /
|
||||
# 'szXXXXXX' format. Hardcoded for fast, deterministic smoke tests. Use
|
||||
# get_zz500_stocks() for the live, full list. Mean reversion tends to be
|
||||
# stronger in these smaller caps than in the HS300 large caps.
|
||||
CSI500_SYMBOLS = [
|
||||
"sh600006", "sh600008", "sh600017", "sh600020", "sh600021",
|
||||
"sh600026", "sh600037", "sh600039", "sh600053", "sh600056",
|
||||
"sh600060", "sh600061", "sh600062", "sh600073", "sh600089",
|
||||
"sh600095", "sh600118", "sh600125", "sh600126", "sh600143",
|
||||
"sh600153", "sh600160", "sh600169", "sh600176", "sh600183",
|
||||
"sz000009", "sz000012", "sz000021", "sz000025", "sz000027",
|
||||
]
|
||||
# A-share code patterns (baostock dotted form): SH main/STAR (sh.6xxxxx),
|
||||
# SZ main/SME (sz.0xxxxx), ChiNext (sz.3xxxxx). Excludes indices and B-shares.
|
||||
_ASHARE_RE = r"^sh\.6\d{5}$|^sz\.[03]\d{5}$"
|
||||
_SZ_INDEX_RE = r"^sz\.399"
|
||||
|
||||
|
||||
def get_hs300_stocks() -> pd.DataFrame:
|
||||
@@ -69,3 +49,44 @@ def get_zz500_stocks() -> pd.DataFrame:
|
||||
df = pd.DataFrame(stocks, columns=["code", "name", "date"])
|
||||
df["code"] = df["code"].str.replace(".", "", regex=False)
|
||||
return df
|
||||
|
||||
|
||||
def get_all_stocks(day: str = "") -> pd.DataFrame:
|
||||
"""Fetch every listed A-share from baostock's all-stock snapshot.
|
||||
|
||||
Queries ``query_all_stock`` for a single trading day and keeps only A-shares
|
||||
(SH main/STAR, SZ main/SME/ChiNext), dropping indices and B-shares. If the
|
||||
given day is a non-trading day baostock returns nothing, so we walk back up
|
||||
to 10 days to land on the most recent trading day.
|
||||
|
||||
Args:
|
||||
day: ``YYYY-MM-DD`` snapshot day; defaults to today (walks back to the
|
||||
last trading day).
|
||||
|
||||
Returns:
|
||||
DataFrame with columns ``code`` (e.g. ``sh600000``), ``name``.
|
||||
"""
|
||||
start = date.fromisoformat(day) if day else date.today()
|
||||
bs.login()
|
||||
try:
|
||||
rows: list = []
|
||||
fields: list = []
|
||||
for back in range(11):
|
||||
probe = (start - timedelta(days=back)).isoformat()
|
||||
rs = bs.query_all_stock(day=probe)
|
||||
fields = rs.fields
|
||||
while rs.next():
|
||||
rows.append(rs.get_row_data())
|
||||
if rows:
|
||||
logger.info("query_all_stock: %d rows on %s", len(rows), probe)
|
||||
break
|
||||
finally:
|
||||
bs.logout()
|
||||
|
||||
df = pd.DataFrame(rows, columns=fields)
|
||||
code = df["code"]
|
||||
keep = code.str.match(_ASHARE_RE) & ~code.str.match(_SZ_INDEX_RE)
|
||||
df = df[keep].copy()
|
||||
df["code"] = df["code"].str.replace(".", "", regex=False)
|
||||
df = df.rename(columns={"code_name": "name"})
|
||||
return df[["code", "name"]].reset_index(drop=True)
|
||||
|
||||
Reference in New Issue
Block a user