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:
parent
bcbecdeb1e
commit
5c56f8dc26
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
| Title | File | Date | Summary |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
@ -14,6 +14,7 @@ from . import (
|
||||||
options_flow, # noqa: F401
|
options_flow, # noqa: F401
|
||||||
reddit_dd, # noqa: F401
|
reddit_dd, # noqa: F401
|
||||||
reddit_trending, # noqa: F401
|
reddit_trending, # noqa: F401
|
||||||
|
rsi_oversold, # noqa: F401
|
||||||
sector_rotation, # noqa: F401
|
sector_rotation, # noqa: F401
|
||||||
semantic_news, # noqa: F401
|
semantic_news, # noqa: F401
|
||||||
short_squeeze, # noqa: F401
|
short_squeeze, # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -271,6 +271,18 @@ DEFAULT_CONFIG = {
|
||||||
"min_price": 5.0, # Filter penny stocks
|
"min_price": 5.0, # Filter penny stocks
|
||||||
"min_avg_volume": 100_000, # Min avg daily volume for liquidity
|
"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
|
# Memory settings
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue