| """ |
| Feature Store β Central registry for computing and caching all features. |
| Single entry point for the signal engine to get a complete feature set. |
| """ |
| import logging |
| import time |
|
|
| import pandas as pd |
|
|
| from backend.data.market_data import fetch_ohlcv |
| from backend.features.fundamental import compute_fundamental_features |
| from backend.features.momentum import compute_momentum_features |
| from backend.features.technical import compute_technical_features |
| from backend.features.volatility import compute_volatility_features |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def compute_all_features(ticker: str, period: str = "1y") -> dict: |
| """ |
| Compute ALL features for a single ticker. |
| |
| Returns a dict with: |
| - 'df': Full feature DataFrame (time series) |
| - 'latest': Latest row as a flat dict (for signal engine) |
| - 'fundamentals': Fundamental metrics dict |
| - 'ticker': The ticker symbol |
| - 'computed_at': Timestamp |
| """ |
| start = time.time() |
| logger.info(f"{ticker}: Computing all features...") |
|
|
| |
| ohlcv = fetch_ohlcv(ticker, period=period) |
| if ohlcv.empty: |
| logger.error(f"{ticker}: No OHLCV data β cannot compute features") |
| return {"ticker": ticker, "error": "no_data"} |
|
|
| |
| df = compute_technical_features(ohlcv, ticker) |
|
|
| |
| df = compute_momentum_features(df, ticker) |
|
|
| |
| df = compute_volatility_features(df, ticker) |
|
|
| |
| fundamentals = compute_fundamental_features(ticker) |
|
|
| |
| latest = {} |
| if not df.empty: |
| last_row = df.iloc[-1] |
| for k, v in last_row.items(): |
| try: |
| if pd.notna(v): |
| latest[k] = round(float(v), 4) if isinstance(v, (int, float)) else str(v) |
| else: |
| latest[k] = None |
| except (TypeError, ValueError): |
| latest[k] = str(v) if v is not None else None |
|
|
| |
| latest["fundamental_score"] = fundamentals.get("fundamental_score", 50.0) |
| latest["sector"] = fundamentals.get("sector", "Unknown") |
| latest["industry"] = fundamentals.get("industry", "Unknown") |
| latest["market_cap"] = fundamentals.get("market_cap") |
| latest["pe_ratio"] = fundamentals.get("pe_ratio") |
|
|
| elapsed = round(time.time() - start, 2) |
| logger.info(f"{ticker}: All features computed in {elapsed}s ({len(df)} rows, {len(df.columns)} cols)") |
|
|
| return { |
| "ticker": ticker, |
| "df": df, |
| "latest": latest, |
| "fundamentals": fundamentals, |
| "computed_at": time.time(), |
| "elapsed_seconds": elapsed, |
| } |
|
|
|
|
| def compute_universe_features(tickers: list[str], period: str = "1y") -> list[dict]: |
| """ |
| Compute features for an entire universe of tickers. |
| Returns a list of feature dicts (one per ticker). |
| """ |
| results = [] |
| total = len(tickers) |
|
|
| for i, ticker in enumerate(tickers): |
| try: |
| result = compute_all_features(ticker, period) |
| if "error" not in result: |
| results.append(result) |
| else: |
| logger.warning(f"{ticker}: Skipped β {result.get('error')}") |
| except Exception as e: |
| logger.error(f"{ticker}: Feature computation failed: {e}") |
|
|
| if (i + 1) % 10 == 0: |
| logger.info(f"Universe progress: {i + 1}/{total} tickers processed") |
|
|
| logger.info(f"Universe features complete: {len(results)}/{total} successful") |
| return results |
|
|
|
|
| def get_cross_sectional_matrix(feature_results: list[dict], columns: list[str] | None = None) -> pd.DataFrame: |
| """ |
| Build a cross-sectional matrix (tickers Γ features) from computed results. |
| Useful for ranking and comparison. |
| """ |
| rows = [] |
| for result in feature_results: |
| latest = result.get("latest", {}) |
| latest["ticker"] = result["ticker"] |
| rows.append(latest) |
|
|
| df = pd.DataFrame(rows) |
| if columns and not df.empty: |
| available = [c for c in columns if c in df.columns] |
| df = df[["ticker"] + available] |
|
|
| return df |
|
|