Claude Skill

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

LLM Mart · 0 points · 16 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ajeeshworkspace-indian-trading-skills-skills_nse-vcp-screener-dc44698.zip · 17 KB
Part of ajeeshworkspace/indian-trading-skills — 10 skills
This skill couldn't be refreshed from GitHub on the last check — you're seeing the last imported snapshot.

Install

skills CLI npx skills add https://github.com/ajeeshworkspace/indian-trading-skills/tree/master/skills/nse-vcp-screener
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ajeeshworkspace-indian-trading-skills@llmmart
Git 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:

  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

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:

  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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related