Files
2026-07-13 13:26:28 +08:00

393 lines
13 KiB
Python

"""Generate synthetic test data for currently-skipped notebooks.
Run once to enrich the test-data repo with minimal synthetic datasets
that allow the remaining skipped notebooks to execute their code paths.
Usage:
uv run python tests/generate_skip_data.py --output ~/ml4t/test-data
This generates data for:
1. FNSPID news dataset (Ch10/07, Ch10/08)
2. SEC 10-Q MD&A text (Ch10/09)
3. ADV columns for Kyle lambda (Ch18/03)
4. Engine divergence predictions (Ch16/07)
5. Signal quality synthesis data (Ch20/02)
6. MLOps drift detection features (Ch26/02)
7. MLOps safe model rollout (Ch26/03)
8. MLOps MLflow registry (Ch26/06)
"""
import argparse
import json
import sqlite3
from datetime import date, timedelta
from pathlib import Path
import numpy as np
import polars as pl
np.random.seed(42)
SYMBOLS_ETF = ["SPY", "QQQ", "IWM", "TLT", "GLD", "XLF", "XLK", "XLE", "EFA", "VWO"]
SYMBOLS_EQ = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "JPM", "V", "JNJ"]
def generate_fnspid_news(data_dir: Path):
"""Generate synthetic FNSPID financial news data."""
out = data_dir / "alternative" / "news" / "fnspid"
out.mkdir(parents=True, exist_ok=True)
headlines = [
"{sym} reports strong quarterly earnings, beats estimates",
"{sym} shares drop on weaker-than-expected revenue guidance",
"{sym} announces major acquisition worth $2.5B",
"Analysts upgrade {sym} citing improving margins",
"{sym} CEO discusses expansion plans in earnings call",
"Market volatility hits {sym} as sector rotates",
"{sym} launches new product line targeting enterprise customers",
"Institutional investors increase {sym} holdings in Q3",
"{sym} faces regulatory scrutiny over data practices",
"{sym} dividend increase signals management confidence",
]
rows = []
dates = pl.date_range(date(2022, 1, 3), date(2024, 12, 31), "1d", eager=True)
for d in dates:
# 2-5 news items per day
n_items = np.random.randint(2, 6)
for _ in range(n_items):
sym = np.random.choice(SYMBOLS_EQ)
headline = np.random.choice(headlines).format(sym=sym)
rows.append(
{
"ticker": sym,
"timestamp": d,
"title": headline,
"source": np.random.choice(["Reuters", "Bloomberg", "CNBC", "WSJ"]),
}
)
df = pl.DataFrame(rows)
df.write_parquet(out / "fnspid_sample.parquet")
print(f" FNSPID: {len(df)} news items -> {out / 'fnspid_sample.parquet'}")
def generate_sec_10q_mda(data_dir: Path):
"""Generate synthetic SEC 10-Q MD&A text data."""
out = data_dir / "alternative" / "text"
out.mkdir(parents=True, exist_ok=True)
rows = []
for sym in SYMBOLS_EQ[:6]:
for year in range(2019, 2024):
for quarter in range(1, 5):
month = quarter * 3 + 1
if month > 12:
month = 1
year_f = year + 1
else:
year_f = year
filing_date = date(year_f, min(month, 12), 15)
period_end = date(year, quarter * 3, 28)
mda_text = (
f"Management's Discussion and Analysis for {sym}. "
f"During Q{quarter} {year}, revenue increased by {np.random.uniform(2, 15):.1f}% "
f"year-over-year. Operating margins improved to {np.random.uniform(15, 35):.1f}%. "
f"We continue to invest in R&D and expect continued growth. "
f"Key risks include market volatility and regulatory changes."
)
rows.append(
{
"symbol": sym,
"cik": str(np.random.randint(100000, 999999)),
"accession_no": f"0001234567-{year_f:04d}-{np.random.randint(10000, 99999):05d}",
"filing_date": filing_date,
"period_end": period_end,
"mda_text": mda_text,
"mda_word_count": len(mda_text.split()),
"mda_char_count": len(mda_text),
}
)
df = pl.DataFrame(rows)
df.write_parquet(out / "sp500_10q_mda.parquet")
print(f" SEC 10-Q: {len(df)} filings -> {out / 'sp500_10q_mda.parquet'}")
def enrich_adv_columns(data_dir: Path):
"""Add adv_21d (21-day average daily volume) to datasets that need it.
The Kyle lambda market impact calibration notebook (Ch18/03) reads
adv_21d from equity price data. The test data doesn't have this computed.
"""
datasets = [
("etfs", "etf_universe.parquet"),
("equities", "us_equities.parquet"),
]
for subdir, filename in datasets:
path = data_dir / subdir / filename
if not path.exists():
print(f" ADV: SKIP {path} (not found)")
continue
df = pl.read_parquet(path)
if "adv_21d" in df.columns:
print(f" ADV: SKIP {path} (already has adv_21d)")
continue
if "volume" not in df.columns:
print(f" ADV: SKIP {path} (no volume column)")
continue
# Compute rolling 21-day average volume per symbol
sort_cols = ["symbol", "timestamp"] if "symbol" in df.columns else ["timestamp"]
group_col = "symbol" if "symbol" in df.columns else None
if group_col:
df = df.sort(sort_cols).with_columns(
pl.col("volume")
.rolling_mean(window_size=21, min_samples=1)
.over(group_col)
.alias("adv_21d")
)
else:
df = df.sort("timestamp").with_columns(
pl.col("volume").rolling_mean(window_size=21, min_samples=1).alias("adv_21d")
)
df.write_parquet(path)
print(f" ADV: Added adv_21d to {path} ({len(df)} rows)")
def generate_engine_divergence_predictions(intermediates_dir: Path):
"""Generate predictions with model column for Ch16/07 engine divergence."""
out = intermediates_dir / "ch16_signal_method_comparison"
out.mkdir(parents=True, exist_ok=True)
dates = pl.date_range(date(2022, 1, 3), date(2023, 12, 29), "1d", eager=True)
rows = []
for d in dates:
for sym in SYMBOLS_ETF[:5]:
rows.append(
{
"timestamp": d,
"symbol": sym,
"prediction": np.random.normal(0, 0.02),
"model": "ridge_a1.0",
}
)
df = pl.DataFrame(rows)
df.write_parquet(out / "predictions_with_model.parquet")
print(f" Engine divergence: {len(df)} rows -> {out}")
def generate_signal_quality_data(intermediates_dir: Path):
"""Generate synthesis data for Ch20/02 signal quality notebook."""
# The notebook reads from Ch20/01 aggregate_synthesis outputs
out = intermediates_dir / "ch20_synthesis"
out.mkdir(parents=True, exist_ok=True)
case_studies = [
"etfs",
"crypto_perps_funding",
"nasdaq100_microstructure",
"sp500_equity_option_analytics",
"us_firm_characteristics",
"fx_pairs",
"cme_futures",
"sp500_options",
"us_equities_panel",
]
models = ["linear/ridge", "gbm/leaves_15", "deep_learning/lstm", "tabular_dl/tabm_l"]
# IC comparison data
ic_rows = []
for cs in case_studies:
for model in models:
ic_rows.append(
{
"case_study": cs,
"source": model,
"ic_mean": np.random.uniform(-0.02, 0.06),
"ic_std": np.random.uniform(0.01, 0.04),
"n_folds": 5,
}
)
ic_df = pl.DataFrame(ic_rows)
ic_df.write_parquet(out / "ic_comparison.parquet")
# Synthesis JSON
synthesis = {
"case_studies": {
cs: {
"champion": {
"source": "gbm/leaves_15",
"sharpe": float(np.random.uniform(-0.5, 2.0)),
},
"holdout": {
"ic": float(np.random.uniform(-0.02, 0.1)),
"sharpe": float(np.random.uniform(-1, 3)),
},
}
for cs in case_studies
}
}
(out / "all_synthesis.json").write_text(json.dumps(synthesis, indent=2))
print(f" Signal quality: IC comparison + synthesis -> {out}")
def generate_mlops_data(intermediates_dir: Path, data_dir: Path):
"""Generate data for Ch26 MLOps notebooks (02, 03, 06)."""
# Ch26/02 needs ETFs features with adv_21d — handled by enrich_adv_columns
# Ch26/03 needs a linear/lasso validation run in registry
out = intermediates_dir / "us_equities_panel" / "run_log"
out.mkdir(parents=True, exist_ok=True)
db_path = out / "registry.db"
db = sqlite3.connect(str(db_path))
db.execute("""
CREATE TABLE IF NOT EXISTS training_runs (
run_id TEXT PRIMARY KEY,
entry_point TEXT,
source TEXT,
label TEXT,
config_hash TEXT,
created_at TEXT,
ic_mean REAL,
status TEXT DEFAULT 'completed'
)
""")
db.execute("""
CREATE TABLE IF NOT EXISTS prediction_sets (
pred_id TEXT PRIMARY KEY,
run_id TEXT,
entry_point TEXT,
source TEXT,
label TEXT,
config_hash TEXT,
created_at TEXT,
ic_mean REAL,
n_rows INTEGER,
pred_path TEXT
)
""")
db.execute("""
CREATE TABLE IF NOT EXISTS prediction_metrics (
metric_id INTEGER PRIMARY KEY AUTOINCREMENT,
pred_id TEXT,
fold INTEGER,
ic REAL,
n_rows INTEGER
)
""")
# Insert a few synthetic runs
for i, (source, ic) in enumerate(
[
("linear/ridge_a1.0", 0.025),
("linear/lasso_a0.01", 0.018),
("gbm/leaves_15_mae", 0.042),
]
):
run_id = f"run_{i:03d}"
pred_id = f"pred_{i:03d}"
db.execute(
"INSERT OR REPLACE INTO training_runs VALUES (?,?,?,?,?,?,?,?)",
(
run_id,
"06_linear" if "linear" in source else "07_gbm",
source,
"fwd_ret_1d",
f"hash_{i}",
"2026-01-01T00:00:00",
ic,
"completed",
),
)
db.execute(
"INSERT OR REPLACE INTO prediction_sets VALUES (?,?,?,?,?,?,?,?,?,?)",
(
pred_id,
run_id,
"06_linear" if "linear" in source else "07_gbm",
source,
"fwd_ret_1d",
f"hash_{i}",
"2026-01-01T00:00:00",
ic,
1000,
f"predictions/{pred_id}.parquet",
),
)
for fold in range(5):
db.execute(
"INSERT INTO prediction_metrics (pred_id, fold, ic, n_rows) VALUES (?,?,?,?)",
(pred_id, fold, ic + np.random.normal(0, 0.005), 200),
)
db.commit()
db.close()
print(f" MLOps registry: 3 runs -> {db_path}")
# Generate stub predictions for the registry entries
preds_dir = out.parent / "predictions"
preds_dir.mkdir(parents=True, exist_ok=True)
dates = pl.date_range(date(2023, 1, 2), date(2023, 12, 29), "1d", eager=True)
for i in range(3):
rows = []
for d in dates:
for sym in SYMBOLS_EQ[:5]:
rows.append(
{
"timestamp": d,
"symbol": sym,
"prediction": np.random.normal(0, 0.02),
"fold": np.random.randint(0, 5),
}
)
df = pl.DataFrame(rows)
df.write_parquet(preds_dir / f"pred_{i:03d}.parquet")
print(f" MLOps predictions: 3 files -> {preds_dir}")
def main():
parser = argparse.ArgumentParser(
description="Generate synthetic test data for skipped notebooks"
)
parser.add_argument("--output", required=True, help="Test data repo root")
args = parser.parse_args()
root = Path(args.output)
data_dir = root / "data"
intermediates_dir = root / "intermediates"
print("Generating synthetic test data for skipped notebooks...")
print()
print("[1/6] FNSPID news data (Ch10/07, Ch10/08)...")
generate_fnspid_news(data_dir)
print("[2/6] SEC 10-Q MD&A text (Ch10/09)...")
generate_sec_10q_mda(data_dir)
print("[3/6] ADV columns for Kyle lambda (Ch18/03)...")
enrich_adv_columns(data_dir)
print("[4/6] Engine divergence predictions (Ch16/07)...")
generate_engine_divergence_predictions(intermediates_dir)
print("[5/6] Signal quality synthesis data (Ch20/02)...")
generate_signal_quality_data(intermediates_dir)
print("[6/6] MLOps registry and predictions (Ch26/02-06)...")
generate_mlops_data(intermediates_dir, data_dir)
print()
print("Done! Now commit changes to the test-data repo and update overrides.yaml.")
if __name__ == "__main__":
main()