Swing_Quant_Engine / backend /features /feature_store.py
SiddharthVenba's picture
Initial commit for HF Space
75d9b3c
Raw
History Blame Contribute Delete
4.21 kB
"""
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...")
# 1. Fetch OHLCV
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"}
# 2. Technical features
df = compute_technical_features(ohlcv, ticker)
# 3. Momentum features
df = compute_momentum_features(df, ticker)
# 4. Volatility features
df = compute_volatility_features(df, ticker)
# 5. Fundamentals (separate β€” not time-series)
fundamentals = compute_fundamental_features(ticker)
# 6. Extract latest row as flat dict
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
# Add fundamental score to latest
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