-
Notifications
You must be signed in to change notification settings - Fork 0
[codex] add chinext growth momentum quality snapshot #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| symbol,sector,close_cny,adv20_cny,market_cap_cny,revenue_yoy,profit_yoy,revenue_acceleration_2q,roe_ttm,roe_stability_3y,gross_margin_stability_3y,mom_12_1,mom_6_1,sma200_gap,realized_vol_126,earnings_positive,suspension_days_63,is_st,list_days | ||
| 300001,software,28.0,80000000.0,25000000000.0,0.32,0.28,0.10,0.18,0.75,0.70,0.24,0.16,0.12,0.28,True,0,False,1200 | ||
| 300002,biotech,44.0,60000000.0,18000000000.0,0.24,0.21,0.06,0.15,0.68,0.66,0.18,0.12,0.08,0.24,True,0,False,900 | ||
| 300003,new_energy,16.0,55000000.0,14000000000.0,0.04,-0.02,-0.01,0.08,0.50,0.48,0.05,0.03,0.01,0.20,True,0,False,800 | ||
| 159915,etf,3.5,1000000000.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.12,True,0,False,3000 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ readme = "README.md" | |
| requires-python = ">=3.11" | ||
| dependencies = [ | ||
| "pandas>=2.0", | ||
| "cn-equity-strategies @ git+https://github.com/QuantStrategyLab/CnEquityStrategies.git@8dfadcf8a4dc6cc516f27a4013248474603d8ce2", | ||
| "cn-equity-strategies @ git+https://github.com/QuantStrategyLab/CnEquityStrategies.git@1bced052d515373e88620dae48499a4ad44c10f7", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This dependency is bumped to the commit that provides the new ChiNext strategy module, but the checked-in Useful? React with 👍 / 👎. |
||
| ] | ||
|
|
||
| [project.optional-dependencies] | ||
|
|
@@ -19,6 +19,7 @@ public-data = ["akshare>=1.14"] | |
|
|
||
| [project.scripts] | ||
| cneq-build-dividend-quality-snapshot = "cn_equity_snapshot_pipelines.dividend_quality:main" | ||
| cneq-build-chinext-growth-momentum-quality-snapshot = "cn_equity_snapshot_pipelines.chinext_growth_momentum_quality:main" | ||
| cneq-stage-akshare-dividend-quality = "cn_equity_snapshot_pipelines.akshare_staging:main" | ||
| cneq-stage-akshare-market-history = "cn_equity_snapshot_pipelines.akshare_market_history:main" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #!/usr/bin/env python3 | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from cn_equity_snapshot_pipelines.chinext_growth_momentum_quality import build_and_write_snapshot | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| SAMPLE = ROOT / "examples" / "chinext_growth_momentum_quality" / "factor_snapshot.sample.csv" | ||
| OUTPUT = ROOT / "data" / "output" / "chinext_growth_momentum_quality" | ||
|
|
||
|
|
||
| def main() -> int: | ||
| result = build_and_write_snapshot( | ||
| factor_snapshot_path=SAMPLE, | ||
| output_dir=OUTPUT, | ||
| ) | ||
| print(f"built snapshot rows={len(result.snapshot)} ranking rows={len(result.ranking)}") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| from datetime import date, datetime, timezone | ||
| from pathlib import Path | ||
|
|
||
| import pandas as pd | ||
|
|
||
| from cn_equity_strategies.strategies.cn_chinext_growth_momentum_quality_snapshot import ( | ||
| compute_signals, | ||
| score_candidates, | ||
| ) | ||
|
|
||
| from .artifacts import SnapshotBuildResult, write_release_status_summary, write_snapshot_manifest | ||
| from .contracts import CN_CHINEXT_GROWTH_MOMENTUM_QUALITY_SNAPSHOT_PROFILE, get_profile_contract | ||
|
|
||
|
|
||
| def _read_csv(path: str | Path) -> pd.DataFrame: | ||
| return pd.read_csv(Path(path)) | ||
|
|
||
|
|
||
| def _ensure_snapshot_as_of(snapshot: pd.DataFrame, *, as_of: str | date | None = None) -> pd.DataFrame: | ||
| frame = snapshot.copy() | ||
| if "as_of" in frame.columns or "snapshot_date" in frame.columns: | ||
| return frame | ||
| stamp = as_of or datetime.now(timezone.utc).date().isoformat() | ||
| frame.insert(0, "as_of", str(stamp)) | ||
| return frame | ||
|
|
||
|
|
||
| def build_and_write_snapshot( | ||
| *, | ||
| factor_snapshot_path: str | Path, | ||
| output_dir: str | Path, | ||
| min_adv20_cny: float = 0.0, | ||
| min_market_cap_cny: float = 0.0, | ||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When callers use this new builder or CLI without threshold flags, these Useful? React with 👍 / 👎. |
||
| as_of: str | date | None = None, | ||
| ) -> SnapshotBuildResult: | ||
| contract = get_profile_contract(CN_CHINEXT_GROWTH_MOMENTUM_QUALITY_SNAPSHOT_PROFILE) | ||
| artifact_paths = contract.artifact_paths(output_dir) | ||
| snapshot = _ensure_snapshot_as_of(_read_csv(factor_snapshot_path), as_of=as_of) | ||
| ranking = score_candidates( | ||
|
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This builder forwards the entire CSV into Useful? React with 👍 / 👎. |
||
| snapshot, | ||
| min_adv20_cny=float(min_adv20_cny), | ||
| min_market_cap_cny=float(min_market_cap_cny), | ||
| ) | ||
|
|
||
| for path in artifact_paths.values(): | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| snapshot.to_csv(artifact_paths["snapshot"], index=False) | ||
| ranking.to_csv(artifact_paths["ranking"], index=False) | ||
| write_snapshot_manifest( | ||
| contract=contract, | ||
| snapshot_path=artifact_paths["snapshot"], | ||
| snapshot=snapshot, | ||
| manifest_path=artifact_paths["manifest"], | ||
| ) | ||
| weights, signal_description, _is_hard_defense, status_description, diagnostics = compute_signals( | ||
| snapshot, | ||
| current_holdings=set(), | ||
| min_adv20_cny=float(min_adv20_cny), | ||
| min_market_cap_cny=float(min_market_cap_cny), | ||
| ) | ||
| write_release_status_summary( | ||
| contract=contract, | ||
| snapshot_path=artifact_paths["snapshot"], | ||
| manifest_path=artifact_paths["manifest"], | ||
| ranking_path=artifact_paths["ranking"], | ||
| summary_path=artifact_paths["release_summary"], | ||
| snapshot=snapshot, | ||
| signal_description=signal_description, | ||
| status_description=status_description, | ||
| diagnostics={**diagnostics, "target_weights": weights}, | ||
| ) | ||
| return SnapshotBuildResult( | ||
| snapshot=snapshot, | ||
| ranking=ranking, | ||
| artifact_paths=artifact_paths, | ||
| signal_description=signal_description, | ||
| status_description=status_description, | ||
| diagnostics=dict(diagnostics), | ||
| ) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument( | ||
| "--factor-snapshot", | ||
| required=True, | ||
| help="CSV with cn_chinext_growth_momentum_quality_snapshot factor columns", | ||
| ) | ||
| parser.add_argument("--output-dir", default="data/output/chinext_growth_momentum_quality") | ||
| parser.add_argument("--min-adv20-cny", type=float, default=0.0) | ||
| parser.add_argument("--min-market-cap-cny", type=float, default=0.0) | ||
| parser.add_argument("--as-of", default=None, help="Snapshot as_of date (YYYY-MM-DD). Defaults to UTC today.") | ||
| args = parser.parse_args(argv) | ||
|
|
||
| result = build_and_write_snapshot( | ||
| factor_snapshot_path=args.factor_snapshot, | ||
| output_dir=args.output_dir, | ||
| min_adv20_cny=args.min_adv20_cny, | ||
| min_market_cap_cny=args.min_market_cap_cny, | ||
| as_of=args.as_of, | ||
| ) | ||
| print(f"snapshot={result.artifact_paths['snapshot']}") | ||
| print(f"manifest={result.artifact_paths['manifest']}") | ||
| print(f"ranking={result.artifact_paths['ranking']}") | ||
| print(f"release_summary={result.artifact_paths['release_summary']}") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from cn_equity_snapshot_pipelines.chinext_growth_momentum_quality import build_and_write_snapshot | ||
|
|
||
|
|
||
| def test_build_and_write_chinext_growth_momentum_quality_snapshot(tmp_path: Path) -> None: | ||
| sample_path = ( | ||
| Path(__file__).resolve().parents[1] | ||
| / "examples" | ||
| / "chinext_growth_momentum_quality" | ||
| / "factor_snapshot.sample.csv" | ||
| ) | ||
|
|
||
| result = build_and_write_snapshot(factor_snapshot_path=sample_path, output_dir=tmp_path) | ||
|
|
||
| assert result.artifact_paths["snapshot"].exists() | ||
| assert result.artifact_paths["manifest"].exists() | ||
| assert result.artifact_paths["ranking"].exists() | ||
| assert result.artifact_paths["release_summary"].exists() | ||
| assert result.diagnostics["snapshot_contract_version"] == ( | ||
| "cn_chinext_growth_momentum_quality_snapshot.factor_snapshot.v1" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the default sample build, the ChiNext strategy assigns the uninvested/defensive weight to its default safe-haven symbol
511880, but this sample includes159915instead. After the production floors/non-ChiNext filtering are fixed, the generated release summary can still reference511880intarget_weights/managed symbols without any corresponding snapshot row, which makes the sample artifact pack inconsistent for downstream jobs that expect metadata for every managed symbol.Useful? React with 👍 / 👎.