File size: 3,495 Bytes
ed1fefe
 
 
 
 
 
 
 
0c62d61
 
 
 
 
 
 
 
 
 
 
ed1fefe
 
 
 
0c62d61
ed1fefe
 
 
0c62d61
 
 
 
ed1fefe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c9cbf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""
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}