From 3b151ae55ae7b70ae4a831ae4689936733379a56 Mon Sep 17 00:00:00 2001 From: "Bobby (aider)" Date: Wed, 19 Feb 2025 23:26:58 -0800 Subject: [PATCH] feat: Add Sunny-SMA scanner combining Sunny Bands and 21 SMA strategy --- src/pages/screener/technical_scanner_page.py | 2 +- src/screener/scanner_controller.py | 4 +- src/screener/t_sunny_sma.py | 125 +++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/screener/t_sunny_sma.py diff --git a/src/pages/screener/technical_scanner_page.py b/src/pages/screener/technical_scanner_page.py index 612bb5e..8ebb46b 100644 --- a/src/pages/screener/technical_scanner_page.py +++ b/src/pages/screener/technical_scanner_page.py @@ -13,7 +13,7 @@ def technical_scanner_page(): with scanner_tab: scanner_type = st.selectbox( "Select Scanner", - ["SunnyBands", "ATR-EMA", "ATR-EMA v2", "Heikin-Ashi", "Candlestick"], + ["SunnyBands", "ATR-EMA", "ATR-EMA v2", "Heikin-Ashi", "Candlestick", "Sunny-SMA"], key="tech_scanner_type" ) diff --git a/src/screener/scanner_controller.py b/src/screener/scanner_controller.py index bbe2b03..a25f87e 100644 --- a/src/screener/scanner_controller.py +++ b/src/screener/scanner_controller.py @@ -4,6 +4,7 @@ from screener.t_atr_ema import run_atr_ema_scanner from screener.t_atr_ema_v2 import run_atr_ema_scanner_v2 from screener.t_heikinashi import run_heikin_ashi_scanner from screener.t_candlestick import run_candlestick_scanner +from screener.t_sunny_sma import run_sunny_sma_scanner def run_technical_scanner(scanner_choice: str, start_date: str, end_date: str, min_price: float, max_price: float, min_volume: int, @@ -31,7 +32,8 @@ def run_technical_scanner(scanner_choice: str, start_date: str, end_date: str, "atr-ema": lambda: run_atr_ema_scanner(min_price, max_price, min_volume, portfolio_size, interval, start_dt, end_dt), "atr-ema_v2": lambda: run_atr_ema_scanner_v2(min_price, max_price, min_volume, portfolio_size, interval, start_dt, end_dt), "heikin-ashi": lambda: run_heikin_ashi_scanner(min_price, max_price, min_volume, portfolio_size, interval, start_dt, end_dt), - "candlestick": lambda: run_candlestick_scanner(min_price, max_price, min_volume, portfolio_size, interval, start_dt, end_dt, selected_patterns) + "candlestick": lambda: run_candlestick_scanner(min_price, max_price, min_volume, portfolio_size, interval, start_dt, end_dt, selected_patterns), + "sunny-sma": lambda: run_sunny_sma_scanner(min_price, max_price, min_volume, portfolio_size, interval, start_dt, end_dt) } scanner_func = scanner_map.get(scanner_choice) diff --git a/src/screener/t_sunny_sma.py b/src/screener/t_sunny_sma.py new file mode 100644 index 0000000..8da5bad --- /dev/null +++ b/src/screener/t_sunny_sma.py @@ -0,0 +1,125 @@ +import pandas as pd +import talib +from datetime import datetime +from utils.data_utils import ( + get_stock_data, validate_signal_date, print_signal, + save_signals_to_csv, get_qualified_stocks +) +from utils.scanner_utils import initialize_scanner, process_signal_data +from indicators.sunny_bands import SunnyBands + +def check_entry_signal(df: pd.DataFrame) -> list: + """ + Check for entry signals based on combined Sunny Bands and SMA strategy + + Conditions: + 1. 21 SMA below lowest Sunny Band + 2. Price crosses above 21 SMA + 3. Price still below Sunny Bands + + Args: + df (pd.DataFrame): DataFrame with price data + + Returns: + list: List of tuples (signal, date, signal_data) for each signal found + """ + if len(df) < 21: # Need at least 21 bars for SMA + return [] + + # Calculate Sunny Bands + sunny = SunnyBands() + sunny_results = sunny.calculate(df) + + # Calculate 21 day SMA + sma21 = talib.SMA(df['close'].values, timeperiod=21) + + signals = [] + + # Start from index 21 to ensure we have enough data for SMA + for i in range(21, len(df)): + current = df.iloc[i] + prev = df.iloc[i-1] + current_bands = sunny_results.iloc[i] + current_sma = sma21[i] + + # Check conditions: + # 1. SMA below lower band + sma_below_band = current_sma < current_bands['lower_band'] + + # 2. Price crosses above SMA + price_cross_sma = (current['close'] > current_sma) and (prev['close'] < sma21[i-1]) + + # 3. Price still below lower band + price_below_band = current['close'] < current_bands['lower_band'] + + if sma_below_band and price_cross_sma and price_below_band: + signal_data = { + 'price': current['close'], + 'sma21': current_sma, + 'upper_band': current_bands['upper_band'], + 'lower_band': current_bands['lower_band'], + 'dma': current_bands['dma'] + } + signals.append((True, current['date'], signal_data)) + + return signals + +def run_sunny_sma_scanner(min_price: float, max_price: float, min_volume: int, + portfolio_size: float = None, interval: str = "1d", + start_date: datetime = None, end_date: datetime = None) -> None: + """ + Run scanner combining Sunny Bands and 21 SMA strategy + """ + try: + # Initialize scanner components + interval, start_date, end_date, qualified_stocks, calculator = initialize_scanner( + min_price=min_price, + max_price=max_price, + min_volume=min_volume, + portfolio_size=portfolio_size, + interval=interval, + start_date=start_date, + end_date=end_date + ) + + if not qualified_stocks: + return + + bullish_signals = [] + + for ticker, current_price, current_volume, last_update, stock_type in qualified_stocks: + try: + df = get_stock_data(ticker, start_date, end_date, interval) + + if df.empty or len(df) < 50: # Need at least 50 bars for the indicators + continue + + signals = check_entry_signal(df) + for signal, signal_date, signal_data in signals: + # Custom print for Sunny-SMA signals + print(f"🌞 {ticker}: SMA-21 Cross at ${signal_data['price']:.2f} on {signal_date.strftime('%Y-%m-%d')}") + print(f" SMA: ${signal_data['sma21']:.2f}") + print(f" Lower Band: ${signal_data['lower_band']:.2f}") + + entry_data = { + 'ticker': ticker, + 'signal_date': signal_date, + 'entry_price': signal_data['price'], + 'sma21': signal_data['sma21'], + 'lower_band': signal_data['lower_band'], + 'volume': current_volume, + 'last_update': last_update, + 'stock_type': stock_type + } + bullish_signals.append(entry_data) + + except Exception as e: + print(f"Error processing {ticker}: {str(e)}") + continue + + save_signals_to_csv(bullish_signals, 'sunny_sma') + return bullish_signals + + except Exception as e: + print(f"Error during scan: {str(e)}") + return []