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

743 lines
26 KiB
Python

#!/usr/bin/env python3
"""ML4T Installation Verification Script.
Validates that all libraries required by the ML4T Third Edition notebooks
are importable and prints version information. Also checks runtime
requirements (CUDA, repo-root importability, matplotlib styling, Plotly).
Run from repo root:
python scripts/verify_installation.py
# or
uv run python scripts/verify_installation.py
# or in Docker
docker compose run --rm ml4t python scripts/verify_installation.py
"""
from __future__ import annotations
import importlib
import os
import sys
import time
from pathlib import Path
# ---------------------------------------------------------------------------
# CUDA library path fix (must happen before any torch import)
# ---------------------------------------------------------------------------
def _setup_cuda_library_path():
"""Set LD_LIBRARY_PATH to torch's bundled CUDA libs if needed."""
if os.environ.get("LD_LIBRARY_PATH", "").startswith("/usr/local/cuda"):
return
try:
import torch
torch_root = Path(torch.__file__).parent
cuda_paths = [str(torch_root / "lib")]
nvidia_dir = torch_root.parent / "nvidia"
if nvidia_dir.exists():
cuda_paths.extend(str(p) for p in nvidia_dir.glob("*/lib"))
existing = os.environ.get("LD_LIBRARY_PATH", "")
os.environ["LD_LIBRARY_PATH"] = ":".join(cuda_paths + ([existing] if existing else []))
except ImportError:
pass
_setup_cuda_library_path()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
PASS = "\033[92mPASS\033[0m"
FAIL = "\033[91mFAIL\033[0m"
SKIP = "\033[93mSKIP\033[0m"
BOLD = "\033[1m"
RESET = "\033[0m"
results: list[tuple[str, str, str, str]] = [] # (category, name, status, detail)
def _version(mod: object) -> str:
"""Best-effort version string from a module object."""
for attr in ("__version__", "VERSION", "version"):
v = getattr(mod, attr, None)
if v is not None:
return str(v) if isinstance(v, str) else str(v)
return "installed (version unknown)"
def check_import(category: str, import_name: str, package_label: str | None = None):
"""Try to import *import_name* and record the result."""
label = package_label or import_name
try:
mod = importlib.import_module(import_name)
ver = _version(mod)
results.append((category, label, "PASS", ver))
except Exception as exc:
results.append((category, label, "FAIL", str(exc)[:120]))
def check_from_import(category: str, module: str, names: list[str], label: str | None = None):
"""Try 'from module import names' and record the result."""
display = label or f"{module} ({', '.join(names)})"
try:
mod = importlib.import_module(module)
missing = [n for n in names if not hasattr(mod, n)]
if missing:
results.append((category, display, "FAIL", f"Missing attributes: {missing}"))
else:
results.append((category, display, "PASS", _version(mod)))
except Exception as exc:
results.append((category, display, "FAIL", str(exc)[:120]))
# ---------------------------------------------------------------------------
# Category 1: Core Data Science
# ---------------------------------------------------------------------------
def check_core_data_science():
cat = "Core Data Science"
check_import(cat, "numpy", "numpy")
check_import(cat, "scipy", "scipy")
check_import(cat, "pandas", "pandas")
check_import(cat, "polars", "polars")
check_import(cat, "pyarrow", "pyarrow")
check_import(cat, "numba", "numba")
check_import(cat, "sympy", "sympy")
# ---------------------------------------------------------------------------
# Category 2: Visualization
# ---------------------------------------------------------------------------
def check_visualization():
cat = "Visualization"
check_import(cat, "matplotlib", "matplotlib")
check_import(cat, "matplotlib.pyplot", "matplotlib.pyplot")
check_import(cat, "seaborn", "seaborn")
check_import(cat, "plotly", "plotly")
check_import(cat, "plotly.express", "plotly.express")
check_import(cat, "plotly.graph_objects", "plotly.graph_objects")
check_import(cat, "plotly.subplots", "plotly.subplots")
check_import(cat, "kaleido", "kaleido")
# ---------------------------------------------------------------------------
# Category 3: Machine Learning (sklearn, boosting, optimization)
# ---------------------------------------------------------------------------
def check_ml():
cat = "Machine Learning"
check_import(cat, "sklearn", "scikit-learn")
# Key sklearn submodules actually used
for sub in [
"sklearn.linear_model",
"sklearn.ensemble",
"sklearn.decomposition",
"sklearn.manifold",
"sklearn.cluster",
"sklearn.metrics",
"sklearn.model_selection",
"sklearn.preprocessing",
"sklearn.pipeline",
"sklearn.impute",
"sklearn.inspection",
"sklearn.calibration",
"sklearn.covariance",
"sklearn.neural_network",
"sklearn.mixture",
"sklearn.feature_extraction.text",
]:
check_import(cat, sub)
check_import(cat, "lightgbm", "lightgbm")
check_import(cat, "xgboost", "xgboost")
check_import(cat, "catboost", "catboost")
check_import(cat, "optuna", "optuna")
check_import(cat, "shap", "shap")
check_import(cat, "tabpfn", "tabpfn")
check_import(cat, "joblib", "joblib")
# ---------------------------------------------------------------------------
# Category 4: Deep Learning
# ---------------------------------------------------------------------------
def check_deep_learning():
cat = "Deep Learning"
check_import(cat, "torch", "pytorch")
check_import(cat, "torch.nn", "torch.nn")
check_import(cat, "torch.optim", "torch.optim")
check_import(cat, "torch.utils.data", "torch.utils.data")
check_import(cat, "pytorch_lightning", "pytorch-lightning")
check_import(cat, "torchdiffeq", "torchdiffeq")
# torchode: optional, Ch5 NB04 has try/except fallback to torchdiffeq
# Broken with torch 2.10+ due to torchtyping dependency
check_import(cat, "einops", "einops")
check_import(cat, "opacus", "opacus")
# ---------------------------------------------------------------------------
# Category 5: NLP / Transformers
# ---------------------------------------------------------------------------
def check_nlp():
cat = "NLP / Transformers"
check_import(cat, "transformers", "transformers")
check_import(cat, "sentence_transformers", "sentence-transformers")
check_import(cat, "datasets", "datasets (HuggingFace)")
check_import(cat, "evaluate", "evaluate (HuggingFace)")
# ---------------------------------------------------------------------------
# Category 6: Time Series
# ---------------------------------------------------------------------------
def check_time_series():
cat = "Time Series"
check_import(cat, "statsmodels", "statsmodels")
check_import(cat, "statsmodels.api", "statsmodels.api")
check_import(cat, "statsmodels.tsa.stattools", "statsmodels.tsa.stattools")
check_import(cat, "statsmodels.tsa.arima.model", "statsmodels.tsa.arima.model")
check_import(cat, "arch", "arch")
check_import(cat, "pmdarima", "pmdarima")
check_import(cat, "sktime", "sktime")
check_import(cat, "darts", "darts")
check_import(cat, "statsforecast", "statsforecast")
check_import(cat, "hmmlearn", "hmmlearn")
check_import(cat, "filterpy", "filterpy")
check_import(cat, "pykalman", "pykalman")
check_import(cat, "pywt", "PyWavelets")
check_import(cat, "ruptures", "ruptures")
# Granite & Chronos (time series foundation models)
check_import(cat, "tsfm_public", "granite-tsfm")
check_import(cat, "chronos", "chronos-forecasting")
# ---------------------------------------------------------------------------
# Category 7: Causal Inference
# ---------------------------------------------------------------------------
def check_causal():
cat = "Causal Inference"
check_import(cat, "dowhy", "dowhy")
check_import(cat, "econml", "econml")
check_import(cat, "causalml", "causalml")
check_import(cat, "tigramite", "tigramite")
check_import(cat, "causalimpact", "pycausalimpact")
check_import(cat, "causallearn", "causal-learn")
check_import(cat, "linearmodels", "linearmodels")
# ---------------------------------------------------------------------------
# Category 8: Portfolio / Finance
# ---------------------------------------------------------------------------
def check_finance():
cat = "Portfolio / Finance"
check_import(cat, "pypfopt", "PyPortfolioOpt")
check_import(cat, "riskfolio", "riskfolio-lib")
check_import(cat, "skfolio", "skfolio")
check_import(cat, "vectorbt", "vectorbt")
check_import(cat, "exchange_calendars", "exchange-calendars")
# pfhedge: moved to py312 image (Python 3.12, numpy<2 constraint)
# ---------------------------------------------------------------------------
# Category 9: RL
# ---------------------------------------------------------------------------
def check_rl():
cat = "Reinforcement Learning"
check_import(cat, "gymnasium", "gymnasium")
check_import(cat, "stable_baselines3", "stable-baselines3")
# ---------------------------------------------------------------------------
# Category 10: Data Sources / Providers
# ---------------------------------------------------------------------------
def check_data_sources():
cat = "Data Sources"
check_import(cat, "yfinance", "yfinance")
check_import(cat, "edgar", "edgartools")
# sec-edgar-api: removed from deps (no notebook imports it, uses edgartools)
check_import(cat, "iex_parser", "iex-parser")
check_import(cat, "bs4", "beautifulsoup4")
check_import(cat, "databento", "databento")
check_import(cat, "gdown", "gdown")
check_import(cat, "oandapyV20", "oandapyV20")
# ---------------------------------------------------------------------------
# Category 11: Technical Analysis
# ---------------------------------------------------------------------------
def check_ta():
cat = "Technical Analysis"
check_import(cat, "talib", "TA-Lib")
check_import(cat, "ta", "ta")
# ---------------------------------------------------------------------------
# Category 12: Synthetic Data
# ---------------------------------------------------------------------------
def check_synthetic():
cat = "Synthetic Data"
check_import(cat, "be_great", "be-great (REaLTabFormer)")
# ---------------------------------------------------------------------------
# Category 13: Utilities / Infrastructure
# ---------------------------------------------------------------------------
def check_utilities():
cat = "Utilities"
check_import(cat, "dotenv", "python-dotenv")
check_import(cat, "yaml", "PyYAML")
check_import(cat, "tqdm", "tqdm")
check_import(cat, "rich", "rich")
check_import(cat, "requests", "requests")
check_import(cat, "nest_asyncio", "nest-asyncio")
check_import(cat, "psutil", "psutil")
check_import(cat, "openpyxl", "openpyxl")
check_import(cat, "neo4j", "neo4j")
check_import(cat, "thefuzz", "thefuzz")
check_import(cat, "rapidfuzz", "rapidfuzz")
check_import(cat, "networkx", "networkx")
# ---------------------------------------------------------------------------
# Category 14: Jupyter / Notebook Tooling
# ---------------------------------------------------------------------------
def check_jupyter():
cat = "Jupyter"
check_import(cat, "jupyter", "jupyter")
check_import(cat, "notebook", "notebook")
check_import(cat, "jupytext", "jupytext")
check_import(cat, "jupyterlab", "jupyterlab")
check_import(cat, "IPython", "IPython")
check_import(cat, "anywidget", "anywidget")
check_import(cat, "nbconvert", "nbconvert")
check_import(cat, "nbformat", "nbformat")
check_import(cat, "papermill", "papermill")
# ---------------------------------------------------------------------------
# Category 15: File Formats / Storage
# ---------------------------------------------------------------------------
def check_storage():
cat = "Storage / Formats"
check_import(cat, "tables", "tables (PyTables/HDF5)")
check_import(cat, "duckdb", "duckdb")
check_import(cat, "sqlite3", "sqlite3 (stdlib)")
check_import(cat, "mlflow", "mlflow")
# ---------------------------------------------------------------------------
# Category 16: ML4T Libraries (PyPI)
# ---------------------------------------------------------------------------
def check_ml4t_libraries():
cat = "ML4T Libraries"
check_import(cat, "ml4t.data", "ml4t-data")
check_import(cat, "ml4t.diagnostic", "ml4t-diagnostic")
check_import(cat, "ml4t.engineer", "ml4t-engineer")
check_import(cat, "ml4t.backtest", "ml4t-backtest")
check_import(cat, "ml4t.live", "ml4t-live")
# Key submodules that notebooks import directly
sub = "ML4T Sub-modules"
check_from_import(sub, "ml4t.data", ["DataManager"], "ml4t.data.DataManager")
check_from_import(sub, "ml4t.diagnostic.signal", ["analyze_signal"], "ml4t.diagnostic.signal")
check_from_import(
sub,
"ml4t.diagnostic.splitters",
["CombinatorialCV", "WalkForwardCV"],
"ml4t.diagnostic.splitters",
)
check_from_import(sub, "ml4t.engineer", ["compute_features"], "ml4t.engineer.compute_features")
check_from_import(
sub,
"ml4t.backtest",
["BacktestConfig", "DataFeed", "Engine"],
"ml4t.backtest core classes",
)
# ---------------------------------------------------------------------------
# Category 17: Repo-Root Imports (utils, data, case_studies)
# ---------------------------------------------------------------------------
def check_repo_imports():
cat = "Repo Packages"
# utils
try:
import utils # noqa: F811
results.append((cat, "utils", "PASS", "importable"))
except Exception as exc:
results.append((cat, "utils", "FAIL", str(exc)[:120]))
check_from_import(
cat, "utils", ["DATA_DIR", "ML4T_DATA_PATH", "REPO_ROOT"], "utils (core exports)"
)
check_from_import(
cat,
"utils.paths",
["get_chapter_dir", "get_output_dir", "get_case_study_dir"],
"utils.paths",
)
check_from_import(cat, "utils.style", ["COLORS"], "utils.style")
check_from_import(
cat,
"utils.data_quality",
["check_ohlc_invariants", "describe_coverage"],
"utils.data_quality",
)
check_from_import(cat, "utils.modeling", ["load_modeling_dataset"], "utils.modeling")
check_from_import(cat, "utils.cv_splits", ["generate_cv_splits"], "utils.cv_splits")
# data package
try:
import data # noqa: F811
results.append((cat, "data", "PASS", "importable"))
except Exception as exc:
results.append((cat, "data", "FAIL", str(exc)[:120]))
check_from_import(
cat, "data", ["load_etfs", "load_us_equities", "load_cme_futures"], "data (loaders)"
)
# case_studies package
try:
import case_studies # noqa: F811
results.append((cat, "case_studies", "PASS", "importable"))
except Exception as exc:
results.append((cat, "case_studies", "FAIL", str(exc)[:120]))
# ---------------------------------------------------------------------------
# Category 18: Runtime Checks
# ---------------------------------------------------------------------------
def check_runtime():
cat = "Runtime"
# 1. PYTHONPATH / repo root on sys.path
repo_root = Path(__file__).resolve().parent.parent
on_path = any(Path(p).resolve() == repo_root for p in sys.path if p)
if on_path:
results.append((cat, "Repo root on sys.path", "PASS", str(repo_root)))
else:
results.append((cat, "Repo root on sys.path", "FAIL", f"{repo_root} not found in sys.path"))
# 2. CUDA / GPU detection
try:
import torch
cuda_available = torch.cuda.is_available()
if cuda_available:
gpu_name = torch.cuda.get_device_name(0)
gpu_mem = torch.cuda.get_device_properties(0).total_memory / (1024**3)
results.append(
(
cat,
"CUDA (GPU)",
"PASS",
f"{gpu_name} ({gpu_mem:.1f} GB) - CUDA {torch.version.cuda}",
)
)
else:
results.append((cat, "CUDA (GPU)", "SKIP", "No GPU detected (CPU-only mode)"))
except ImportError:
results.append(
(cat, "CUDA (GPU)", "SKIP", "PyTorch not importable (see Deep Learning section)")
)
except Exception as exc:
results.append((cat, "CUDA (GPU)", "FAIL", str(exc)[:120]))
# 3. matplotlibrc detection
matplotlibrc_path = repo_root / "matplotlibrc"
if matplotlibrc_path.exists():
results.append((cat, "matplotlibrc", "PASS", str(matplotlibrc_path)))
else:
results.append((cat, "matplotlibrc", "FAIL", "matplotlibrc not found at repo root"))
# 4. Matplotlib styling active
try:
import matplotlib
matplotlib.use("Agg") # non-interactive backend for script mode
import matplotlib.pyplot as plt
# Check that our matplotlibrc settings took effect
spines_top = plt.rcParams.get("axes.spines.top", True)
if not spines_top:
results.append(
(
cat,
"Matplotlib styling (matplotlibrc)",
"PASS",
"axes.spines.top=False (ML4T style active)",
)
)
else:
results.append(
(
cat,
"Matplotlib styling (matplotlibrc)",
"FAIL",
"axes.spines.top=True (ML4T matplotlibrc not loaded - run from repo root)",
)
)
except Exception as exc:
results.append((cat, "Matplotlib styling (matplotlibrc)", "FAIL", str(exc)[:120]))
# 5. Plotly renderer available
try:
import plotly.io as pio
renderer = pio.renderers.default or os.environ.get("PLOTLY_RENDERER", "(not set)")
results.append((cat, "Plotly renderer", "PASS", f"default={renderer}"))
except Exception as exc:
results.append((cat, "Plotly renderer", "FAIL", str(exc)[:120]))
# 6. ML4T Plotly template registered
try:
import plotly.io as pio
# The template is registered when utils.style is imported
if "ml4t" not in pio.templates:
try:
from utils.style import _register_plotly_template
_register_plotly_template()
except Exception:
pass
if "ml4t" in pio.templates:
results.append((cat, "Plotly ML4T template", "PASS", "registered"))
else:
results.append(
(
cat,
"Plotly ML4T template",
"FAIL",
"template 'ml4t' not in plotly.io.templates (import utils.style to register)",
)
)
except Exception as exc:
results.append((cat, "Plotly ML4T template", "FAIL", str(exc)[:120]))
# 7. ML4T_DATA_PATH set and exists
data_path = os.environ.get("ML4T_DATA_PATH")
if data_path:
p = Path(data_path)
if p.exists():
results.append((cat, "ML4T_DATA_PATH", "PASS", str(p)))
else:
results.append(
(cat, "ML4T_DATA_PATH", "FAIL", f"Set to {data_path} but directory does not exist")
)
else:
# Check .env fallback
env_file = repo_root / ".env"
if env_file.exists():
results.append(
(
cat,
"ML4T_DATA_PATH",
"PASS",
"Not in env but .env file found (loaded at runtime)",
)
)
else:
results.append((cat, "ML4T_DATA_PATH", "FAIL", "Not set and no .env file found"))
# 8. Python version
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
results.append((cat, "Python version", "PASS", py_ver))
# ---------------------------------------------------------------------------
# Category 19: Optional / Docker-Profile Extras
# ---------------------------------------------------------------------------
def check_optional():
cat = "Optional"
# gensim: separate docker profile (word2vec)
check_import(cat, "gensim", "gensim (word2vec Docker profile)")
# signatory: separate env (signatures)
check_import(cat, "signatory", "signatory (signatures env)")
# Bayesian
check_import(cat, "pymc", "pymc (bayesian extra)")
check_import(cat, "arviz", "arviz (bayesian extra)")
# Agents
check_import(cat, "crewai", "crewai (agents extra)")
check_import(cat, "langgraph", "langgraph (agents extra)")
# DB benchmark extras
check_import(cat, "arcticdb", "arcticdb (db-benchmark extra)")
check_import(cat, "clickhouse_connect", "clickhouse-connect (db-benchmark extra)")
check_import(cat, "influxdb_client", "influxdb-client (db-benchmark extra)")
# Live trading
check_import(cat, "ib_async", "ib_insync/ib_async (live extra)")
check_import(cat, "alpaca", "alpaca-py (live extra)")
# Feast / MLOps
check_import(cat, "feast", "feast (mlops extra)")
# COT reports
check_import(cat, "cot_reports", "cot-reports")
# secedgar
check_import(cat, "secedgar", "secedgar")
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
def print_summary():
# Group by category
categories: dict[str, list[tuple[str, str, str]]] = {}
for cat, name, status, detail in results:
categories.setdefault(cat, []).append((name, status, detail))
total = len(results)
passed = sum(1 for *_, s, _ in results if s == "PASS")
failed = sum(1 for *_, s, _ in results if s == "FAIL")
skipped = sum(1 for *_, s, _ in results if s == "SKIP")
print()
print("=" * 78)
print(f"{BOLD}ML4T Third Edition - Installation Verification{RESET}")
print("=" * 78)
print()
optional_cats = {"Optional"}
for cat, items in categories.items():
cat_passed = sum(1 for _, s, _ in items if s == "PASS")
cat_total = len(items)
is_optional = cat in optional_cats
if cat_passed == cat_total:
cat_indicator = PASS
elif is_optional:
cat_indicator = f"{SKIP} (not required)"
else:
cat_indicator = FAIL
print(f"{BOLD}--- {cat} ({cat_passed}/{cat_total}) {cat_indicator}{RESET}")
for name, status, detail in items:
if status == "PASS":
indicator = PASS
elif status == "SKIP":
indicator = SKIP
else:
indicator = FAIL
# Truncate detail for display
detail_short = detail[:60] + "..." if len(detail) > 63 else detail
print(f" {indicator} {name:<45s} {detail_short}")
print()
# Separate required vs optional failures
required_fails = [(c, n, d) for c, n, s, d in results if s == "FAIL" and c not in optional_cats]
optional_fails = [(c, n, d) for c, n, s, d in results if s == "FAIL" and c in optional_cats]
# Final summary
print("=" * 78)
if not required_fails and not optional_fails:
print(f"{BOLD}{PASS} All {total} checks passed.{RESET}")
elif not required_fails:
print(
f"{BOLD}{PASS} All required checks passed ({passed - len(optional_fails)}/{total - len(optional_fails)}).{RESET}"
)
print(f" {len(optional_fails)} optional packages not installed (Docker-only extras).")
print(" These are not needed for standard usage — install with:")
print(" uv sync --extra db-benchmark --extra bayesian --extra agents --extra mlops")
else:
print(f"{BOLD}{FAIL} {len(required_fails)} REQUIRED package(s) failed:{RESET}")
print()
for cat, name, detail in required_fails:
print(f" {FAIL} {cat} / {name}")
print(f" {detail[:100]}")
if optional_fails:
print()
print(f" Plus {len(optional_fails)} optional packages (not required).")
print("=" * 78)
return failed
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
start = time.time()
# Ensure repo root is on sys.path (so utils/data/case_studies are importable)
repo_root = Path(__file__).resolve().parent.parent
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
# Run all checks
check_core_data_science()
check_visualization()
check_ml()
check_deep_learning()
check_nlp()
check_time_series()
check_causal()
check_finance()
check_rl()
check_data_sources()
check_ta()
check_synthetic()
check_utilities()
check_jupyter()
check_storage()
check_ml4t_libraries()
check_repo_imports()
check_runtime()
check_optional()
elapsed = time.time() - start
print_summary()
print(f"\nCompleted in {elapsed:.1f}s")
# Exit code: 0 if all required (non-Optional) checks pass
required_failures = sum(
1 for cat, _, status, _ in results if status == "FAIL" and cat != "Optional"
)
sys.exit(1 if required_failures > 0 else 0)
if __name__ == "__main__":
main()