Skip to content

Commit 33bb903

Browse files
authored
[codex] add russell 1000 research toolchain (#5)
* add russell 1000 research toolchain * fix lint issues in russell 1000 toolchain
1 parent a4ce2da commit 33bb903

21 files changed

Lines changed: 2449 additions & 0 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
symbol,as_of,close,volume
2+
AAPL,2026-03-30,220.73,60273300
3+
AAPL,2026-03-31,221.10,57500100
4+
MSFT,2026-03-30,438.20,19544000
5+
MSFT,2026-03-31,441.05,20133500
6+
JNJ,2026-03-30,163.44,7231100
7+
JNJ,2026-03-31,164.02,7012500
8+
SPY,2026-03-30,589.11,61233400
9+
SPY,2026-03-31,592.48,65510200
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
symbol,sector
2+
AAPL,Information Technology
3+
MSFT,Information Technology
4+
JNJ,Health Care
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
symbol,sector
2+
AAPL,Information Technology
3+
MSFT,Information Technology
4+
JNJ,Health Care
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
symbol,sector
2+
AAPL,Information Technology
3+
MSFT,Information Technology
4+
LLY,Health Care

scripts/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Local helper scripts for UsEquityStrategies."""
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
import pandas as pd
7+
8+
from us_equity_strategies.backtests.russell_1000_multi_factor_defensive import (
9+
BACKTEST_SUMMARY_COLUMNS,
10+
run_backtest,
11+
)
12+
from us_equity_strategies.snapshots.russell_1000_multi_factor_defensive import read_table
13+
14+
15+
def build_parser() -> argparse.ArgumentParser:
16+
parser = argparse.ArgumentParser(
17+
description="Backtest the Russell 1000 price-only multi-factor defensive strategy.",
18+
)
19+
parser.add_argument("--prices", required=True, help="Input price history file")
20+
parser.add_argument("--universe", required=True, help="Input universe file")
21+
parser.add_argument("--start", dest="start_date", help="Backtest start date")
22+
parser.add_argument("--end", dest="end_date", help="Backtest end date")
23+
parser.add_argument("--output-dir", help="Optional output directory for summary/equity/weights csv")
24+
parser.add_argument("--benchmark-symbol", default="SPY")
25+
parser.add_argument("--safe-haven", default="BOXX")
26+
parser.add_argument("--holdings-count", type=int, default=24)
27+
parser.add_argument("--single-name-cap", type=float, default=0.06)
28+
parser.add_argument("--sector-cap", type=float, default=0.20)
29+
parser.add_argument("--hold-bonus", type=float, default=0.15)
30+
parser.add_argument("--turnover-cost-bps", type=float, default=0.0)
31+
return parser
32+
33+
34+
def _format_summary(summary: dict[str, float | str]) -> pd.DataFrame:
35+
return pd.DataFrame([{column: summary.get(column) for column in BACKTEST_SUMMARY_COLUMNS}])
36+
37+
38+
def main(argv: list[str] | None = None) -> int:
39+
parser = build_parser()
40+
args = parser.parse_args(argv)
41+
42+
result = run_backtest(
43+
read_table(args.prices),
44+
read_table(args.universe),
45+
start_date=args.start_date,
46+
end_date=args.end_date,
47+
benchmark_symbol=args.benchmark_symbol,
48+
safe_haven=args.safe_haven,
49+
holdings_count=args.holdings_count,
50+
single_name_cap=args.single_name_cap,
51+
sector_cap=args.sector_cap,
52+
hold_bonus=args.hold_bonus,
53+
turnover_cost_bps=args.turnover_cost_bps,
54+
)
55+
56+
summary_frame = _format_summary(result["summary"])
57+
print(summary_frame.to_string(index=False))
58+
59+
if args.output_dir:
60+
output_dir = Path(args.output_dir)
61+
output_dir.mkdir(parents=True, exist_ok=True)
62+
summary_frame.to_csv(output_dir / "summary.csv", index=False)
63+
result["portfolio_returns"].rename("portfolio_return").to_csv(output_dir / "portfolio_returns.csv")
64+
result["weights_history"].to_csv(output_dir / "weights_history.csv")
65+
result["turnover_history"].to_csv(output_dir / "turnover_history.csv")
66+
print(f"wrote backtest outputs -> {output_dir}")
67+
68+
return 0
69+
70+
71+
if __name__ == "__main__":
72+
raise SystemExit(main())
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
5+
from us_equity_strategies.data_prep.russell_1000_history import (
6+
backfill_universe_history_start,
7+
build_interval_universe_history_from_directory,
8+
write_interval_universe_history,
9+
)
10+
11+
12+
def build_parser() -> argparse.ArgumentParser:
13+
parser = argparse.ArgumentParser(
14+
description="Build interval-form Russell 1000 universe history from dated snapshot files.",
15+
)
16+
parser.add_argument("--input-dir", required=True, help="Directory containing dated constituent snapshots")
17+
parser.add_argument("--output", required=True, help="Output universe history path")
18+
parser.add_argument(
19+
"--backfill-start-date",
20+
help="Optional date used to backfill the earliest snapshot start_date (useful when the first PIT snapshot starts after the backtest start)",
21+
)
22+
return parser
23+
24+
25+
def main(argv: list[str] | None = None) -> int:
26+
parser = build_parser()
27+
args = parser.parse_args(argv)
28+
29+
history = build_interval_universe_history_from_directory(args.input_dir)
30+
if args.backfill_start_date:
31+
history = backfill_universe_history_start(history, args.backfill_start_date)
32+
write_interval_universe_history(history, args.output)
33+
print(f"wrote {len(history)} rows -> {args.output}")
34+
return 0
35+
36+
37+
if __name__ == "__main__":
38+
raise SystemExit(main())
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
5+
from us_equity_strategies.data_prep.russell_1000_history import (
6+
build_symbol_alias_candidates_from_directory,
7+
build_symbol_alias_table,
8+
collect_symbol_universe,
9+
)
10+
from us_equity_strategies.data_prep.yfinance_prices import download_price_history
11+
from us_equity_strategies.snapshots.russell_1000_multi_factor_defensive import read_table, write_table
12+
13+
14+
def build_parser() -> argparse.ArgumentParser:
15+
parser = argparse.ArgumentParser(
16+
description="Fetch Russell 1000 price history from yfinance using universe-history symbols.",
17+
)
18+
parser.add_argument("--universe-history", required=True, help="Universe history file")
19+
parser.add_argument("--output", required=True, help="Output price history file")
20+
parser.add_argument("--start", required=True, help="Price download start date")
21+
parser.add_argument("--end", help="Price download end date")
22+
parser.add_argument("--benchmark-symbol", default="SPY")
23+
parser.add_argument("--safe-haven", default="BOXX")
24+
parser.add_argument("--chunk-size", type=int, default=100)
25+
parser.add_argument(
26+
"--snapshot-dir",
27+
help="Optional directory of dated universe snapshots; when provided, build ticker alias candidates from snapshot identifiers",
28+
)
29+
parser.add_argument(
30+
"--alias-output",
31+
help="Optional output path for derived ticker alias candidates (.csv/.json/.jsonl/.parquet)",
32+
)
33+
return parser
34+
35+
36+
def main(argv: list[str] | None = None) -> int:
37+
parser = build_parser()
38+
args = parser.parse_args(argv)
39+
40+
universe_history = read_table(args.universe_history)
41+
symbols = collect_symbol_universe(
42+
universe_history,
43+
benchmark_symbol=args.benchmark_symbol,
44+
safe_haven=args.safe_haven,
45+
)
46+
symbol_aliases = (
47+
build_symbol_alias_candidates_from_directory(args.snapshot_dir)
48+
if str(args.snapshot_dir or "").strip()
49+
else {}
50+
)
51+
alias_table = build_symbol_alias_table(symbol_aliases)
52+
prices = download_price_history(
53+
symbols,
54+
start=args.start,
55+
end=args.end,
56+
chunk_size=args.chunk_size,
57+
symbol_aliases=symbol_aliases,
58+
)
59+
write_table(prices, args.output)
60+
if args.alias_output:
61+
write_table(alias_table, args.alias_output)
62+
print(f"downloaded {len(symbols)} symbols, wrote {len(prices)} rows -> {args.output}")
63+
if args.alias_output:
64+
print(f"wrote {len(alias_table)} ticker alias rows across {len(symbol_aliases)} symbols -> {args.alias_output}")
65+
return 0
66+
67+
68+
if __name__ == "__main__":
69+
raise SystemExit(main())
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
from us_equity_strategies.data_prep.russell_1000_history import (
7+
download_ishares_historical_universe_snapshots,
8+
download_ishares_universe_snapshots,
9+
)
10+
from us_equity_strategies.snapshots.russell_1000_multi_factor_defensive import write_table
11+
12+
13+
def build_parser() -> argparse.ArgumentParser:
14+
parser = argparse.ArgumentParser(
15+
description="Fetch Russell 1000 proxy universe snapshots from iShares IWB holdings history.",
16+
)
17+
parser.add_argument("--output-dir", required=True, help="Directory where dated snapshot files will be written")
18+
parser.add_argument(
19+
"--metadata-output",
20+
help="Optional metadata output path (.csv/.json/.jsonl/.parquet)",
21+
)
22+
parser.add_argument(
23+
"--source",
24+
choices=("official_monthly", "wayback"),
25+
default="official_monthly",
26+
help="official_monthly = iShares official dated JSON history; wayback = archived live CSV captures",
27+
)
28+
parser.add_argument("--start-date", help="Earliest date to request for official monthly history (defaults from --from-year)")
29+
parser.add_argument("--end-date", help="Latest date to request for official monthly history")
30+
parser.add_argument(
31+
"--max-lookback-days",
32+
type=int,
33+
default=7,
34+
help="When an exact requested date has no holdings, step back up to this many days to find the latest available trading date",
35+
)
36+
parser.add_argument("--from-year", type=int, default=2020, help="Earliest Wayback capture year to query")
37+
parser.add_argument("--to-year", type=int, help="Latest Wayback capture year to query")
38+
parser.add_argument(
39+
"--no-live",
40+
action="store_true",
41+
help="Skip the current live IWB holdings file",
42+
)
43+
return parser
44+
45+
46+
def main(argv: list[str] | None = None) -> int:
47+
parser = build_parser()
48+
args = parser.parse_args(argv)
49+
50+
output_dir = Path(args.output_dir)
51+
output_dir.mkdir(parents=True, exist_ok=True)
52+
53+
if args.source == "official_monthly":
54+
start_date = args.start_date or f"{args.from_year:04d}-01-01"
55+
end_date = args.end_date or (f"{args.to_year:04d}-12-31" if args.to_year else None)
56+
snapshot_tables, metadata = download_ishares_historical_universe_snapshots(
57+
start_date=start_date,
58+
end_date=end_date,
59+
max_lookback_days=args.max_lookback_days,
60+
)
61+
else:
62+
snapshot_tables, metadata = download_ishares_universe_snapshots(
63+
from_year=args.from_year,
64+
to_year=args.to_year,
65+
include_live=not args.no_live,
66+
)
67+
68+
for as_of_date, snapshot in snapshot_tables:
69+
output_path = output_dir / f"r1000_{as_of_date:%Y-%m-%d}.csv"
70+
write_table(snapshot, output_path)
71+
72+
if args.metadata_output:
73+
write_table(metadata, args.metadata_output)
74+
75+
print(f"wrote {len(snapshot_tables)} snapshot files -> {output_dir}")
76+
if args.metadata_output:
77+
print(f"wrote snapshot metadata -> {args.metadata_output}")
78+
return 0
79+
80+
81+
if __name__ == "__main__":
82+
raise SystemExit(main())
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
from us_equity_strategies.snapshots.russell_1000_multi_factor_defensive import (
7+
build_feature_snapshot,
8+
read_table,
9+
write_table,
10+
)
11+
12+
13+
def build_parser() -> argparse.ArgumentParser:
14+
parser = argparse.ArgumentParser(
15+
description="Generate a Russell 1000 price-only feature snapshot.",
16+
)
17+
parser.add_argument("--prices", required=True, help="Input price history file (.csv/.json/.jsonl/.parquet)")
18+
parser.add_argument("--universe", required=True, help="Input universe file (.csv/.json/.jsonl/.parquet)")
19+
parser.add_argument("--output", required=True, help="Output feature snapshot path")
20+
parser.add_argument("--as-of", dest="as_of_date", help="Snapshot date (defaults to latest price date)")
21+
parser.add_argument("--benchmark-symbol", default="SPY")
22+
parser.add_argument("--min-price-usd", type=float, default=10.0)
23+
parser.add_argument("--min-adv20-usd", type=float, default=20_000_000.0)
24+
parser.add_argument("--min-history-days", type=int, default=252)
25+
return parser
26+
27+
28+
def main(argv: list[str] | None = None) -> int:
29+
parser = build_parser()
30+
args = parser.parse_args(argv)
31+
32+
price_history = read_table(args.prices)
33+
universe_snapshot = read_table(args.universe)
34+
snapshot = build_feature_snapshot(
35+
price_history,
36+
universe_snapshot,
37+
as_of_date=args.as_of_date,
38+
benchmark_symbol=args.benchmark_symbol,
39+
min_price_usd=args.min_price_usd,
40+
min_adv20_usd=args.min_adv20_usd,
41+
min_history_days=args.min_history_days,
42+
)
43+
write_table(snapshot, args.output)
44+
print(f"wrote {len(snapshot)} rows -> {Path(args.output)}")
45+
return 0
46+
47+
48+
if __name__ == "__main__":
49+
raise SystemExit(main())

0 commit comments

Comments
 (0)