volatilitypredictor / utils_vol.py
Gil Stetler
utils fix 1-d
3c3c589
# utils_vol.py — robuste Version
import yfinance as yf
import numpy as np
import pandas as pd
def _to_1d_series(obj: pd.Series | pd.DataFrame) -> pd.Series:
"""
Erzwingt eine 1D-Serie:
- DataFrame (n,1) -> squeeze
- MultiIndex -> erste passende Spalte
- alles in float konvertieren, NaNs droppen
"""
if isinstance(obj, pd.DataFrame):
# (n,1) -> Serie
if obj.shape[1] == 1:
ser = obj.squeeze(axis=1)
else:
# Fallback: nimm die erste numerische Spalte
num_cols = obj.select_dtypes(include=[np.number]).columns
if len(num_cols) > 0:
ser = obj[num_cols[0]]
else:
# nimm einfach die erste Spalte
ser = obj.iloc[:, 0]
else:
ser = obj
ser = pd.to_numeric(ser, errors="coerce")
ser = ser.dropna()
# Index in DatetimeIndex verwandeln, wenn möglich
if not isinstance(ser.index, pd.DatetimeIndex):
try:
ser.index = pd.to_datetime(ser.index, errors="coerce")
ser = ser[ser.index.notna()]
except Exception:
# notfalls RangeIndex lassen
pass
return ser.astype(float)
def fetch_close_series(ticker: str, start: str = "2015-01-01", interval: str = "1d") -> pd.Series:
"""
Lädt OHLCV via yfinance und gibt eine 1D-Schlusskurs-Serie zurück.
Nutzt auto_adjust=True (aktuelles yfinance-Default) bewusst,
damit der FutureWarning verschwindet und Adjusted/Close konsistent ist.
"""
df = yf.download(
ticker.strip(),
start=start,
interval=interval,
auto_adjust=True, # explizit setzen, um Warnung zu vermeiden
progress=False,
threads=True,
)
if df is None or df.empty:
raise ValueError(f"Keine Daten für {ticker} (start={start}, interval={interval}).")
# MultiIndex-Handling (bei mehreren Tickern oder Börsen-Suffixen)
if isinstance(df.columns, pd.MultiIndex):
# versuche 'Close' auf Level 0
if "Close" in df.columns.get_level_values(0):
sub = df.xs("Close", axis=1, level=0)
# falls mehrere Spalten (mehrere Ticker): nimm die erste
if sub.shape[1] > 1:
sub = sub.iloc[:, 0]
return _to_1d_series(sub)
# Fallback: erste numerische Spalte
num_cols = df.select_dtypes(include=[np.number]).columns
if len(num_cols) > 0:
sub = df[num_cols[0]]
return _to_1d_series(sub)
# letzter Ausweg: erste Spalte
return _to_1d_series(df.iloc[:, 0])
# Flache Spalten
for name in ["Close", "Adj Close", "close", "adj close", "Price", "price"]:
if name in df.columns:
return _to_1d_series(df[name])
# Fallback: erste numerische Spalte
num_cols = df.select_dtypes(include=[np.number]).columns
if len(num_cols) == 0:
raise ValueError("Keine numerische Close-Spalte gefunden.")
return _to_1d_series(df[num_cols[0]])
def realized_vol(close: pd.Series, window: int = 20, annualize: bool = True) -> pd.Series:
"""
20-Tage-Rolling-Std der Logrenditen; gibt IMMER eine 1D-Serie zurück.
"""
close = _to_1d_series(close)
r = np.log(close).diff().dropna()
rv = r.rolling(window, min_periods=window).std()
if annualize:
rv = rv * np.sqrt(252.0)
rv = rv.dropna()
# Sicherheitshalber 1D
return _to_1d_series(rv)
def rv_to_autogluon_df(rv: pd.Series | pd.DataFrame) -> pd.DataFrame:
"""
Formatiert Realized Vol als DataFrame für AutoGluon TimeSeries:
columns: ['item_id', 'timestamp', 'target']
"""
# Erzwinge Serie 1D
rv = _to_1d_series(rv)
# Werte & Index robust extrahieren
values = np.asarray(rv.values).reshape(-1) # 1D
idx = rv.index
if not isinstance(idx, pd.DatetimeIndex):
try:
idx = pd.to_datetime(idx, errors="coerce")
except Exception:
# Fallback: generiere einfache Range-Dates
idx = pd.date_range(start="2000-01-01", periods=len(values), freq="D")
# gültige Punkte
mask = ~np.isnan(values)
df = pd.DataFrame({
"item_id": "series_1",
"timestamp": idx[mask],
"target": values[mask],
})
# sortiert & ohne NaN-Timestamps
df = df[df["timestamp"].notna()].sort_values("timestamp")
return df