A two-phase Python scanner for Nifty 500 stocks using the Inside Breakout SRAEGY strategy based on CPR (Central Pivot Range) and Camarilla pivot points.
# Phase 1 — After market close, build today's watchlist
py cpr_scanner.py
# Phase 2 — After market open, monitor for breakouts (Ctrl+C to stop)
py cpr_breakout.pyOr with options:
py cpr_scanner.py --date 2026-08-17 --output data/watchlist.json
py cpr_breakout.py --watchlist data/watchlist.json --interval 5- Grabs the prior 3 trading days of daily OHLC for all 500 Nifty constituents via
yfinance - Computes CPR (Pivot/TC/BC) and Camarilla (H1–H4, L1–L4) levels
- Filters stocks using 6 conditions (see below) to produce a tight watchlist
- Saves filtered watchlist to
data/watchlist.json
- Polls
NSELive.stock_quote(viajugaad_data.nse.NSELive) every 5 seconds for live prices - Builds 5-minute candles from tick volume
- Scans watchlist for signal/breakout candles (conditions 4–6)
- Alerts on valid entry with stop-loss and 1:1 / 1:2 targets (70% / 30% split)
All six conditions below must pass for a stock to appear on the watchlist. Yesterday's OHLC is the "current" period; the day before is "prior". Weekly levels come from the prior Monday–Friday window.
Goal: Find stocks with unusually tight consolidation.
Math: width = abs(TC − BC), where BC = (H + L) / 2 and TC = 2×PP − BC. CPR% = width / close × 100.
Rule: CPR% ≤ 0.15% (from config.CPR_WIDTH_THRESHOLD = 0.0015).
Interpretation: A narrow CPR means the daily pivot range is compressed — buyers and sellers are roughly balanced at the same prices. This is the hallmark of a coiled spring: volatility is low and a breakout is imminent. Wide CPR (> 0.15%) means the stock is trending or choppy — not a setup.
Goal: Confirm the consolidation is tightening, not static.
Rule: cur_width < pri_width.
Interpretation: If yesterday's CPR was narrow but today's is wider, the range is expanding (momentum building upward, but the entry is too late). Only stocks where the CPR is narrowing day-over-day are selected, signaling continued compression.
Goal: Ensure the upper Camarilla resistance zone is contracting (moving down).
Camarilla H3: Close + 0.275 × (H − L).
Rule: cur_h3 < pri_h3.
Interpretation: H3 is the "breakout pivot" level. When today's H3 is below yesterday's H3, it means the breakout threshold is descending — price is coiling tighter and the resistance ceiling is lowering. This creates a tighter "ceiling" the price must punch through.
Goal: Ensure the lower Camarilla support zone is contracting upward.
Camarilla L3: Close − 0.275 × (H − L).
Rule: cur_l3 > pri_l3.
Interpretation: When today's L3 is above yesterday's L3, the "floor" is rising. Combined with a falling H3, this confirms the Camarilla range (H3 to L3) is contracting from both ends — a textbook inside-day compression.
Goal: Ensure the current day's Camarilla H4 (immediate ceiling) sits between or above yesterday's CPR range.
Camarilla H4: Close + 0.55 × (H − L).
Rule: cur_h4 >= pri_bc (i.e., not below the bottom of the CPR band).
Interpretation: If H4 falls below yesterday's CPR BC, the stock is in a downtrending range (lower highs relative to yesterday's pivot). Requiring H4 ≥ BC means the current day's immediate ceiling sits at or above yesterday's CPR midpoint — the stock is consolidating, not weakening. H4 can sit anywhere from BC up — inside the CPR band (between BC and TC) or above TC (strong bullish).
Goal: Prevent selecting stocks where the weekly trend is decisively bullish, which could mean the "tight daily range" is just a rest within a larger up-move (less reliable breakout).
Rule: If weekly_TC > current_TC and weekly_BC > current_BC → skip the stock.
Interpretation: When the weekly CPR band sits entirely above the daily CPR band, the stock is in a strong weekly uptrend. Daily compressions in a strong trend tend to resolve upward (good for breakout momentum, but not "Inside Breakout" setups which prefer range-bound consolidations). Only stocks where the weekly CPR intersects or overlaps the daily CPR are kept.
Phase 2 — check_breakout(signal_candle, breakout_candle, h4, prev_cpr_high, resting_low=None) → dict
Conditions 4–6 are evaluated in real time during market hours. A "signal candle" is the first candle that closes above Camarilla H4; the "breakout candle" is the next candle that must confirm the breakout.
Goal: Confirm the compression has resolved upward.
Rule: signal_candle.close > h4.
Interpretation: The CPR/Camarilla narrowing (Phase 1) means the stock is coiled. A candle closing above H4 means price has broken out of the tight range. The closing price is used (not the high) to ensure sustained momentum, not just an intrabar spike.
Goal: Confirm the breakout has follow-through and cleared key resistance.
Rule: breakout_candle.close > max(signal_candle.high, previous_day's_CPR_TC).
Interpretation: prev_cpr.tc (yesterday's TC) is the prior day's upper pivot boundary. If the breakout candle closes above both the signal candle's high (no pullback below entry) and yesterday's TC (confirmed bullish), the breakout is genuine. If it fails to clear either, it is a false breakout (a "fakey").
Goal: Confirm retail/option activity isn't driving a dead-cat bounce.
Rule: breakout_candle.volume > signal_candle.volume.
Interpretation: The signal candle broke out on real volume. The breakout candle must show even higher volume — if volume dries up on confirmation, the move lacks institutional backing and is likely a pullback. Higher volume on the breakout candle confirms conviction.
| Variable | Calculation | Description |
|---|---|---|
entry |
breakout_candle.high + ENTRY_BUFFER (0.5) |
Buy a few ticks above the breakout candle's high |
stop |
max(signal_candle.low, resting_low) |
Below the signal candle's low, or the lowest point touched between signal and breakout |
risk |
entry − stop |
Per-share risk |
target1 |
entry + risk |
1:1 reward-to-risk |
target2 |
entry + 2 × risk |
1:2 reward-to-risk |
Position sizing: 70% exited at target1, 30% at target2 (see exit_target1_pct in the returned dict).
calculate_cpr(high, low, close) -> dict
Calculates Central Pivot Range (standard industry formula):
- PP = (H + L + C) / 3
- BC = (H + L) / 2 (Bottom Central)
- TC = 2 × PP − BC (Top Central)
- width =
abs(TC − BC)=(2C − H − L) / 3(absolute value ensures positive width) - cpr_pct =
(width / close) × 100
Returns: {"pp", "tc", "bc", "width", "cpr_pct"}
calculate_camarilla(high, low, close) -> dict
Calculates 8 Camarilla pivot levels using the range (H − L):
- H4 = C + 0.55 × rng, H3 = C + 0.275 × rng, H2 = C + 0.183 × rng, H1 = C + 0.0916 × rng
- L1 = C − 0.0916 × rng, L2 = C − 0.183 × rng, L3 = C − 0.275 × rng, L4 = C − 0.55 × rng
Returns: {"h1", "h2", "h3", "h4", "l1", "l2", "l3", "l4"}
get_nifty500_symbols() -> list[str]
Downloads the Nifty 500 constituent list from niftyindices.com (with offline CSV fallback at docs/ind_nifty500list.csv). Results are cached in the module-level _nifty500_symbols variable for the lifetime of the process.
fetch_daily_data(symbol, from_date, to_date) -> pd.DataFrame | None
Fetches daily OHLC data via yfinance (Ticker(f"{symbol}.NS").history(start, end)). Adds 1 day to to_date because yfinance's end date is exclusive. If the latest row has NaN OHLC (publication lag), patches via ticker.info (currentPrice, dayHigh, dayLow). Drops remaining NaN rows requiring at least 3 valid trading days.
fetch_weekly_data(symbol, today) -> dict | None
Fetches the prior week's Mon–Fri OHLC. Walks back to the last completed Friday. Returns {"high": max(high), "low": min(low), "close": last_close} or None if insufficient data.
should_include_in_watchlist(current_hlc, prior_hlc, weekly_hlc) -> bool
Applies the 6 pre-market filter conditions. Returns True only if all pass.
scan_premarket(today, output_path) -> list[dict]
Iterates all Nifty 500 symbols → fetches daily + weekly data → applies should_include_in_watchlist. Appends passing stocks as {"symbol", "ohlc", "cpr", "weekly_cpr", "camarilla"} dicts. Writes results to data/watchlist.json.
CandleBuilder(bucket_minutes=5)
Aggregates live tick data into fixed-interval candles. update(price, volume, ts) returns a completed candle dict {"bucket", "open", "high", "low", "close", "volume", "open_time"} or None mid-bucket.
check_breakout(signal_candle, breakout_candle, h4, prev_cpr_high, resting_low=None) -> dict
Validates the 3 breakout conditions (4–6). On success returns {"valid": True, "entry", "stop", "risk", "target1", "target2", "exit_target1_pct"}. On failure returns {"valid": False, "reason": slug}.
get_live_tick(symbol, nse=None) -> dict | None
Fetches the latest live quote via NSELive.stock_quote. Returns {"last_price", "volume", "ts"} or None on error.
scan_breakout(watchlist_path, poll_interval, max_iterations) -> None
Main Phase 2 loop: loads watchlist → polls live ticks → builds candles → detects signal candles (close > H4) → validates breakout candles (close > max(signal.high, CPR TC) + volume surge) → prints entry/stop/target alerts.
Phase 1 CLI entrypoint. Args: --date (YYYY-MM-DD, default today), --output (default data/watchlist.json).
Phase 2 CLI entrypoint. Args: --watchlist (default data/watchlist.json), --interval (seconds, default 5). Handles KeyboardInterrupt for clean exit.
| Constant | Value | Description |
|---|---|---|
NIFTY500_CSV_URL |
https://www.niftyindices.com/IndexConstituent/ind_nifty500list.csv |
Network source for constituents |
NIFTY500_LOCAL_CSV |
docs/ind_nifty500list.csv |
Offline fallback CSV path |
CPR_WIDTH_THRESHOLD |
0.0015 |
0.15% of close — max CPR width to pass condition 1A |
ENTRY_BUFFER |
0.5 |
Price units above breakout candle high for entry placement |
POLL_INTERVAL_SECONDS |
5 |
Seconds between live tick polls |
pip install yfinance jugaad-data pandas pytest
pip install pyright
PYTHONPATH=. python -m pytest tests/- The CPR width threshold of 0.15% (
CPR_WIDTH_THRESHOLD = 0.0015) is narrow. Typical daily CPR widths with the standard formula range 0.03–1.5%. Increase this value if you want a wider watchlist. - The Nifty 500 list is downloaded from
niftyindices.comwith a fallback todocs/ind_nifty500list.csv. - Historical data comes from
yfinance; live market data comes fromjugaad_data.nse.NSELive.