import os import json import subprocess from datetime import datetime from typing import Dict, Any, List, Optional from pydantic import BaseModel, Field class ExperimentManifest(BaseModel): """ Typed Manifest tracking all dataset, parameter, model, and git metadata to ensure 100% reproducible out-of-sample experiment runs. """ experiment_id: str = Field(default_factory=lambda: f"exp_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}") timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat()) dataset_id: str = "us_etf_daily_v1" hf_repo: str = "thecharttruth/etf-data" hf_revision: str = "main" universe_rule: str = "price > 5 & adv20 > 50m & age >= 252" asset_universe: List[str] = Field(default_factory=list) start_date: str = "2014-01-01" end_date: str = "2023-12-31" lookback_days: int = 20 holding_days: int = 5 rebalance_freq: str = "weekly" transaction_cost_bps: float = 5.0 slippage_bps: float = 2.0 cv_scheme: str = "purged_walk_forward_v1" embargo_days: int = 5 random_seed: int = 42 git_commit: str = "unknown" feature_config: Dict[str, Any] = Field(default_factory=dict) model_hyperparams: Dict[str, Any] = Field(default_factory=dict) metrics_summary: Dict[str, Any] = Field(default_factory=dict) def get_git_commit_hash() -> str: """Retrieve current git commit hash if in a git repository.""" try: commit = subprocess.check_output(["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL).decode("utf-8").strip() return commit except Exception: return "uncommitted_workspace" def create_manifest( asset_universe: List[str], dataset_id: str = "us_etf_daily_v1", hf_repo: str = "thecharttruth/etf-data", hf_revision: str = "main", lookback_days: int = 20, holding_days: int = 5, transaction_cost_bps: float = 5.0, random_seed: int = 42, feature_config: Optional[Dict[str, Any]] = None, model_hyperparams: Optional[Dict[str, Any]] = None, ) -> ExperimentManifest: """Helper to instantiate an ExperimentManifest with default git commit resolution.""" return ExperimentManifest( dataset_id=dataset_id, hf_repo=hf_repo, hf_revision=hf_revision, asset_universe=asset_universe, lookback_days=lookback_days, holding_days=holding_days, transaction_cost_bps=transaction_cost_bps, random_seed=random_seed, git_commit=get_git_commit_hash(), feature_config=feature_config or {"residual_mom": True, "sortino_mom": True, "volume_z": True}, model_hyperparams=model_hyperparams or {"type": "lightgbm", "max_depth": 3, "learning_rate": 0.01}, ) def save_manifest(manifest: ExperimentManifest, filepath: str) -> str: """Save manifest to JSON file.""" os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True) with open(filepath, "w", encoding="utf-8") as f: f.write(manifest.model_dump_json(indent=2)) return filepath def load_manifest(filepath: str) -> ExperimentManifest: """Load manifest from JSON file.""" with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) return ExperimentManifest(**data)