"""Clean Yahoo Finance OHLCV data and compute log returns.""" from __future__ import annotations import numpy as np import pandas as pd from src.config import DATA_PROCESSED, DATA_RAW_YAHOO from src.utils import setup_logging logger = setup_logging("yahoo_cleaner") def clean_yahoo_data() -> pd.DataFrame: """Load all Yahoo CSVs, compute log returns, return a combined DataFrame. For each ticker file (SPY.csv, BTC_USD.csv, etc.) we: 1. Parse dates and set DatetimeIndex. 2. Keep Close and Volume. 3. Compute log return = ln(Close_t / Close_{t-1}). Returns a wide DataFrame with columns like ``SPY_close``, ``SPY_volume``, ``SPY_log_return``, etc. """ combined = pd.DataFrame() for csv_path in sorted(DATA_RAW_YAHOO.glob("*.csv")): # Skip the combined all_tickers file -- we use per-ticker files if csv_path.stem == "all_tickers": continue ticker = csv_path.stem # e.g. "SPY", "BTC_USD" df = pd.read_csv(csv_path, parse_dates=["date"], index_col="date") df.index = pd.to_datetime(df.index, utc=True).normalize().tz_localize(None) # Coerce numeric for col in ["Close", "Volume"]: if col in df.columns: df[col] = pd.to_numeric(df[col], errors="coerce") if "Close" not in df.columns: logger.warning("No 'Close' column in %s, skipping", csv_path.name) continue close = df["Close"].rename(f"{ticker}_close") log_ret = np.log(df["Close"] / df["Close"].shift(1)).rename(f"{ticker}_log_return") combined[f"{ticker}_close"] = close combined[f"{ticker}_log_return"] = log_ret if "Volume" in df.columns: volume = df["Volume"].rename(f"{ticker}_volume") combined[f"{ticker}_volume"] = volume logger.info("Loaded Yahoo ticker '%s': %d observations", ticker, len(df)) combined = combined.sort_index() logger.info( "Combined Yahoo data: %d rows, %d columns, date range %s to %s", len(combined), len(combined.columns), combined.index.min(), combined.index.max(), ) return combined def clean_all() -> None: """Save ``financial_data.parquet`` to ``data/processed/``.""" DATA_PROCESSED.mkdir(parents=True, exist_ok=True) financial = clean_yahoo_data() if not financial.empty: out = DATA_PROCESSED / "financial_data.parquet" financial.to_parquet(out) logger.info("Saved %s (%d rows)", out.name, len(financial)) logger.info("=== Yahoo cleaning complete ===")