diff --git a/tradingagents/dashboards/momentum/README.md b/tradingagents/dashboards/momentum/README.md new file mode 100644 index 00000000..83e15ff4 --- /dev/null +++ b/tradingagents/dashboards/momentum/README.md @@ -0,0 +1,79 @@ +# Momentum Dashboard + +Real-time momentum analysis dashboard for TradingAgents. + +## Features + +✅ **21 EMA Trend Filter** - Long above, short below +✅ **Bollinger Band Squeeze** - Identify low-volatility consolidation +✅ **Volume Momentum** - Confirm breakouts +✅ **RSI Indicator** - Overbought/oversold signals +✅ **Multi-timeframe** - 1H/Daily/Weekly analysis +✅ **Magnificent Seven** + Custom watchlists + +## Quick Start + +### 1. Install Dependencies +```bash +pip install streamlit plotly yfinance pandas numpy +``` + +### 2. Run Dashboard +```bash +cd tradingagents/dashboards/momentum +streamlit run app.py +``` + +### 3. CLI Scanner (No UI) +```bash +python -m tradingagents.dashboards.momentum +``` + +## Signals + +| Signal | Meaning | +|--------|---------| +| STRONG_BUY | Bullish trend + high strength | +| BUY | Moderate bullish | +| WATCH_FOR_BREAKOUT | Squeeze detected, potential move | +| HOLD | No clear signal | +| SELL | Moderate bearish | +| STRONG_SELL | Bearish trend + low strength | + +## Architecture + +``` +momentum/ +├── __init__.py # Core scanner & indicators +├── app.py # Streamlit UI +└── README.md # This file +``` + +## Integration + +To integrate with TradingAgents main workflow: + +```python +from tradingagents.dashboards.momentum import MomentumScanner + +scanner = MomentumScanner(["AAPL", "NVDA", "TSLA"]) +signals = scanner.scan_all() + +# Use signals in trading decisions +for signal in signals: + if signal["signal"] == "STRONG_BUY": + # Execute buy logic + pass +``` + +## Future Enhancements + +- [ ] Real-time WebSocket data (Polygon.io) +- [ ] Alert notifications (email/Telegram) +- [ ] Portfolio tracking +- [ ] Backtesting mode +- [ ] Multi-exchange support + +--- + +Built for TradingAgents by OpenClaw Community \ No newline at end of file diff --git a/tradingagents/dashboards/momentum/__init__.py b/tradingagents/dashboards/momentum/__init__.py new file mode 100644 index 00000000..38018d46 --- /dev/null +++ b/tradingagents/dashboards/momentum/__init__.py @@ -0,0 +1,193 @@ +""" +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) \ No newline at end of file diff --git a/tradingagents/dashboards/momentum/app.py b/tradingagents/dashboards/momentum/app.py new file mode 100644 index 00000000..733d50f6 --- /dev/null +++ b/tradingagents/dashboards/momentum/app.py @@ -0,0 +1,258 @@ +""" +Momentum Dashboard - Streamlit Web UI + +Run: streamlit run app.py +""" + +import streamlit as st +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from datetime import datetime, timedelta +import sys +import os + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from __init__ import MomentumScanner, MomentumIndicator, MAGNIFICENT_SEVEN, get_top_momentum_stocks + + +# Page config +st.set_page_config( + page_title="Momentum Dashboard", + page_icon="📈", + layout="wide", + initial_sidebar_state="expanded" +) + +# Custom CSS +st.markdown(""" + +""", unsafe_allow_html=True) + + +def main(): + st.title("📈 Momentum Dashboard") + st.subheader("Real-time Momentum Analysis for Trading") + + # Sidebar + st.sidebar.header("Settings") + + # Stock selection + stock_option = st.sidebar.radio( + "Stock Universe", + ["Magnificent Seven", "Top Momentum", "Custom"] + ) + + if stock_option == "Magnificent Seven": + symbols = MAGNIFICENT_SEVEN + elif stock_option == "Top Momentum": + symbols = get_top_momentum_stocks(15) + else: + custom_symbols = st.sidebar.text_input( + "Enter symbols (comma-separated)", + "AAPL,MSFT,GOOGL" + ) + symbols = [s.strip().upper() for s in custom_symbols.split(",")] + + # Timeframe selection + timeframe = st.sidebar.selectbox( + "Timeframe", + ["1h", "1d", "1wk"] + ) + + # Refresh button + if st.sidebar.button("🔄 Refresh Data"): + st.rerun() + + # Scan stocks + with st.spinner("Scanning momentum signals..."): + scanner = MomentumScanner(symbols) + results = scanner.scan_all() + + # Display results + col1, col2, col3 = st.columns(3) + + # Metrics + bullish_count = sum(1 for r in results if r.get("trend") == "BULLISH" and "error" not in r) + bearish_count = sum(1 for r in results if r.get("trend") == "BEARISH" and "error" not in r) + squeeze_count = sum(1 for r in results if r.get("squeeze") and "error" not in r) + + with col1: + st.metric("🟢 Bullish", bullish_count) + with col2: + st.metric("🔴 Bearish", bearish_count) + with col3: + st.metric("🟠 Squeeze", squeeze_count) + + st.divider() + + # Results table + st.subheader("📊 Momentum Signals") + + # Create DataFrame + df_data = [] + for r in results: + if "error" not in r: + df_data.append({ + "Symbol": r["symbol"], + "Price": f"${r['price']}", + "EMA 21": f"${r['ema_21']}", + "Trend": r["trend"], + "RSI": r["rsi"], + "Vol Mom": f"{r['volume_momentum']:.2f}x", + "Squeeze": "🔴" if r["squeeze"] else "", + "Signal": r["signal"], + "Strength": r["signal_strength"] + }) + + df = pd.DataFrame(df_data) + + # Style the dataframe + def color_trend(val): + if val == "BULLISH": + return "color: green; font-weight: bold" + elif val == "BEARISH": + return "color: red; font-weight: bold" + return "" + + def color_signal(val): + if "BUY" in val: + return "background-color: #1a4d1a; color: white" + elif "SELL" in val: + return "background-color: #4d1a1a; color: white" + elif "BREAKOUT" in val: + return "background-color: #4d3d1a; color: white" + return "" + + styled_df = df.style.applymap(color_trend, subset=["Trend"]).applymap(color_signal, subset=["Signal"]) + st.dataframe(styled_df, use_container_width=True) + + st.divider() + + # Detailed chart for selected symbol + st.subheader("📈 Detailed Analysis") + + selected_symbol = st.selectbox( + "Select symbol for detailed view", + [r["symbol"] for r in results if "error" not in r] + ) + + if selected_symbol: + with st.spinner(f"Loading {selected_symbol} chart..."): + # Fetch data for chart + import yfinance as yf + ticker = yf.Ticker(selected_symbol) + hist = ticker.history(period="3mo", interval=timeframe) + + if not hist.empty: + # Calculate indicators + indicators = MomentumIndicator() + close = hist['Close'] + + ema_21 = indicators.ema(close, 21) + bb_upper, bb_mid, bb_lower = indicators.bollinger_bands(close) + + # Create candlestick chart with indicators + fig = make_subplots( + rows=3, cols=1, + shared_xaxes=True, + vertical_spacing=0.05, + row_heights=[0.6, 0.2, 0.2] + ) + + # Candlestick + fig.add_trace( + go.Candlestick( + x=hist.index, + open=hist['Open'], + high=hist['High'], + low=hist['Low'], + close=hist['Close'], + name="Price" + ), + row=1, col=1 + ) + + # EMA 21 + fig.add_trace( + go.Scatter( + x=hist.index, + y=ema_21, + line=dict(color='yellow', width=1), + name="EMA 21" + ), + row=1, col=1 + ) + + # Bollinger Bands + fig.add_trace( + go.Scatter( + x=hist.index, + y=bb_upper, + line=dict(color='gray', width=1, dash='dash'), + name="BB Upper" + ), + row=1, col=1 + ) + fig.add_trace( + go.Scatter( + x=hist.index, + y=bb_lower, + line=dict(color='gray', width=1, dash='dash'), + name="BB Lower" + ), + row=1, col=1 + ) + + # Volume + fig.add_trace( + go.Bar( + x=hist.index, + y=hist['Volume'], + name="Volume", + marker_color='lightblue' + ), + row=2, col=1 + ) + + # RSI + rsi = indicators.rsi(close) + fig.add_trace( + go.Scatter( + x=hist.index, + y=rsi, + line=dict(color='purple', width=1), + name="RSI" + ), + row=3, col=1 + ) + # RSI levels + fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1) + fig.add_hline(y=30, line_dash="dash", line_color="green", row=3, col=1) + + fig.update_layout( + title=f"{selected_symbol} - Momentum Analysis", + xaxis_rangeslider_visible=False, + height=800 + ) + + st.plotly_chart(fig, use_container_width=True) + + # Footer + st.divider() + st.caption(f"Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Data: Yahoo Finance") + + +if __name__ == "__main__": + main() \ No newline at end of file