Blum / backend /app /scoring /factor_engine.py
Italianhype's picture
Add self learning financial brain layer
89afbf3 verified
Raw
History Blame Contribute Delete
3.35 kB
from __future__ import annotations
from app.scoring.explanations import build_score_explanation, opportunity_label, watch_points_from_factors
from app.scoring.normalization import avg, clamp, safe_float, scale
from app.scoring.weights import OPPORTUNITY_WEIGHTS, normalized_weights
def compute_opportunity_score(asset: dict, signal: dict | None, market_snapshot: dict, sector_score: float = 50, macro_score: float = 50, weights_override: dict | None = None) -> dict:
breakdown = (signal or {}).get("score_breakdown") or {}
narrative = (signal or {}).get("narrative_flags") or {}
technical = (signal or {}).get("technical_flags") or {}
momentum = avg(
safe_float(breakdown.get("momentum_score")),
scale(market_snapshot.get("perf_5d"), -8, 8),
scale(market_snapshot.get("perf_1m"), -14, 16),
)
trend = avg(safe_float(breakdown.get("trend_score")), 82 if technical.get("above_sma20") else 42, 82 if technical.get("above_sma50") else 42)
relative_strength = safe_float(breakdown.get("etf_confirmation_score"), 50)
volume = avg(scale(technical.get("volume_spike"), 0, 180), 72 if safe_float(market_snapshot.get("volume")) > 0 else 38)
sentiment = avg(safe_float(breakdown.get("sentiment_score")), scale(narrative.get("sentiment_7d"), -0.6, 0.6))
news = avg(scale(narrative.get("news_count_7d"), 0, 6), scale(narrative.get("narrative_intensity"), 0, 100))
risk = compute_risk_score(signal, technical)
weights = normalized_weights(weights_override or OPPORTUNITY_WEIGHTS)
opportunity = (
momentum * weights["momentum"]
+ trend * weights["trend"]
+ relative_strength * weights["relative_strength"]
+ volume * weights["volume"]
+ sentiment * weights["sentiment"]
+ news * weights["news"]
+ sector_score * weights["sector"]
+ macro_score * weights["macro"]
+ (100 - risk) * weights["risk"]
)
factors = {
"opportunity_score": round(clamp(opportunity), 1),
"trend_score": round(clamp(trend), 1),
"momentum_score": round(clamp(momentum), 1),
"sentiment_score": round(clamp(sentiment), 1),
"news_score": round(clamp(news), 1),
"risk_score": round(clamp(risk), 1),
"relative_strength_score": round(clamp(relative_strength), 1),
"volume_score": round(clamp(volume), 1),
"sector_score": round(clamp(sector_score), 1),
"macro_score": round(clamp(macro_score), 1),
}
ticker = asset.get("ticker", "")
return {
**factors,
"status_label": opportunity_label(factors["opportunity_score"], factors["risk_score"]),
"why_today": build_score_explanation(ticker, factors, factors["opportunity_score"]),
"watch_points": watch_points_from_factors(factors),
"weights": weights,
}
def compute_risk_score(signal: dict | None, technical: dict) -> float:
if signal and signal.get("risk_level") == "High":
base = 78
elif signal and signal.get("risk_level") == "Low":
base = 35
else:
base = 52
volatility = scale(technical.get("historical_volatility"), 12, 70)
drawdown = scale(abs(safe_float(technical.get("recent_drawdown"))), 0, 30)
overextension = scale(technical.get("rsi"), 58, 78)
return avg(base, volatility, drawdown, overextension)