From 5c56f8dc267f7a4cf0bfd0a04294b603889dccda Mon Sep 17 00:00:00 2001 From: Aitous <58769760+Aitous@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:49:43 -0700 Subject: [PATCH] =?UTF-8?q?research(autonomous):=202026-04-15=20=E2=80=94?= =?UTF-8?q?=20automated=20research=20run=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- docs/iterations/LEARNINGS.md | 1 + .../research/2026-04-15-rsi-mean-reversion.md | 88 +++++++++ .../dataflows/discovery/scanners/__init__.py | 1 + .../discovery/scanners/rsi_oversold.py | 172 ++++++++++++++++++ tradingagents/default_config.py | 12 ++ 5 files changed, 274 insertions(+) create mode 100644 docs/iterations/research/2026-04-15-rsi-mean-reversion.md create mode 100644 tradingagents/dataflows/discovery/scanners/rsi_oversold.py diff --git a/docs/iterations/LEARNINGS.md b/docs/iterations/LEARNINGS.md index 0f616ef4..d5a4024d 100644 --- a/docs/iterations/LEARNINGS.md +++ b/docs/iterations/LEARNINGS.md @@ -20,6 +20,7 @@ | Title | File | Date | Summary | |-------|------|------|---------| +| RSI(2) Mean Reversion Oversold Bounce | research/2026-04-15-rsi-mean-reversion.md | 2026-04-15 | Connors RSI(2)<10 + price above 200d SMA = 75-79% win rate over 25y backtest; only contrarian signal in pipeline; implemented as rsi_oversold scanner | | OBV Divergence Accumulation | research/2026-04-14-obv-divergence.md | 2026-04-14 | OBV rising while price flat/down = multi-week institutional accumulation; qualitative 68% win rate; implemented as obv_divergence scanner | | Short Interest Squeeze Scanner | research/2026-04-12-short-interest-squeeze.md | 2026-04-12 | High SI (>20%) + DTC >5 as squeeze-risk discovery; implemented as short_squeeze scanner | | 52-Week High Breakout Momentum | research/2026-04-13-52-week-high-breakout.md | 2026-04-13 | George & Hwang (2004) validated: 52w high crossing + 1.5x volume = 72% win rate, +11.4% avg over 31d; implemented as high_52w_breakout scanner | diff --git a/docs/iterations/research/2026-04-15-rsi-mean-reversion.md b/docs/iterations/research/2026-04-15-rsi-mean-reversion.md new file mode 100644 index 00000000..588667ff --- /dev/null +++ b/docs/iterations/research/2026-04-15-rsi-mean-reversion.md @@ -0,0 +1,88 @@ +# Research: RSI(2) Mean Reversion Oversold Bounce + +**Date:** 2026-04-15 +**Mode:** autonomous + +## Summary + +Larry Connors' 2-period RSI mean-reversion strategy surfaces stocks in uptrends (price +above 200-day SMA) that have pulled back sharply enough to register RSI(2) < 10. The +200-day SMA filter is the critical guard against catching falling knives — without it, +the plain RSI < 30 rule fails in persistent downtrends. Academic evidence from Lehmann +(1990) and Alpha Architect confirms weekly losers revert at 0.86–1.24% per week, with +contrarian strategies generating >2% per month in abnormal returns. This is the only +contrarian signal not represented anywhere in the current momentum-heavy pipeline. + +## Sources Reviewed + +- **QuantifiedStrategies (search results)**: RSI(2) strategy with 75–79% win rate over + 25-year backtest (2000–2025); lower RSI at entry → higher subsequent returns; profit + factor ≈ 2.08 at best settings. +- **Medium / FMZQuant — Larry Connors RSI2**: Exact rule: price above 200d SMA AND + RSI(2) < 10 → buy; exit when RSI(2) > 90. Tested on DIA and individual equities. + Described as "fairly aggressive short-term" with entry on close. +- **StockCharts ChartSchool — RSI(2)**: Entry RSI(2) ≤ 5 (aggressive) or ≤ 10; exit + on move above 5-day SMA or RSI(2) > 90. Volume filter: 20-day avg volume > 40k. + Warns: "RSI(2) can remain oversold a long time in a bear" → SMA200 filter mandatory. +- **Alpha Architect — Short-Term Return Reversal (Lehmann 1990)**: Weekly losers + generate +0.86% to +1.24% per week in the subsequent week; contrarian strategies + (buy losers, sell winners) produce >2%/month abnormal returns. Effect is strongest + for liquid, actively-traded stocks. +- **Alpha Architect — Combining Reversals + Momentum**: Reversal and momentum coexist + at the 1-month horizon — reversal is dominant among low-turnover stocks, momentum + among high-turnover. Filtering to high-liquidity names (min avg volume) reduces noise. +- **WebSearch aggregate**: Connors 25-year backtest CAGR 8.2%, max drawdown 16%; + performance degrades in prolonged bear markets (2008, Mar 2020) — SMA200 filter + critical; best results when SPY itself is not in freefall. + +## Cross-Reference: Existing Pipeline + +- **No existing mean-reversion scanner.** All current scanners (minervini, + high_52w_breakout, technical_breakout, obv_divergence, short_squeeze, insider_buying, + options_flow, earnings_beat) are momentum- or event-driven. The RSI oversold bounce + is fully orthogonal. +- **technical_breakout** (scanners/technical_breakout.md): targets resistance breakouts, + opposite signal direction. No overlap. +- **obv_divergence**: detects flat price + rising OBV (accumulation). Partial overlap + in that both can flag a beaten-down stock, but OBV divergence requires volume evidence + of buying; RSI oversold can fire on pure price action. +- **No prior research file** on mean reversion or RSI. + +## Fit Evaluation + +| Dimension | Score | Notes | +|-----------|-------|-------| +| Data availability | ✅ | yfinance OHLCV + `download_ohlcv_cached` fully integrated; RSI(2) computable from close prices, 200d SMA from same data | +| Complexity | trivial/moderate | RSI(2) is a 6-line calculation; same code pattern as `high_52w_breakout` which already uses `download_ohlcv_cached` | +| Signal uniqueness | low overlap | Only contrarian scanner in the entire pipeline; orthogonal to all momentum signals | +| Evidence quality | backtested | Connors 25-year backtest, 75–79% win rate; Lehmann (1990) academic paper; Alpha Architect reversal review | + +All four auto-implement thresholds pass → **implement**. + +## Recommendation + +**Implement** — Pipeline gap: zero mean-reversion coverage. RSI(2) with SMA200 trend +filter is one of the most replicated mean-reversion signals in quant literature, data +is fully available, and implementation is trivial following the `high_52w_breakout` +template. Expected holding period: 3–7 days (exit when RSI(2) > 90 or closes above +5-day SMA). + +## Proposed Scanner Spec + +- **Scanner name:** `rsi_oversold` +- **Data source:** `tradingagents/dataflows/data_cache/ohlcv_cache.py` via + `download_ohlcv_cached` (same as `high_52w_breakout`) +- **Signal logic:** + 1. Load 1-year OHLCV for full universe + 2. Compute RSI(2) from last 3 closes: avg_gain/avg_loss over 2 periods + 3. Compute 200-day SMA from close series + 4. **Filter:** price > 200d SMA (uptrend guard) AND RSI(2) < `max_rsi` (default 10) + AND close > `min_price` (default $5) AND avg_vol_20d > `min_avg_volume` (default 100k) + 5. Sort by RSI(2) ascending (most oversold first) +- **Priority rules:** + - CRITICAL if RSI(2) < 5 (extreme oversold, highest expected bounce) + - HIGH if RSI(2) < 8 + - MEDIUM if RSI(2) < 10 +- **Context format:** + `"RSI(2) oversold at {rsi:.1f} | Price ${price:.2f} above 200d SMA ${sma200:.2f} + (+{pct:.1f}%) | 3–7d mean-reversion bounce setup"` diff --git a/tradingagents/dataflows/discovery/scanners/__init__.py b/tradingagents/dataflows/discovery/scanners/__init__.py index 50f3c71d..0ca0c4dd 100644 --- a/tradingagents/dataflows/discovery/scanners/__init__.py +++ b/tradingagents/dataflows/discovery/scanners/__init__.py @@ -14,6 +14,7 @@ from . import ( options_flow, # noqa: F401 reddit_dd, # noqa: F401 reddit_trending, # noqa: F401 + rsi_oversold, # noqa: F401 sector_rotation, # noqa: F401 semantic_news, # noqa: F401 short_squeeze, # noqa: F401 diff --git a/tradingagents/dataflows/discovery/scanners/rsi_oversold.py b/tradingagents/dataflows/discovery/scanners/rsi_oversold.py new file mode 100644 index 00000000..6cae0cba --- /dev/null +++ b/tradingagents/dataflows/discovery/scanners/rsi_oversold.py @@ -0,0 +1,172 @@ +"""RSI(2) mean-reversion oversold-bounce scanner. + +Based on Larry Connors' 2-period RSI strategy (backtested 25 years, 75-79% win +rate) and Lehmann (1990) academic evidence for short-term weekly return reversals +(+0.86% to +1.24% per week for prior-week losers). + +Signal: RSI(2) < 10 AND price above 200-day SMA. +The 200-day SMA filter is the critical guard against catching falling knives — +in persistent downtrends RSI(2) can stay oversold for weeks. With the trend +filter in place, the signal captures temporary pullbacks within an ongoing uptrend. + +Expected holding period: 3–7 days. Exit target: RSI(2) > 90 or close above 5-day SMA. + +Research: docs/iterations/research/2026-04-15-rsi-mean-reversion.md +""" + +from typing import Any, Dict, List, Optional + +import pandas as pd + +from tradingagents.dataflows.data_cache.ohlcv_cache import download_ohlcv_cached +from tradingagents.dataflows.discovery.scanner_registry import SCANNER_REGISTRY, BaseScanner +from tradingagents.dataflows.discovery.utils import Priority +from tradingagents.dataflows.universe import load_universe +from tradingagents.utils.logger import get_logger + +logger = get_logger(__name__) + + +class RSIOversoldScanner(BaseScanner): + """Scan for stocks with RSI(2) deeply oversold while price remains above 200-day SMA. + + Contrarian mean-reversion signal — the only non-momentum scanner in the pipeline. + Identifies short-term panic selloffs within broader uptrends where a 3–7 day bounce + is statistically expected. + + Data requirement: ~210 trading days of OHLCV (200d SMA + RSI buffer). + Cost: single batch yfinance download via shared OHLCV cache, zero per-ticker API calls. + """ + + name = "rsi_oversold" + pipeline = "momentum" + strategy = "mean_reversion_bounce" + + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.max_rsi = self.scanner_config.get("max_rsi", 10.0) + self.sma_period = self.scanner_config.get("sma_period", 200) + self.rsi_period = self.scanner_config.get("rsi_period", 2) + self.min_price = self.scanner_config.get("min_price", 5.0) + self.min_avg_volume = self.scanner_config.get("min_avg_volume", 100_000) + self.vol_avg_days = self.scanner_config.get("vol_avg_days", 20) + self.max_tickers = self.scanner_config.get("max_tickers", 0) # 0 = no cap + + def scan(self, state: Dict[str, Any]) -> List[Dict[str, Any]]: + if not self.is_enabled(): + return [] + + logger.info(f"📉 Scanning RSI({self.rsi_period}) oversold bounce (RSI < {self.max_rsi})...") + + tickers = load_universe(self.config) + if not tickers: + logger.warning("No tickers loaded for RSI oversold scan") + return [] + + if self.max_tickers: + tickers = tickers[: self.max_tickers] + + cache_dir = self.config.get("discovery", {}).get("ohlcv_cache_dir", "data/ohlcv_cache") + logger.info(f"Loading OHLCV for {len(tickers)} tickers from cache...") + data = download_ohlcv_cached(tickers, period="1y", cache_dir=cache_dir) + + if not data: + return [] + + candidates = [] + for ticker, df in data.items(): + result = self._check_rsi_oversold(df) + if result: + result["ticker"] = ticker + candidates.append(result) + + # Sort by RSI ascending — most oversold (lowest RSI) gets highest rank + candidates.sort(key=lambda c: c.get("_rsi_value", 99)) + # Strip internal sort key + for c in candidates: + c.pop("_rsi_value", None) + + candidates = candidates[: self.limit] + logger.info(f"RSI oversold bounce: {len(candidates)} candidates") + return candidates + + def _check_rsi_oversold(self, df: pd.DataFrame) -> Optional[Dict[str, Any]]: + """Compute RSI(2) and 200-day SMA; return candidate dict if signal fires.""" + try: + df = df.dropna(subset=["Close", "Volume"]) + + # Need at least sma_period + rsi_period + a buffer + min_rows = self.sma_period + self.rsi_period + 5 + if len(df) < min_rows: + return None + + close = df["Close"] + volume = df["Volume"] + + current_close = float(close.iloc[-1]) + + # --- Liquidity gates --- + avg_vol = float(volume.iloc[-(self.vol_avg_days + 1) : -1].mean()) + if avg_vol < self.min_avg_volume: + return None + if current_close < self.min_price: + return None + + # --- 200-day SMA trend filter --- + sma200 = float(close.iloc[-self.sma_period :].mean()) + if current_close <= sma200: + return None # Price below 200d SMA → falling knife risk, skip + + # --- RSI(2) calculation --- + # Use the last rsi_period+1 closes to compute one RSI value + rsi_window = close.iloc[-(self.rsi_period + 2) :].values + if len(rsi_window) < self.rsi_period + 1: + return None + + deltas = [rsi_window[i] - rsi_window[i - 1] for i in range(1, len(rsi_window))] + gains = [max(d, 0.0) for d in deltas] + losses = [abs(min(d, 0.0)) for d in deltas] + + avg_gain = sum(gains[-self.rsi_period :]) / self.rsi_period + avg_loss = sum(losses[-self.rsi_period :]) / self.rsi_period + + if avg_loss == 0: + rsi = 100.0 # No losses → not oversold + else: + rs = avg_gain / avg_loss + rsi = 100.0 - (100.0 / (1.0 + rs)) + + if rsi >= self.max_rsi: + return None + + # --- Priority --- + if rsi < 5.0: + priority = Priority.CRITICAL.value + elif rsi < 8.0: + priority = Priority.HIGH.value + else: + priority = Priority.MEDIUM.value + + pct_above_sma = ((current_close - sma200) / sma200) * 100 + + context = ( + f"RSI({self.rsi_period}) oversold at {rsi:.1f} | " + f"Price ${current_close:.2f} above 200d SMA ${sma200:.2f} " + f"(+{pct_above_sma:.1f}%) | " + f"3-7d mean-reversion bounce setup" + ) + + return { + "source": self.name, + "context": context, + "priority": priority, + "strategy": self.strategy, + "_rsi_value": round(rsi, 2), + } + + except Exception as e: + logger.debug(f"RSI oversold check failed: {e}") + return None + + +SCANNER_REGISTRY.register(RSIOversoldScanner) diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index a5510978..a75b434b 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -271,6 +271,18 @@ DEFAULT_CONFIG = { "min_price": 5.0, # Filter penny stocks "min_avg_volume": 100_000, # Min avg daily volume for liquidity }, + "rsi_oversold": { + "enabled": True, + "pipeline": "momentum", + "limit": 10, + "max_tickers": 0, # 0 = no cap (nightly cache makes full universe fast) + "rsi_period": 2, # Connors RSI(2): most sensitive to short-term pullbacks + "max_rsi": 10.0, # Entry threshold: RSI(2) < 10 (aggressive: < 5) + "sma_period": 200, # Trend filter SMA period; critical falling-knife guard + "min_price": 5.0, # Filter penny stocks + "min_avg_volume": 100_000, # Min avg daily volume for liquidity + "vol_avg_days": 20, # Days for volume average baseline + }, }, }, # Memory settings