nse-vcp-screener
Screen Nifty 500 stocks for Mark Minervini's Volatility Contraction Pattern (VCP) — identifying Stage 2 uptrends with tightening price ranges and declining volume before potential breakouts. Use this skill when the user requests VCP screening, Minervini-style setups, Stage 2 brea
Install
npx skills add https://github.com/ajeeshworkspace/indian-trading-skills/tree/master/skills/nse-vcp-screener
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ajeeshworkspace-indian-trading-skills@llmmart
git clone https://github.com/ajeeshworkspace/indian-trading-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ajeeshworkspace/indian-trading-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
NSE VCP Screener
Overview
This skill screens Indian stocks (Nifty 50/200/500) for Mark Minervini's Volatility Contraction Pattern (VCP). The VCP identifies stocks in Stage 2 uptrends that are forming tightening bases with declining volume — the classic setup before a potential breakout.
The screening pipeline has 3 phases:
- Pre-filter: Quick quote-based filtering to eliminate obvious non-candidates
- Trend Template: Apply Minervini's 7-point Stage 2 criteria using 260-day histories
- VCP Detection & Scoring: Pattern analysis with 5-component composite scoring
Data Source
This screener uses yfinance with .NS suffix for NSE stocks and the niftystocks package for stock universe lists. No paid API keys required.
Execution
python3 scripts/screen_vcp.py --universe nifty500
Command-Line Arguments
| Argument | Default | Description |
|---|---|---|
--universe |
nifty50 |
Stock universe: nifty50, nifty200, nifty500, or custom |
--custom-tickers |
— | Comma-separated tickers for custom universe (e.g., RELIANCE,TCS,INFY) |
--min-contractions |
2 |
Minimum number of contractions (2-4) |
--t1-depth-min |
10 |
Minimum T1 contraction depth % |
--t1-depth-max |
40 |
Maximum T1 contraction depth % |
--contraction-ratio |
0.75 |
Each contraction must be ≤ this ratio of the previous |
--min-contraction-days |
5 |
Minimum days per contraction |
--lookback-days |
120 |
Days to look back for pattern detection |
--breakout-volume-ratio |
1.5 |
Minimum volume ratio for breakout confirmation |
--trend-min-score |
85 |
Minimum trend template score (0-100) |
--output-dir |
reports/ |
Output directory for results |
Workflow
Step 1: Execute the Screener
Run the Python script with desired parameters:
python3 skills/nse-vcp-screener/scripts/screen_vcp.py \
--universe nifty500 \
--output-dir reports/
Step 2: Review Results
Load and review the generated reports:
- JSON:
reports/vcp_screener_YYYY-MM-DD_HHMMSS.json(structured data) - Markdown:
reports/vcp_screener_YYYY-MM-DD_HHMMSS.md(human-readable report)
Step 3: Load References for Interpretation
Read: references/vcp_methodology.md
Read: references/scoring_system.md
Step 4: Present Top Candidates
For each top-scoring candidate, present:
- Composite Score (0-100)
- Contraction Structure (T1/T2/T3 depths and durations)
- Volume Pattern (dry-up ratio)
- Pivot Level (breakout price)
- Relative Strength vs Nifty 50
Step 5: Actionable Insights
For the top 5-10 candidates:
- Note proximity to pivot/breakout level
- Assess if volume is confirming or diverging
- Check for upcoming F&O expiry or result season impacts
- Identify F&O lot size (if stock is in F&O segment)
Scoring System
The composite score (0-100) weights 5 components:
| Component | Weight | What It Measures |
|---|---|---|
| Trend Template | 25% | Minervini's 7-point Stage 2 criteria |
| Contraction Quality | 25% | Tightening base structure |
| Volume Pattern | 20% | Volume dry-up ratio |
| Pivot Proximity | 15% | Distance from breakout level |
| Relative Strength | 15% | Performance vs Nifty 50 |
Indian Market Adaptations
- Universe: Nifty 50/200/500 instead of S&P 500
- Benchmark: Relative strength measured vs Nifty 50 (^NSEI) instead of S&P 500
- Volatility: T1 depth range widened to 10-40% (vs 8-35% for US) due to higher small-cap volatility
- Circuit Limits: Stocks hitting circuits may show false VCP patterns — flagged in results
- Liquidity Filter: Minimum average daily volume of ₹1 crore to filter illiquid stocks
- F&O Availability: Results indicate whether the stock is in the F&O segment
Resources
references/vcp_methodology.md
Mark Minervini's VCP theory, Stage 2 criteria, contraction rules, and entry methodology adapted for Indian markets.
references/scoring_system.md
Detailed breakdown of the 5-component composite scoring system with thresholds and examples.
Files (indian-trading-skills)
-
references
-
scoring_system.md 3.6 KB
# VCP Composite Scoring System ## Overview The composite score (0-100) combines five independent components, each measuring a different aspect of VCP quality. Higher scores indicate more textbook-like setups with better risk/reward characteristics. ## Component Weights | # | Component | Weight | What It Measures | |---|-----------|--------|-----------------| | 1 | Trend Template | 25% | Minervini's 7-point Stage 2 criteria | | 2 | Contraction Quality | 25% | Tightening base structure | | 3 | Volume Pattern | 20% | Volume dry-up signature | | 4 | Pivot Proximity | 15% | Distance from breakout level | | 5 | Relative Strength | 15% | Performance vs Nifty 50 | ``` Composite Score = (Trend × 0.25) + (Contraction × 0.25) + (Volume × 0.20) + (Pivot × 0.15) + (RS × 0.15) ``` ## Component 1: Trend Template Score (0-100) Based on Minervini's 7-point criteria. Each criterion = ~14.3 points. | Criteria Met | Score | |-------------|-------| | 7/7 | 100 | | 6/7 | 85 | | 5/7 | 71 | | < 5/7 | Disqualified | **Minimum threshold**: 85 (6/7 criteria). Stocks scoring below this are eliminated in Phase 2. ## Component 2: Contraction Quality Score (0-100) Evaluates the structure and cleanliness of price contractions. ### Base Score | Pattern | Base Score | |---------|-----------| | 4+ clean contractions | 80-90 | | 3 clean contractions | 65-80 | | 2 clean contractions | 50-65 | ### Modifiers | Condition | Modifier | |-----------|----------| | Consistent contraction ratio < 0.60 | +10 | | Final contraction depth < 5% | +10 | | T1 depth in ideal range (15-30%) | +5 | | Irregular/overlapping contractions | -15 to -25 | | T1 too shallow (< 10%) | -10 | | T1 too deep (> 40%) | -15 | ## Component 3: Volume Pattern Score (0-100) Based on the volume dry-up ratio (recent 10-day avg / 50-day avg). | Dry-Up Ratio | Score | |--------------|-------| | < 0.40 | 90 | | 0.40-0.50 | 80 | | 0.50-0.60 | 70 | | 0.60-0.70 | 60 | | 0.70-0.80 | 45 | | 0.80-0.90 | 30 | | > 0.90 | 15 | ## Component 4: Pivot Proximity Score (0-100) Measures how close the current price is to the breakout pivot level. | Distance from Pivot | Score | |--------------------|-------| | 0-3% below pivot | 90 | | 3-5% below pivot | 75 | | 5-8% below pivot | 60 | | 8-12% below pivot | 45 | | 12-20% below pivot | 30 | | > 20% below pivot | 15 | | Above pivot (already broken out) | 50 (late entry risk) | ## Component 5: Relative Strength Score (0-100) Measures stock's performance vs Nifty 50 using Minervini-weighted formula. ``` RS = 0.40 × (3-month return vs Nifty) + 0.20 × (6-month return vs Nifty) + 0.20 × (9-month return vs Nifty) + 0.20 × (12-month return vs Nifty) ``` | RS Value | Score | |----------|-------| | > 50% outperformance | 95 | | 30-50% | 80 | | 15-30% | 65 | | 5-15% | 50 | | 0-5% | 35 | | < 0% (underperforming) | 15 | ## Interpreting Composite Scores | Score Range | Quality | Action | |-------------|---------|--------| | 80-100 | Excellent | High conviction setup — prepare entry plan | | 65-79 | Good | Watchlist — monitor for pivot approach | | 50-64 | Fair | Early stage or flawed pattern — check individual components | | < 50 | Poor | Not a valid VCP — skip | ## Example Scoring **Stock: PERSISTENT (NSE)** | Component | Raw Score | Weight | Weighted | |-----------|----------|--------|----------| | Trend Template (7/7) | 100 | 25% | 25.0 | | Contraction Quality (3 contractions, ratio 0.55) | 80 | 25% | 20.0 | | Volume Dry-Up (ratio 0.48) | 80 | 20% | 16.0 | | Pivot Proximity (2.5% below) | 90 | 15% | 13.5 | | Relative Strength (RS = 32%) | 80 | 15% | 12.0 | | **Composite** | | | **86.5** | Verdict: Excellent setup — prepare entry plan with stop below final contraction low. -
vcp_methodology.md 5.1 KB
# VCP Methodology — Indian Markets Edition ## Mark Minervini's Volatility Contraction Pattern The VCP (Volatility Contraction Pattern) is a consolidation pattern identified by Mark Minervini that precedes powerful breakouts. The pattern shows a series of price contractions, each tighter than the last, with declining volume — indicating sellers being absorbed and supply drying up. ## The 4 Stages of Stock Movement 1. **Stage 1 — Neglect (Basing)**: Stock moves sideways after a decline. Low volume, no institutional interest. Most investors ignore it. 2. **Stage 2 — Advancing (Markup)**: Stock begins a sustained uptrend. Increasing volume, positive momentum. **This is where VCPs form within the uptrend.** 3. **Stage 3 — Topping (Distribution)**: Uptrend loses momentum. Wide and loose price swings, high volatility. Institutions distribute. 4. **Stage 4 — Declining (Markdown)**: Stock enters a sustained downtrend. Avoid at all costs. **Only trade VCP setups in Stage 2 stocks.** ## Minervini's 7-Point Trend Template (Stage 2 Filter) A stock must pass at least 6 of 7 criteria to qualify as Stage 2: | # | Criterion | Description | |---|-----------|-------------| | 1 | Price > 150-day MA | Above medium-term trend | | 2 | Price > 200-day MA | Above long-term trend | | 3 | 150-day MA > 200-day MA | Medium-term leading long-term | | 4 | 200-day MA trending up for ≥1 month | Long-term trend is positive | | 5 | 50-day MA > 150-day MA | Short-term leading medium-term | | 6 | 50-day MA > 200-day MA | Short-term above long-term | | 7 | Price > 50-day MA | Above short-term trend | Each criterion scores ~14.3 points (total = 100). Minimum qualifying score: 85 (6/7 criteria). ## VCP Contraction Structure ### Contraction Rules A valid VCP requires: 1. **Minimum 2 contractions** (T1, T2; optionally T3, T4) 2. **T1 depth**: 10-40% for Indian stocks (higher volatility allows wider first contraction) 3. **Each subsequent contraction ≤ 75% of the previous** (e.g., T1=25%, T2≤18.75%, T3≤14%) 4. **Minimum contraction duration**: 5 trading days each 5. **Pattern lookback**: 120 trading days (approximately 6 months) ### Contraction Quality Scoring | Factor | Score Impact | |--------|-------------| | 2 clean contractions | Base score 50-60 | | 3 clean contractions | Base score 70-80 | | 4+ clean contractions | Base score 80-90 | | Ratio consistently < 0.6 | +10 bonus | | Final contraction < 5% | +10 bonus | | Irregular/overlapping | -15 to -25 penalty | ## Pivot Point & Entry The **pivot** (buy point) is the highest price point within the most recent contraction. This is where the stock breaks out of the VCP pattern. ### Entry Rules - **Buy at or slightly above the pivot** (within 1-2% above pivot) - **Volume on breakout day should be ≥ 1.5x the 50-day average** (adjusted from original for Indian market liquidity) - **Do not chase**: If stock gaps up >5% above pivot, wait for a pullback ### Indian Market Considerations for Entry - **Pre-open session**: NSE pre-open auction (9:00-9:08 AM) can cause gap opens. Set limit orders during pre-open if possible. - **Circuit limits**: If a stock hits upper circuit on breakout day, it may not allow entry. Use next-day opening or GTT orders. - **F&O segment**: If the stock is in F&O, consider using options (calls near pivot strike) for defined risk entry. ## Volume Signature ### Volume Dry-Up The volume pattern is critical: - **Early contractions**: Normal to above-average volume (selling pressure) - **Later contractions**: Declining volume (sellers exhausted) - **Near pivot**: Volume should be at its lowest — "dry-up" - **On breakout**: Volume surge (≥1.5x average, ideally 2x+) ### Dry-Up Ratio ``` Dry-Up Ratio = Recent 10-day Avg Volume / 50-day Avg Volume ``` | Dry-Up Ratio | Interpretation | Score | |--------------|---------------|-------| | < 0.50 | Excellent dry-up | 80-90 | | 0.50-0.70 | Good dry-up | 60-80 | | 0.70-0.90 | Moderate dry-up | 40-60 | | > 0.90 | Insufficient dry-up | 20-40 | ## Relative Strength Relative strength compares a stock's performance to the benchmark (Nifty 50 for Indian stocks). ### Minervini-Weighted RS Formula ``` RS = 0.40 × (3-month return) + 0.20 × (6-month return) + 0.20 × (9-month return) + 0.20 × (12-month return) ``` Weight the most recent performance heaviest. A stock with RS > 0 is outperforming Nifty 50. ### RS Scoring | RS Value | Score | |----------|-------| | > 50% | 90-100 | | 25-50% | 70-90 | | 10-25% | 50-70 | | 0-10% | 30-50 | | < 0% | 0-30 | ## Common VCP Failures in Indian Markets 1. **Circuit-driven patterns**: Stocks hitting repeated upper circuits can mimic VCP contractions but lack genuine base-building. These are often operator-driven and should be avoided. 2. **Low liquidity traps**: Stocks with < ₹1 crore daily turnover may show VCP-like patterns due to thin trading, not genuine supply/demand dynamics. 3. **Sector rotation breaks**: A stock may form a perfect VCP but fail if the entire sector rotates out of favour (e.g., IT during INR appreciation). 4. **Result season volatility**: Earnings announcements can invalidate VCP patterns. Check result dates before trading a setup.
-
-
scripts
-
calculators
-
pivot_proximity_calculator.py 1.3 KB
""" Pivot proximity calculator. Scores how close the current price is to the VCP breakout pivot level. """ def calculate_pivot_proximity(current_price: float, pivot: float) -> dict: """ Calculate distance from pivot and score. Args: current_price: Current stock price pivot: Breakout pivot level Returns: dict with distance_pct, score, and position """ if pivot <= 0 or current_price <= 0: return { "distance_pct": 0.0, "score": 0.0, "position": "invalid", } distance_pct = (pivot - current_price) / pivot * 100 if distance_pct < 0: # Already above pivot (broken out) score = 50.0 position = "above_pivot" elif distance_pct <= 3: score = 90.0 position = "near_pivot" elif distance_pct <= 5: score = 75.0 position = "approaching_pivot" elif distance_pct <= 8: score = 60.0 position = "moderate_distance" elif distance_pct <= 12: score = 45.0 position = "far_from_pivot" elif distance_pct <= 20: score = 30.0 position = "very_far" else: score = 15.0 position = "too_far" return { "distance_pct": round(abs(distance_pct), 2), "score": score, "position": position, } -
relative_strength_calculator.py 2.6 KB
""" Relative Strength calculator. Measures stock performance vs Nifty 50 using Minervini-weighted formula. """ import pandas as pd import yfinance as yf def calculate_relative_strength( stock_df: pd.DataFrame, benchmark_df: pd.DataFrame | None = None, ) -> dict: """ Calculate Minervini-weighted relative strength vs Nifty 50. RS = 0.40 × (3-month return) + 0.20 × (6-month return) + 0.20 × (9-month return) + 0.20 × (12-month return) Args: stock_df: DataFrame with 'Close' column, at least 252 rows preferred. benchmark_df: DataFrame with 'Close' for Nifty 50. If None, fetches ^NSEI data. Returns: dict with rs_value, score, and period returns. """ close = stock_df["Close"] current = float(close.iloc[-1]) periods = { "3m": min(63, len(close) - 1), "6m": min(126, len(close) - 1), "9m": min(189, len(close) - 1), "12m": min(252, len(close) - 1), } stock_returns = {} for label, days in periods.items(): if days > 0: past = float(close.iloc[-days - 1]) stock_returns[label] = (current - past) / past * 100 if past > 0 else 0.0 else: stock_returns[label] = 0.0 # Benchmark returns bench_returns = {"3m": 0.0, "6m": 0.0, "9m": 0.0, "12m": 0.0} if benchmark_df is not None and len(benchmark_df) > 0: bench_close = benchmark_df["Close"] bench_current = float(bench_close.iloc[-1]) for label, days in periods.items(): days = min(days, len(bench_close) - 1) if days > 0: past = float(bench_close.iloc[-days - 1]) bench_returns[label] = (bench_current - past) / past * 100 if past > 0 else 0.0 # Excess returns excess = {k: stock_returns[k] - bench_returns[k] for k in stock_returns} # Minervini weighted RS rs_value = ( 0.40 * excess["3m"] + 0.20 * excess["6m"] + 0.20 * excess["9m"] + 0.20 * excess["12m"] ) score = _score_rs(rs_value) return { "rs_value": round(rs_value, 2), "score": round(score, 1), "stock_returns": {k: round(v, 2) for k, v in stock_returns.items()}, "benchmark_returns": {k: round(v, 2) for k, v in bench_returns.items()}, "excess_returns": {k: round(v, 2) for k, v in excess.items()}, } def _score_rs(rs: float) -> float: """Score relative strength on 0-100 scale.""" if rs > 50: return 95 elif rs > 30: return 80 elif rs > 15: return 65 elif rs > 5: return 50 elif rs > 0: return 35 else: return 15 -
trend_template_calculator.py 2.5 KB
""" Minervini's 7-Point Trend Template Calculator for Stage 2 identification. Adapted for Indian markets (NSE/BSE). """ import pandas as pd def calculate_trend_template(df: pd.DataFrame) -> dict: """ Apply Minervini's 7-point Stage 2 trend template. Args: df: DataFrame with columns: Close (at minimum), indexed by date. Must have at least 200 rows of daily data. Returns: dict with keys: - criteria: list of 7 booleans (True = criterion met) - score: float 0-100 - stage: int (1-4 estimated stage) - details: dict with MA values and comparisons """ if len(df) < 200: return { "criteria": [False] * 7, "score": 0.0, "stage": 0, "details": {"error": "Insufficient data (need 200+ days)"}, } close = df["Close"] current_price = float(close.iloc[-1]) ma_50 = float(close.rolling(50).mean().iloc[-1]) ma_150 = float(close.rolling(150).mean().iloc[-1]) ma_200 = float(close.rolling(200).mean().iloc[-1]) # Check if 200-day MA is trending up for at least 1 month (22 trading days) ma_200_series = close.rolling(200).mean() ma_200_month_ago = float(ma_200_series.iloc[-22]) if len(ma_200_series) >= 22 else ma_200 ma_200_trending_up = ma_200 > ma_200_month_ago criteria = [ current_price > ma_150, # 1. Price > 150-day MA current_price > ma_200, # 2. Price > 200-day MA ma_150 > ma_200, # 3. 150-day MA > 200-day MA ma_200_trending_up, # 4. 200-day MA trending up ≥1 month ma_50 > ma_150, # 5. 50-day MA > 150-day MA ma_50 > ma_200, # 6. 50-day MA > 200-day MA current_price > ma_50, # 7. Price > 50-day MA ] criteria_met = sum(criteria) score = criteria_met * (100 / 7) # Estimate stage if criteria_met >= 6: stage = 2 elif current_price < ma_200 and ma_50 < ma_200: stage = 4 elif current_price > ma_200 and ma_50 < ma_150: stage = 1 else: stage = 3 return { "criteria": criteria, "score": round(score, 1), "stage": stage, "details": { "price": current_price, "ma_50": round(ma_50, 2), "ma_150": round(ma_150, 2), "ma_200": round(ma_200, 2), "ma_200_trending_up": ma_200_trending_up, "criteria_met": criteria_met, }, } -
vcp_pattern_calculator.py 5.6 KB
""" VCP (Volatility Contraction Pattern) detection calculator. Identifies tightening price contractions in a stock's recent price history. """ import pandas as pd def find_contractions( df: pd.DataFrame, lookback_days: int = 120, min_contraction_days: int = 5, ) -> list[dict]: """ Find price contractions (swings from high to low) in the lookback window. Returns list of contractions with high, low, depth_pct, and duration. """ if len(df) < lookback_days: lookback_days = len(df) recent = df.tail(lookback_days).copy() highs = recent["High"].values lows = recent["Low"].values contractions = [] window_sizes = _get_adaptive_windows(lookback_days) for window in window_sizes: if len(recent) < window: continue segment = recent.tail(window) seg_high = float(segment["High"].max()) seg_low = float(segment["Low"].min()) if seg_low <= 0: continue depth_pct = (seg_high - seg_low) / seg_high * 100 contractions.append({ "high": round(seg_high, 2), "low": round(seg_low, 2), "depth_pct": round(depth_pct, 2), "duration_days": window, }) # Sort by duration descending (T1 = longest, T2 = next, etc.) contractions.sort(key=lambda x: x["duration_days"], reverse=True) return contractions def _get_adaptive_windows(lookback: int) -> list[int]: """Generate contraction windows proportional to lookback period.""" if lookback >= 100: return [lookback, lookback // 2, lookback // 4, lookback // 8] elif lookback >= 60: return [lookback, lookback // 2, lookback // 4] else: return [lookback, lookback // 2] def calculate_vcp( df: pd.DataFrame, lookback_days: int = 120, min_contractions: int = 2, t1_depth_min: float = 10.0, t1_depth_max: float = 40.0, contraction_ratio: float = 0.75, min_contraction_days: int = 5, ) -> dict: """ Detect VCP pattern and calculate contraction quality score. Args: df: DataFrame with OHLCV data lookback_days: Number of days to look back min_contractions: Minimum contractions required t1_depth_min: Minimum T1 depth % t1_depth_max: Maximum T1 depth % contraction_ratio: Max ratio of each contraction to previous min_contraction_days: Minimum days per contraction Returns: dict with is_vcp, score, contractions, pivot, details """ contractions = find_contractions(df, lookback_days, min_contraction_days) if len(contractions) < min_contractions: return { "is_vcp": False, "score": 0.0, "contractions": contractions, "pivot": None, "details": {"reason": f"Only {len(contractions)} contractions found (need {min_contractions})"}, } # Check T1 depth t1 = contractions[0] if t1["depth_pct"] < t1_depth_min: return { "is_vcp": False, "score": 0.0, "contractions": contractions, "pivot": None, "details": {"reason": f"T1 depth {t1['depth_pct']}% < minimum {t1_depth_min}%"}, } if t1["depth_pct"] > t1_depth_max: return { "is_vcp": False, "score": 0.0, "contractions": contractions, "pivot": None, "details": {"reason": f"T1 depth {t1['depth_pct']}% > maximum {t1_depth_max}%"}, } # Check contraction tightening is_tightening = True for i in range(1, len(contractions)): ratio = contractions[i]["depth_pct"] / contractions[i - 1]["depth_pct"] if ratio > contraction_ratio: is_tightening = False break if not is_tightening: return { "is_vcp": False, "score": 0.0, "contractions": contractions, "pivot": None, "details": {"reason": "Contractions not consistently tightening"}, } # Calculate pivot (highest point of most recent contraction) pivot = contractions[-1]["high"] # Score the contraction quality score = _score_contraction_quality(contractions, t1_depth_min, t1_depth_max) return { "is_vcp": True, "score": round(score, 1), "contractions": contractions, "pivot": pivot, "details": { "num_contractions": len(contractions), "t1_depth": t1["depth_pct"], "final_depth": contractions[-1]["depth_pct"], }, } def _score_contraction_quality( contractions: list[dict], t1_depth_min: float, t1_depth_max: float, ) -> float: """Score contraction quality 0-100.""" n = len(contractions) # Base score by number of contractions if n >= 4: base = 85 elif n >= 3: base = 72 else: base = 57 score = base # Ratio bonus: average contraction ratio ratios = [] for i in range(1, n): ratios.append(contractions[i]["depth_pct"] / contractions[i - 1]["depth_pct"]) avg_ratio = sum(ratios) / len(ratios) if ratios else 1.0 if avg_ratio < 0.60: score += 10 elif avg_ratio < 0.70: score += 5 # Final contraction tightness bonus if contractions[-1]["depth_pct"] < 5.0: score += 10 elif contractions[-1]["depth_pct"] < 8.0: score += 5 # T1 ideal range bonus (15-30%) t1_depth = contractions[0]["depth_pct"] if 15.0 <= t1_depth <= 30.0: score += 5 # Penalties if t1_depth < t1_depth_min: score -= 10 if t1_depth > t1_depth_max: score -= 15 return max(0, min(100, score)) -
volume_pattern_calculator.py 1.6 KB
""" Volume dry-up pattern calculator for VCP screening. Measures whether volume is declining as the pattern tightens — a key VCP characteristic. """ import pandas as pd def calculate_volume_pattern(df: pd.DataFrame) -> dict: """ Calculate volume dry-up ratio and score. Args: df: DataFrame with 'Volume' column, at least 50 rows. Returns: dict with dry_up_ratio, score, and details. """ if len(df) < 50 or "Volume" not in df.columns: return { "dry_up_ratio": 1.0, "score": 0.0, "details": {"error": "Insufficient data or no volume column"}, } vol = df["Volume"] avg_50 = float(vol.tail(50).mean()) avg_10 = float(vol.tail(10).mean()) if avg_50 <= 0: return { "dry_up_ratio": 1.0, "score": 0.0, "details": {"error": "Zero or negative 50-day average volume"}, } dry_up_ratio = avg_10 / avg_50 score = _score_dry_up(dry_up_ratio) return { "dry_up_ratio": round(dry_up_ratio, 3), "score": round(score, 1), "details": { "avg_volume_50d": round(avg_50, 0), "avg_volume_10d": round(avg_10, 0), }, } def _score_dry_up(ratio: float) -> float: """Score volume dry-up ratio on 0-100 scale.""" if ratio < 0.40: return 90 elif ratio < 0.50: return 80 elif ratio < 0.60: return 70 elif ratio < 0.70: return 60 elif ratio < 0.80: return 45 elif ratio < 0.90: return 30 else: return 15 -
__init__.py 0 B
-
-
report_generator.py 4.4 KB
""" Report generator for VCP screener results. Produces both JSON and Markdown output files. """ import json from datetime import datetime from pathlib import Path def generate_reports(results: list[dict], output_dir: str = "reports") -> dict: """ Generate JSON and Markdown reports from screening results. Args: results: List of dicts, each containing screening data for one stock. output_dir: Directory to save reports. Returns: dict with json_path and md_path. """ output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") json_file = output_path / f"vcp_screener_{timestamp}.json" md_file = output_path / f"vcp_screener_{timestamp}.md" # Sort by composite score descending results.sort(key=lambda x: x.get("composite_score", 0), reverse=True) # JSON report report_data = { "generated_at": datetime.now().isoformat(), "total_screened": len(results), "results": results, } with open(json_file, "w") as f: json.dump(report_data, f, indent=2, default=str) # Markdown report md_content = _generate_markdown(results, timestamp) with open(md_file, "w") as f: f.write(md_content) return { "json_path": str(json_file), "md_path": str(md_file), } def _generate_markdown(results: list[dict], timestamp: str) -> str: """Generate a human-readable Markdown report.""" lines = [ "# NSE VCP Screener Results", "", f"**Generated**: {timestamp}", f"**Candidates Found**: {len(results)}", "", "---", "", ] if not results: lines.append("No VCP candidates found matching the criteria.") return "\n".join(lines) # Summary table lines.extend([ "## Top Candidates", "", "| Rank | Ticker | Price (₹) | Score | Quality | Pivot (₹) | Distance | Dry-Up | RS |", "|------|--------|-----------|-------|---------|-----------|----------|--------|----|", ]) for i, r in enumerate(results[:20], 1): ticker = r.get("ticker", "?") price = r.get("price", 0) score = r.get("composite_score", 0) quality = r.get("quality", "?") pivot = r.get("pivot", 0) distance = r.get("pivot_distance_pct", 0) dry_up = r.get("dry_up_ratio", 0) rs = r.get("rs_value", 0) lines.append( f"| {i} | **{ticker}** | ₹{price:,.2f} | {score:.1f} | {quality} " f"| ₹{pivot:,.2f} | {distance:.1f}% | {dry_up:.2f} | {rs:+.1f}% |" ) lines.append("") # Detailed analysis for top 10 lines.extend(["---", "", "## Detailed Analysis", ""]) for i, r in enumerate(results[:10], 1): ticker = r.get("ticker", "?") lines.extend([ f"### {i}. {ticker} — Score: {r.get('composite_score', 0):.1f} ({r.get('quality', '?')})", "", f"- **Price**: ₹{r.get('price', 0):,.2f}", f"- **Pivot**: ₹{r.get('pivot', 0):,.2f} ({r.get('pivot_position', '?')})", f"- **Stage**: {r.get('stage', '?')}", f"- **Trend Score**: {r.get('trend_score', 0):.1f}/100", f"- **Contraction Score**: {r.get('contraction_score', 0):.1f}/100", f"- **Volume Dry-Up**: {r.get('dry_up_ratio', 0):.2f} (Score: {r.get('volume_score', 0):.1f})", f"- **Relative Strength**: {r.get('rs_value', 0):+.1f}% vs Nifty", "", ]) contractions = r.get("contractions", []) if contractions: lines.append("**Contractions:**") for j, c in enumerate(contractions): label = f"T{j+1}" lines.append( f" - {label}: {c.get('depth_pct', 0):.1f}% depth over {c.get('duration_days', 0)} days" ) lines.append("") lines.append("---") lines.append("") # Disclaimer lines.extend([ "## Disclaimer", "", "This screening is based on technical pattern recognition and does not constitute investment advice. " "VCP patterns can fail. Always conduct your own due diligence, manage risk with stop-losses, " "and consider fundamental factors before trading. Past patterns do not guarantee future results.", "", "---", "*Generated by NSE VCP Screener — Indian Trading Skills*", ]) return "\n".join(lines) -
scorer.py 1.9 KB
""" VCP Composite Scorer. Combines 5 component scores into a single 0-100 composite score. """ WEIGHTS = { "trend_template": 0.25, "contraction_quality": 0.25, "volume_pattern": 0.20, "pivot_proximity": 0.15, "relative_strength": 0.15, } def calculate_composite_score( trend_score: float, contraction_score: float, volume_score: float, pivot_score: float, rs_score: float, ) -> dict: """ Calculate the weighted composite score. Args: trend_score: Trend template score (0-100) contraction_score: Contraction quality score (0-100) volume_score: Volume dry-up score (0-100) pivot_score: Pivot proximity score (0-100) rs_score: Relative strength score (0-100) Returns: dict with composite_score, components, and quality rating. """ composite = ( trend_score * WEIGHTS["trend_template"] + contraction_score * WEIGHTS["contraction_quality"] + volume_score * WEIGHTS["volume_pattern"] + pivot_score * WEIGHTS["pivot_proximity"] + rs_score * WEIGHTS["relative_strength"] ) composite = round(composite, 1) if composite >= 80: quality = "Excellent" elif composite >= 65: quality = "Good" elif composite >= 50: quality = "Fair" else: quality = "Poor" return { "composite_score": composite, "quality": quality, "components": { "trend_template": {"score": trend_score, "weight": WEIGHTS["trend_template"]}, "contraction_quality": {"score": contraction_score, "weight": WEIGHTS["contraction_quality"]}, "volume_pattern": {"score": volume_score, "weight": WEIGHTS["volume_pattern"]}, "pivot_proximity": {"score": pivot_score, "weight": WEIGHTS["pivot_proximity"]}, "relative_strength": {"score": rs_score, "weight": WEIGHTS["relative_strength"]}, }, } -
screen_vcp.py 8.6 KB
#!/usr/bin/env python3 """ NSE VCP Screener — Main Orchestrator Screens Indian stocks (Nifty 50/200/500) for Minervini's Volatility Contraction Pattern. Usage: python3 screen_vcp.py --universe nifty50 python3 screen_vcp.py --universe nifty500 --output-dir reports/ python3 screen_vcp.py --custom-tickers RELIANCE,TCS,INFY """ import argparse import sys from datetime import datetime from pathlib import Path import pandas as pd import yfinance as yf # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent)) from calculators.trend_template_calculator import calculate_trend_template from calculators.vcp_pattern_calculator import calculate_vcp from calculators.volume_pattern_calculator import calculate_volume_pattern from calculators.pivot_proximity_calculator import calculate_pivot_proximity from calculators.relative_strength_calculator import calculate_relative_strength from scorer import calculate_composite_score from report_generator import generate_reports def get_universe(universe: str, custom_tickers: str | None = None) -> list[str]: """Get stock universe tickers in yfinance format (.NS suffix).""" if universe == "custom" and custom_tickers: tickers = [t.strip().upper() for t in custom_tickers.split(",")] return [f"{t}.NS" for t in tickers if t] try: from niftystocks import ns if universe == "nifty50": return ns.get_nifty50_with_ns() elif universe == "nifty200": return ns.get_nifty200_with_ns() elif universe == "nifty500": return ns.get_nifty_total_market_with_ns() else: return ns.get_nifty50_with_ns() except ImportError: # Fallback: Nifty 50 hardcoded core components print("Warning: niftystocks package not available. Using hardcoded Nifty 50 list.", file=sys.stderr) nifty50_core = [ "RELIANCE", "TCS", "HDFCBANK", "INFY", "ICICIBANK", "BHARTIARTL", "ITC", "SBIN", "LT", "KOTAKBANK", "HINDUNILVR", "AXISBANK", "BAJFINANCE", "MARUTI", "TATAMOTORS", "SUNPHARMA", "TITAN", "HCLTECH", "NTPC", "POWERGRID", "ULTRACEMCO", "ADANIENT", "ASIANPAINT", "TATASTEEL", "WIPRO", "ONGC", "JSWSTEEL", "COALINDIA", "NESTLEIND", "BAJAJFINSV", "M&M", "TECHM", "DRREDDY", "CIPLA", "EICHERMOT", "APOLLOHOSP", "DIVISLAB", "BRITANNIA", "HEROMOTOCO", "INDUSINDBK", "TATACONSUM", "HDFCLIFE", "SBILIFE", "BAJAJ-AUTO", "GRASIM", "BPCL", "ADANIPORTS", "HINDALCO", "BEL", "TRENT", ] return [f"{t}.NS" for t in nifty50_core] def fetch_benchmark(period: str = "1y") -> pd.DataFrame: """Fetch Nifty 50 index data as benchmark.""" try: df = yf.download("^NSEI", period=period, interval="1d", progress=False) if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.get_level_values(0) return df except Exception as e: print(f"Warning: Could not fetch Nifty 50 benchmark: {e}", file=sys.stderr) return pd.DataFrame() def screen_stock( ticker: str, benchmark_df: pd.DataFrame, args: argparse.Namespace, ) -> dict | None: """Screen a single stock for VCP pattern. Returns result dict or None.""" try: df = yf.download(ticker, period="1y", interval="1d", progress=False) if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.get_level_values(0) if len(df) < 200: return None # Check minimum liquidity (₹1 crore daily avg turnover) if "Volume" in df.columns and "Close" in df.columns: avg_turnover = (df["Volume"].tail(20) * df["Close"].tail(20)).mean() if avg_turnover < 1_00_00_000: # ₹1 crore return None # Phase 1: Trend Template trend = calculate_trend_template(df) if trend["score"] < args.trend_min_score: return None # Phase 2: VCP Detection vcp = calculate_vcp( df, lookback_days=args.lookback_days, min_contractions=args.min_contractions, t1_depth_min=args.t1_depth_min, t1_depth_max=args.t1_depth_max, contraction_ratio=args.contraction_ratio, min_contraction_days=args.min_contraction_days, ) if not vcp["is_vcp"]: return None # Phase 3: Scoring volume = calculate_volume_pattern(df) current_price = float(df["Close"].iloc[-1]) pivot = vcp["pivot"] pivot_prox = calculate_pivot_proximity(current_price, pivot) rs = calculate_relative_strength(df, benchmark_df) composite = calculate_composite_score( trend_score=trend["score"], contraction_score=vcp["score"], volume_score=volume["score"], pivot_score=pivot_prox["score"], rs_score=rs["score"], ) clean_ticker = ticker.replace(".NS", "").replace(".BO", "") return { "ticker": clean_ticker, "price": round(current_price, 2), "composite_score": composite["composite_score"], "quality": composite["quality"], "stage": trend["stage"], "trend_score": trend["score"], "contraction_score": vcp["score"], "volume_score": volume["score"], "pivot_score": pivot_prox["score"], "rs_score": rs["score"], "pivot": round(pivot, 2), "pivot_distance_pct": pivot_prox["distance_pct"], "pivot_position": pivot_prox["position"], "dry_up_ratio": volume["dry_up_ratio"], "rs_value": rs["rs_value"], "contractions": vcp["contractions"], "trend_details": trend["details"], } except Exception as e: print(f" Error screening {ticker}: {e}", file=sys.stderr) return None def main(): parser = argparse.ArgumentParser(description="NSE VCP Screener") parser.add_argument("--universe", default="nifty50", choices=["nifty50", "nifty200", "nifty500", "custom"], help="Stock universe to screen") parser.add_argument("--custom-tickers", type=str, default=None, help="Comma-separated tickers for custom universe") parser.add_argument("--min-contractions", type=int, default=2, help="Minimum contractions (2-4)") parser.add_argument("--t1-depth-min", type=float, default=10.0, help="Minimum T1 depth %%") parser.add_argument("--t1-depth-max", type=float, default=40.0, help="Maximum T1 depth %%") parser.add_argument("--contraction-ratio", type=float, default=0.75, help="Max contraction ratio") parser.add_argument("--min-contraction-days", type=int, default=5, help="Min days per contraction") parser.add_argument("--lookback-days", type=int, default=120, help="Pattern lookback days") parser.add_argument("--breakout-volume-ratio", type=float, default=1.5, help="Min breakout volume ratio") parser.add_argument("--trend-min-score", type=float, default=85.0, help="Min trend template score") parser.add_argument("--output-dir", type=str, default="reports", help="Output directory") args = parser.parse_args() print(f"NSE VCP Screener — {datetime.now().strftime('%Y-%m-%d %H:%M')}") print(f"Universe: {args.universe}") # Get tickers tickers = get_universe(args.universe, args.custom_tickers) print(f"Stocks to screen: {len(tickers)}") # Fetch benchmark print("Fetching Nifty 50 benchmark...") benchmark_df = fetch_benchmark() # Screen all stocks results = [] total = len(tickers) for i, ticker in enumerate(tickers, 1): clean = ticker.replace(".NS", "") if i % 25 == 0 or i == total: print(f" Progress: {i}/{total} ({clean})") result = screen_stock(ticker, benchmark_df, args) if result: results.append(result) print(f" ✓ VCP found: {clean} (Score: {result['composite_score']:.1f})") print(f"\nScreening complete. {len(results)} VCP candidates found.") # Generate reports if results: paths = generate_reports(results, args.output_dir) print(f"Reports saved:") print(f" JSON: {paths['json_path']}") print(f" Markdown: {paths['md_path']}") else: print("No candidates found. Try relaxing parameters or expanding universe.") return results if __name__ == "__main__": main()
-
-
SKILL.md 4.4 KB
--- name: nse-vcp-screener description: Screen Nifty 500 stocks for Mark Minervini's Volatility Contraction Pattern (VCP) — identifying Stage 2 uptrends with tightening price ranges and declining volume before potential breakouts. Use this skill when the user requests VCP screening, Minervini-style setups, Stage 2 breakout candidates, or volatility contraction patterns on NSE/BSE stocks. --- # NSE VCP Screener ## Overview This skill screens Indian stocks (Nifty 50/200/500) for Mark Minervini's Volatility Contraction Pattern (VCP). The VCP identifies stocks in Stage 2 uptrends that are forming tightening bases with declining volume — the classic setup before a potential breakout. The screening pipeline has 3 phases: 1. **Pre-filter**: Quick quote-based filtering to eliminate obvious non-candidates 2. **Trend Template**: Apply Minervini's 7-point Stage 2 criteria using 260-day histories 3. **VCP Detection & Scoring**: Pattern analysis with 5-component composite scoring ## Data Source This screener uses **yfinance** with `.NS` suffix for NSE stocks and the **niftystocks** package for stock universe lists. No paid API keys required. ## Execution ```bash python3 scripts/screen_vcp.py --universe nifty500 ``` ### Command-Line Arguments | Argument | Default | Description | |----------|---------|-------------| | `--universe` | `nifty50` | Stock universe: `nifty50`, `nifty200`, `nifty500`, or `custom` | | `--custom-tickers` | — | Comma-separated tickers for custom universe (e.g., `RELIANCE,TCS,INFY`) | | `--min-contractions` | `2` | Minimum number of contractions (2-4) | | `--t1-depth-min` | `10` | Minimum T1 contraction depth % | | `--t1-depth-max` | `40` | Maximum T1 contraction depth % | | `--contraction-ratio` | `0.75` | Each contraction must be ≤ this ratio of the previous | | `--min-contraction-days` | `5` | Minimum days per contraction | | `--lookback-days` | `120` | Days to look back for pattern detection | | `--breakout-volume-ratio` | `1.5` | Minimum volume ratio for breakout confirmation | | `--trend-min-score` | `85` | Minimum trend template score (0-100) | | `--output-dir` | `reports/` | Output directory for results | ## Workflow ### Step 1: Execute the Screener Run the Python script with desired parameters: ```bash python3 skills/nse-vcp-screener/scripts/screen_vcp.py \ --universe nifty500 \ --output-dir reports/ ``` ### Step 2: Review Results Load and review the generated reports: - **JSON**: `reports/vcp_screener_YYYY-MM-DD_HHMMSS.json` (structured data) - **Markdown**: `reports/vcp_screener_YYYY-MM-DD_HHMMSS.md` (human-readable report) ### Step 3: Load References for Interpretation ``` Read: references/vcp_methodology.md Read: references/scoring_system.md ``` ### Step 4: Present Top Candidates For each top-scoring candidate, present: 1. **Composite Score** (0-100) 2. **Contraction Structure** (T1/T2/T3 depths and durations) 3. **Volume Pattern** (dry-up ratio) 4. **Pivot Level** (breakout price) 5. **Relative Strength** vs Nifty 50 ### Step 5: Actionable Insights For the top 5-10 candidates: - Note proximity to pivot/breakout level - Assess if volume is confirming or diverging - Check for upcoming F&O expiry or result season impacts - Identify F&O lot size (if stock is in F&O segment) ## Scoring System The composite score (0-100) weights 5 components: | Component | Weight | What It Measures | |-----------|--------|-----------------| | Trend Template | 25% | Minervini's 7-point Stage 2 criteria | | Contraction Quality | 25% | Tightening base structure | | Volume Pattern | 20% | Volume dry-up ratio | | Pivot Proximity | 15% | Distance from breakout level | | Relative Strength | 15% | Performance vs Nifty 50 | ## Indian Market Adaptations - **Universe**: Nifty 50/200/500 instead of S&P 500 - **Benchmark**: Relative strength measured vs Nifty 50 (^NSEI) instead of S&P 500 - **Volatility**: T1 depth range widened to 10-40% (vs 8-35% for US) due to higher small-cap volatility - **Circuit Limits**: Stocks hitting circuits may show false VCP patterns — flagged in results - **Liquidity Filter**: Minimum average daily volume of ₹1 crore to filter illiquid stocks - **F&O Availability**: Results indicate whether the stock is in the F&O segment ## Resources ### references/vcp_methodology.md Mark Minervini's VCP theory, Stage 2 criteria, contraction rules, and entry methodology adapted for Indian markets. ### references/scoring_system.md Detailed breakdown of the 5-component composite scoring system with thresholds and examples.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.