research(autonomous): 2026-04-15 — automated research run (#19)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Aitous 2026-04-14 22:49:43 -07:00 committed by GitHub
parent bcbecdeb1e
commit 5c56f8dc26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 274 additions and 0 deletions

View File

@ -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 |

View File

@ -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.861.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 7579% win rate over
25-year backtest (20002025); 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, 7579% 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: 37 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}%) | 37d mean-reversion bounce setup"`

View File

@ -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

View File

@ -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: 37 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 37 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)

View File

@ -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