193 lines
6.9 KiB
Python
193 lines
6.9 KiB
Python
"""
|
|
Momentum Dashboard - Real-time momentum analysis for trading
|
|
|
|
Features:
|
|
- 21 EMA Trend Filter (long above, short below)
|
|
- Bollinger Band Squeeze detection
|
|
- Volume Momentum confirmation
|
|
- Multi-timeframe analysis (1H/Daily/Weekly/Monthly/Quarterly)
|
|
"""
|
|
|
|
import yfinance as yf
|
|
import pandas as pd
|
|
import numpy as np
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Dict, Optional, Tuple
|
|
|
|
# Magnificent Seven stocks
|
|
MAGNIFICENT_SEVEN = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA"]
|
|
|
|
|
|
class MomentumIndicator:
|
|
"""Core momentum indicators for the dashboard"""
|
|
|
|
@staticmethod
|
|
def ema(data: pd.Series, period: int = 21) -> pd.Series:
|
|
"""Calculate Exponential Moving Average"""
|
|
return data.ewm(span=period, adjust=False).mean()
|
|
|
|
@staticmethod
|
|
def bollinger_bands(data: pd.Series, period: int = 20, std_dev: float = 2.0) -> Tuple[pd.Series, pd.Series, pd.Series]:
|
|
"""Calculate Bollinger Bands"""
|
|
sma = data.rolling(window=period).mean()
|
|
std = data.rolling(window=period).std()
|
|
upper = sma + (std * std_dev)
|
|
lower = sma - (std * std_dev)
|
|
return upper, sma, lower
|
|
|
|
@staticmethod
|
|
def bollinger_squeeze(bb_upper: pd.Series, bb_lower: pd.Series, bb_mid: pd.Series,
|
|
threshold: float = 0.1) -> pd.Series:
|
|
"""
|
|
Detect Bollinger Band Squeeze (low volatility consolidation)
|
|
Returns True when bandwidth is below threshold
|
|
"""
|
|
bandwidth = (bb_upper - bb_lower) / bb_mid
|
|
return bandwidth < threshold
|
|
|
|
@staticmethod
|
|
def volume_momentum(volume: pd.Series, period: int = 20) -> pd.Series:
|
|
"""Calculate Volume Momentum (current volume vs average)"""
|
|
avg_volume = volume.rolling(window=period).mean()
|
|
return volume / avg_volume
|
|
|
|
@staticmethod
|
|
def rsi(data: pd.Series, period: int = 14) -> pd.Series:
|
|
"""Calculate Relative Strength Index"""
|
|
delta = data.diff()
|
|
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
|
rs = gain / loss
|
|
return 100 - (100 / (1 + rs))
|
|
|
|
|
|
class MomentumScanner:
|
|
"""Scan stocks for momentum signals"""
|
|
|
|
def __init__(self, symbols: List[str] = None):
|
|
self.symbols = symbols or MAGNIFICENT_SEVEN
|
|
self.indicators = MomentumIndicator()
|
|
|
|
def fetch_data(self, symbol: str, period: str = "3mo", interval: str = "1h") -> pd.DataFrame:
|
|
"""Fetch price data from yfinance"""
|
|
ticker = yf.Ticker(symbol)
|
|
df = ticker.history(period=period, interval=interval)
|
|
return df
|
|
|
|
def analyze_symbol(self, symbol: str) -> Dict:
|
|
"""Analyze a single symbol for momentum signals"""
|
|
try:
|
|
df = self.fetch_data(symbol)
|
|
if df.empty:
|
|
return {"symbol": symbol, "error": "No data"}
|
|
|
|
close = df['Close']
|
|
volume = df['Volume']
|
|
|
|
# Calculate indicators
|
|
ema_21 = self.indicators.ema(close, 21)
|
|
bb_upper, bb_mid, bb_lower = self.indicators.bollinger_bands(close)
|
|
squeeze = self.indicators.bollinger_squeeze(bb_upper, bb_lower, bb_mid)
|
|
vol_momentum = self.indicators.volume_momentum(volume)
|
|
rsi = self.indicators.rsi(close)
|
|
|
|
# Current values
|
|
current_price = close.iloc[-1]
|
|
current_ema = ema_21.iloc[-1]
|
|
current_rsi = rsi.iloc[-1]
|
|
current_vol_mom = vol_momentum.iloc[-1]
|
|
is_squeeze = squeeze.iloc[-1]
|
|
|
|
# Signal determination
|
|
trend = "BULLISH" if current_price > current_ema else "BEARISH"
|
|
signal_strength = self._calculate_signal_strength(
|
|
current_price, current_ema, current_rsi, current_vol_mom, is_squeeze
|
|
)
|
|
|
|
return {
|
|
"symbol": symbol,
|
|
"price": round(current_price, 2),
|
|
"ema_21": round(current_ema, 2),
|
|
"trend": trend,
|
|
"rsi": round(current_rsi, 2),
|
|
"volume_momentum": round(current_vol_mom, 2),
|
|
"squeeze": bool(is_squeeze),
|
|
"signal_strength": signal_strength,
|
|
"signal": self._get_signal(trend, signal_strength, is_squeeze)
|
|
}
|
|
except Exception as e:
|
|
return {"symbol": symbol, "error": str(e)}
|
|
|
|
def _calculate_signal_strength(self, price, ema, rsi, vol_mom, squeeze) -> float:
|
|
"""Calculate overall signal strength (0-100)"""
|
|
score = 50 # Base score
|
|
|
|
# Price vs EMA contribution
|
|
price_ema_diff = (price - ema) / ema * 100
|
|
score += min(max(price_ema_diff * 5, -20), 20)
|
|
|
|
# RSI contribution (oversold/overbought)
|
|
if rsi < 30:
|
|
score += 15 # Oversold - potential buy
|
|
elif rsi > 70:
|
|
score -= 15 # Overbought - potential sell
|
|
|
|
# Volume momentum contribution
|
|
if vol_mom > 1.5:
|
|
score += 10 # High volume confirms trend
|
|
elif vol_mom < 0.5:
|
|
score -= 10 # Low volume weakens signal
|
|
|
|
return max(0, min(100, round(score)))
|
|
|
|
def _get_signal(self, trend: str, strength: float, squeeze: bool) -> str:
|
|
"""Generate trading signal"""
|
|
if squeeze and strength > 60:
|
|
return "WATCH_FOR_BREAKOUT"
|
|
elif trend == "BULLISH" and strength >= 70:
|
|
return "STRONG_BUY"
|
|
elif trend == "BULLISH" and strength >= 55:
|
|
return "BUY"
|
|
elif trend == "BEARISH" and strength <= 30:
|
|
return "STRONG_SELL"
|
|
elif trend == "BEARISH" and strength <= 45:
|
|
return "SELL"
|
|
else:
|
|
return "HOLD"
|
|
|
|
def scan_all(self) -> List[Dict]:
|
|
"""Scan all symbols and return results"""
|
|
results = []
|
|
for symbol in self.symbols:
|
|
result = self.analyze_symbol(symbol)
|
|
results.append(result)
|
|
return results
|
|
|
|
|
|
def get_top_momentum_stocks(limit: int = 20) -> List[str]:
|
|
"""Get top momentum stocks (could be enhanced with real data source)"""
|
|
# For now, return a static list of popular momentum stocks
|
|
momentum_stocks = [
|
|
"SMCI", "ARM", "PLTR", "SNOW", "DDOG",
|
|
"MDB", "NET", "CRWD", "ZS", "PANW",
|
|
"AMD", "INTC", "QCOM", "AVGO", "TXN"
|
|
]
|
|
return momentum_stocks[:limit]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test the scanner
|
|
scanner = MomentumScanner()
|
|
results = scanner.scan_all()
|
|
|
|
print("=" * 60)
|
|
print("MOMENTUM DASHBOARD - MAGNIFICENT SEVEN")
|
|
print("=" * 60)
|
|
print(f"{'Symbol':<8} {'Price':<10} {'Trend':<10} {'RSI':<8} {'Signal':<20}")
|
|
print("-" * 60)
|
|
|
|
for r in results:
|
|
if "error" not in r:
|
|
print(f"{r['symbol']:<8} ${r['price']:<9} {r['trend']:<10} {r['rsi']:<8} {r['signal']:<20}")
|
|
|
|
print("=" * 60) |