A systematic implementation of William O'Neil's CANSLIM methodology, adapted for a market where ~30% of the index is a handful of correlated AI names. It screens the US universe on growth fundamentals, ranks leaders by relative strength, detects cup-with-handle breakouts on volume, and trades them through Alpaca with volatility-aware sizing and server-side stops.
It's the mirror image of my other system, alpha-scanner-core: that one buys weakness (mean reversion), this one buys strength (breakouts). They rarely fire on the same name at the same time, so they're complementary. When CANSLIM sits in cash waiting for a bull regime, the mean-reversion sibling is usually deploying it.
After building the mean-reversion scanner I wanted the opposite discipline: momentum, breakouts, buying strength instead of fading it. CANSLIM is the canonical framework for that, but it was reverse-engineered from winners of the 1950s to 90s. I was more interested in the parts that break when you point it at a modern, AI-concentrated market than in copying it out of the book. So I built it, then spent most of the effort on the honest question: does the machinery actually have an edge once you account for survivorship bias and look-ahead, or does it just look good?
| Letter | Rule | Where |
|---|---|---|
| C | Current quarterly EPS ≥ 25% Y/Y, accelerating | fundamentals.py, screener.py |
| A | Annual EPS 3yr CAGR ≥ 25%, ROE ≥ 17% | fundamentals.py, screener.py |
| N | New high, breakout from a base | signals.py |
| S | Supply/demand, breakout volume ≥ 1.4× 50-day avg | signals.py |
| L | Leader, RS rating ≥ 80 | rs_rating.py |
| I | Institutional sponsorship | ownership.py |
| M | Market direction, NASDAQ above 200-day MA + distribution-day count | regime.py, signals.py |
fundamentals.py WRDS/Compustat: EPS, revenue, ROE, sector (weekly)
signals.py C+A quality gate, RS leaders, breakout on volume, regime check
bot.py Alpaca execution, volatility-aware sizing, server-side stops (daily)
tracker.py exit detection + P&L
The C/A/L/I fundamental screen refreshes weekly (./run_daily.sh --weekly); the N/S/M breakout scan and execution run daily (./run_daily.sh).
Two live modes. --sp500 (the default) trades RS≥90 S&P 500 leaders with a +8% take-profit, the strongest config I found: backtest 2000 to 2026 shows ~27 trades/yr, 68% win rate, Sharpe 0.78, 7.8% CAGR, win rate steady across all 23 years. The no-flag mode is full CANSLIM: fundamental screen into RS≥80 breakouts, let winners run on a trailing stop, for fewer and larger wins. The single most useful thing the backtest taught me: win rate is an exit property, not an entry one. Taking a defined +8% instead of holding for +20% flips the win rate from ~30% to ~68%; it just trades big runners for many small wins.
The parts I actually cared about, the modifications that keep it from being a 1990s pattern-matcher in a 2020s market:
- Revenue-confirmation gate. EPS growth has to be backed by real top-line growth (rev ≥ 15%), which screens out buyback/margin/one-time EPS spikes that mimic a "C" winner with no durable demand.
- Anti-spike scoring. A lone monster quarter (easy comp, tiny base, pull-forward) is capped, so steady +40/50/60% growth outranks a single +600%.
- Theme-concentration cap. The shortlist limits names per GICS sector, so the book isn't secretly one AI-beta bet wearing seven tickers.
- Extension flag. Anything already >90% above its 200-day MA is flagged and never bought. CANSLIM buys breakouts from proper bases; it doesn't chase vertical names into air pockets.
- Volatility-aware sizing. Each trade risks a fixed fraction of equity; share count comes from the stop distance, so a jumpy high-ATR AI name gets fewer shares than a calm one. Equal dollar risk regardless of how violent the stock is.
- Earnings blackout. A breakout within 5 days of the next earnings print is downgraded from BUY to WATCH. Buying a base breakout into a binary earnings gap is a coin flip, not a CANSLIM trade.
Kept from O'Neil: no valuation filter on entry, since expensive winners run further; valuation belongs in sizing and sell rules, not as an entry gate. Deliberately dropped: the low-float half of S, since this universe is large/mid-cap where low-float names barely appear and are too thin to size and stop safely.
CANSLIM is a bull-market strategy and fails badly in bear or choppy markets. O'Neil was explicit: if M isn't met, you don't play. The M gate in regime.py enforces it: NASDAQ above its 200-day MA and not under institutional distribution (≥5 heavy-volume down days in 25 sessions means stand down). The walk-forward backtest confirms the gate pulls the book to cash in bear years like 2008 and 2022. This is non-negotiable before going live.
With WRDS online the backtest runs full CANSLIM with the C/A gates applied point-in-time, anchored on each quarter's rdq announcement date (pit_fundamentals.py) so it never uses earnings before they were public. --no-fundamentals isolates the technical layer.
python3 fundamentals.py # point-in-time C/A history (Compustat)
python3 data.py --backfill # deep OHLCV history
python3 backtest.py # full-CANSLIM backtest (--no-fundamentals for tech-only)
python3 validate.py # walk-forward across regime years
python3 breakout_analysis.py # follow-through stats + walk-forward quality modelWhat it shows: the C/A fundamental screen is a strong quality filter (win rate +10pts, profit factor +60%, drawdown cut to a quarter vs technical-only). But full-quality CANSLIM is inherently low-frequency: ~5 to 7 trades/yr is the ceiling here, and pushing past it with looser RS only adds junk. Standalone, it still trails an index fund and the walk-forward verdict stays cautious. That's the point: it's a concentrated breakout sleeve whose idle stretches are by design, not a standalone index-beater.
Caveats that bound every number here: (1) survivorship bias, since the universe is today's surviving tickers, so absolute win rates are optimistic; trust the relative and out-of-sample comparisons, not the headline. (2) Universe size, since ~700 names is far smaller than O'Neil's whole-market scan, which is why full CANSLIM is trade-starved here. (3) Point-in-time fundamentals start in 2000, so the full-CANSLIM window effectively begins there.
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in Alpaca keys; WRDS username/password prompted at runtimepandas / numpy / scipy, vectorbt (indicators + sim), scikit-learn (breakout-quality model), yfinance (OHLCV top-up), alpaca-py (brokerage), psycopg2 (WRDS/Compustat over Postgres), python-dotenv.
Built to learn quant systems end to end, not to give financial advice. Backtested and paper-traded results do not predict live returns. Trade your own money at your own risk.