""" Data + metric helpers for the hosted dashboard (trimmed, no matplotlib/CLI). Mirrors the functions of the same name in ../predict_us.py. """ import numpy as np import pandas as pd def _clamp_intraday_period(interval: str, period: str) -> str: """yfinance only serves intraday history for a limited window: minute bars ~60d (1m/2m ~7d), hourly ~730d. Clamp long periods for intraday so it still returns data.""" iv = interval.lower() if iv.endswith("m") and not iv.endswith("mo"): return "7d" if iv in ("1m", "2m") else "60d" if iv.endswith("h"): return "730d" return period def fetch_ohlcv(ticker: str, interval: str, period: str) -> pd.DataFrame: """Download OHLCV via yfinance and normalise to Kronos' expected schema.""" import yfinance as yf period = _clamp_intraday_period(interval, period) raw = yf.download(ticker, interval=interval, period=period, auto_adjust=True, progress=False) if raw is None or raw.empty: raise RuntimeError( f"No data returned for {ticker} ({interval}, {period}). " f"Intraday history is limited (5m≈60d, 1h≈730d)." ) if isinstance(raw.columns, pd.MultiIndex): raw.columns = raw.columns.get_level_values(0) raw = raw.rename(columns=str.lower) df = raw[["open", "high", "low", "close", "volume"]].copy() df = df.dropna().reset_index() ts_col = "Date" if "Date" in df.columns else "Datetime" df = df.rename(columns={ts_col: "timestamps"}) df["timestamps"] = pd.to_datetime(df["timestamps"]).dt.tz_localize(None) return df def future_timestamps(last_ts: pd.Timestamp, interval: str, n: int) -> pd.Series: """Generate n future timestamps after last_ts, respecting the bar interval.""" if interval.endswith("d"): idx = pd.bdate_range(start=last_ts, periods=n + 1, freq="C")[1:] elif interval.endswith("h"): idx = pd.date_range(start=last_ts, periods=n + 1, freq="h")[1:] else: step = int("".join(c for c in interval if c.isdigit()) or "1") idx = pd.date_range(start=last_ts, periods=n + 1, freq=f"{step}min")[1:] return pd.Series(idx) def report_metrics(truth: np.ndarray, pred: np.ndarray) -> dict: """Accuracy metrics on the close price.""" mae = float(np.mean(np.abs(pred - truth))) rmse = float(np.sqrt(np.mean((pred - truth) ** 2))) mape = float(np.mean(np.abs((pred - truth) / truth)) * 100) dir_truth = np.sign(np.diff(truth)) dir_pred = np.sign(np.diff(pred)) da = float(np.mean(dir_truth == dir_pred) * 100) if len(dir_truth) else float("nan") return {"MAE": mae, "RMSE": rmse, "MAPE%": mape, "DirAcc%": da} def baseline_skill(hist_close: np.ndarray, truth: np.ndarray, pred: np.ndarray) -> dict: """MASE (vs in-sample one-step naive) + skill-vs-random-walk (vs flat persistence). <1 means the forecast beats doing nothing; the core trust yardstick.""" hist_close = np.asarray(hist_close, float) truth = np.asarray(truth, float) pred = np.asarray(pred, float) mae = float(np.mean(np.abs(pred - truth))) scale = float(np.mean(np.abs(np.diff(hist_close)))) if len(hist_close) > 1 else float("nan") mase = mae / scale if scale and scale > 1e-9 else float("nan") last = hist_close[-1] mae_rw = float(np.mean(np.abs(last - truth))) skill = mae / mae_rw if mae_rw > 1e-9 else float("nan") return {"MASE": mase, "skill_vs_rw": skill, "MAE_rw": mae_rw}