Find IV Inversions and Calendar Spread Opportunities Like a Pro 🚀

🔍 Introduction

Trading is all about identifying hidden edges where risk and reward align in your favor. One such edge comes from Implied Volatility (IV) inversions of option contracts between near and far expiries. These inversions often create profitable calendar spread setups, where you sell a short-term option and buy a longer-term one.

This article walks you through a powerful Python script that fetches real-time NSE F&O data, analyzes IV structures, and highlights calendar spread opportunities. Whether you’re an aspiring quant, an active options trader, or just curious about market structure, this tool will give you deeper insights into how volatility shapes profitable trades 📊.

📦 Step 1: Importing the Libraries

# Imports necessary libraries
import time
import pandas as pd
import logging
from nsepython import expiry_list, nse_fno, fnolist
from datetime import datetime
from tabulate import tabulate
from concurrent.futures import ThreadPoolExecutor

We bring in essential libraries: pandas for data wrangling, nsepython to fetch NSE option data, and ThreadPoolExecutor for parallel execution ⚡.

📝 Step 2: Logging

logging.basicConfig(filename="errors.log", level=logging.ERROR)

All errors are logged into errors.log, ensuring smooth debugging without breaking execution 🛠️.

📊 Step 3: Symbol Lists

We maintain lists of Nifty50 stocks and indices (NIFTY, BANKNIFTY) to run our analysis.

⚙️ Step 4: Core Functions

1️⃣ Fetch F&O Data

def get_fno_data(symbol):
    fno_data = nse_fno(symbol)
    return fno_data['stocks'], fno_data['stocks'][0]['underlyingValue']

This function fetches option chain data 📥 and the spot price for the given symbol.

2️⃣ Get Lot Size

def get_lot_size(records):
    for rec in records:
        try:
            lot_size = rec['marketDeptOrderBook']['tradeInfo']['marketLot']
            if lot_size:
                return lot_size
        except:
            continue
    return 0

Retrieves the lot size (number of shares per contract). Crucial for P&L calculation 💰.

3️⃣ Flatten Data

def get_flat_data(records):
    flat_data = []
    for rec in records:
        meta = rec.get("metadata", {})
        iv = rec.get('marketDeptOrderBook', {}).get('otherInfo', {}).get('impliedVolatility')
        flat = {
            'strikePrice': meta.get('strikePrice'),
            'expiryDate': meta.get('expiryDate'),
            'optionType': meta.get('optionType'),
            'lastPrice': meta.get('lastPrice'),
            'identifier': meta.get('identifier'),
            'instrumentType': meta.get('instrumentType'),
            'impliedVolatility': iv
        }
        flat_data.append(flat)
    return pd.DataFrame(flat_data)

Converts nested JSON into a clean DataFrame 🧹 for easier filtering.

📈 Step 5: Analyzing Symbols

🕵️‍♂️

The function, analyze_symbol, is like a financial detective. Its mission is to sift through mountains of options data to find a special clue called an Implied Volatility (IV) Inversion. When it finds this clue, it builds a complete case file for a potential trade!

Let’s dive into how it cracks the case, step-by-step.


1. The Setup: Getting the Dates Right 📅

Python

def analyze_symbol(symbol):
    """
    Analyzes option data for a symbol to find IV inversions.
    ...
    """
    try:
        expiry_dates = expiry_list(symbol)
        near_expiry_str, far_expiry_str = expiry_dates[0], expiry_dates[1]
    except Exception as e:
        logging.error(f"Could not get expiry dates for {symbol}: {e}")
        return []

    # Parse and reformat the expiry date strings
    near_expiry_dt = datetime.strptime(near_expiry_str, "%d-%b-%Y")
    far_expiry_dt = datetime.strptime(far_expiry_str, "%d-%b-%Y")
    near_expiry_formatted = near_expiry_dt.strftime("%d-%b")
    far_expiry_formatted = far_expiry_dt.strftime("%d-%b")
  • Mission Briefing: The function starts by accepting its target symbol (like ‘NIFTY’).
  • Finding Timelines: It immediately asks for all available expiration dates. For this strategy, it only cares about the first two: the near-expiry (the one ending soonest) and the far-expiry (the one right after it). The try-except block is its safety net 🛡️, catching errors if the dates can’t be found.
  • Date Makeover: It then converts the date strings (e.g., “28-Aug-2025”) into a more usable format and creates snazzy, short versions (like “28-Aug”) for the final report.

2. The Data Heist & Cleanup Crew 🧹

Python

    records, spot = get_fno_data(symbol)
    lot_size = get_lot_size(records)
    df = get_flat_data(records)
    df = df.dropna(subset=['strikePrice', 'expiryDate', 'optionType', 'lastPrice']).copy()
    df = df[(df['lastPrice'] > 0) & (df['impliedVolatility'] > 0)]
  • Grab the Goods: get_fno_data(symbol) is like sending a drone to fetch all the options data and the current price (spot) of the stock.
  • Organize the Loot: The raw data is messy, so it’s loaded into a pandas DataFrame df, which is like a super-powered spreadsheet.
  • The Cleanup Crew: Now, it’s time to tidy up!
    • dropna(...): It throws out any “junk” data—options missing crucial info like their price or strike.
    • df[...] > 0: It also gets rid of any options with a price or IV of zero. These are like ghost contracts 👻 that haven’t been traded and are useless for our analysis.

3. Focusing the Search: Zeroing in on the Target 🎯

Python

    unique_strikes = sorted(df['strikePrice'].unique())
    if not unique_strikes:
        return []

    atm_strike = min(unique_strikes, key=lambda x: abs(x - spot))
    atm_index = unique_strikes.index(atm_strike)
    min_index = max(0, atm_index - 5)
    max_index = min(len(unique_strikes), atm_index + 6)
    allowed_strikes = set(unique_strikes[min_index:max_index])
    df = df[df['strikePrice'].isin(allowed_strikes)]

Instead of looking everywhere, our detective focuses on where the action is.

  • Find the Bullseye: It first finds the At-The-Money (ATM) strike. This is the strike price that’s closest to the stock’s current price. It’s the center of the trading universe for that stock.
  • Create a Hot Zone: It then creates a “hot zone” by selecting only the 5 strikes above and 5 strikes below the ATM strike. Why? Because these are the most actively traded and liquid options. Looking too far out is a waste of time.
  • Apply the Filter: The code then filters the main DataFrame to keep only the options within this hot zone.

4. The Magic Trick: Pivoting the Data 🔄

Python

    # Filter for only the two relevant expiries
    filtered_df = df[df['expiryDate'].isin([near_expiry_str, far_expiry_str])].copy()

    # Pivot the DataFrame
    pivoted_df = filtered_df.pivot_table(
        index=['strikePrice', 'optionType'],
        columns='expiryDate',
        values=['lastPrice', 'impliedVolatility']
    )

    # Clean up column names...
    pivoted_df.columns = [f"{col[0]}_{col[1].replace('-', '_')}" for col in pivoted_df.columns]
    ...

This is one of the coolest parts! Imagine your data is a long, jumbled list. The pivot_table function is like a magician that reorganizes it into a perfect, wide table.

  • Before: One row for the NIFTY 22500 Call for Aug, another row for the NIFTY 22500 Call for Sep, etc.
  • After ✨: ONE single row for the NIFTY 22500 Call, with columns for lastPrice_Aug, IV_Aug, lastPrice_Sep, and IV_Sep.

This trick puts the near-term and far-term data for the exact same option right next to each other, making comparison a breeze!


5. The Final Assembly: Calls ❤️ Puts

Python

    # Separate Calls and Puts and merge them
    calls_df = pivoted_df[pivoted_df['Strategy'] == 'Call'].copy()
    puts_df = pivoted_df[pivoted_df['Strategy'] == 'Put'].copy()

    merged = calls_df.merge(puts_df, on='Strike', suffixes=('_Call', '_Put'))

The table from the last step is good, but Calls and Puts are still on separate rows. This step brings them together.

  • Unite!: It merges the Call data and Put data for the same Strike price. Now, a single row contains everything for that strike: Call prices for both expiries, Put prices for both expiries, and all their IVs. We now have the complete picture for each strike price. 🤝

6. The “Aha!” Moment: Spotting the Inversion ⚡

Python

    # Recalculate IV differences and flags
    merged['Call_IV_Diff'] = ... # IV_near - IV_far
    merged['Put_IV_Diff'] = ... # IV_near - IV_far

    merged['Call_Inv'] = merged['Call_IV_Diff'] > 0
    merged['Put_Inv'] = merged['Put_IV_Diff'] > 0

This is the core of the investigation!

  • The Norm (Contango): Usually, options with more time have higher IV (more time for things to go crazy!). Think of it as long-term weather forecasts being more uncertain than tomorrow’s.
  • The Anomaly (Inversion): An IV inversion is when this flips! The near-term option has higher IV than the far-term one. IV_near > IV_far. This is the market screaming that it’s worried about something right now—like an earnings report or a big news event. 😱
  • Find the Clue: The code calculates IV_near - IV_far. If this number is positive, BINGO! We have an inversion. It flags these rows with True.

7. The Payoff: Building the Trade Idea 💡💰

Python

    trades = []
    for _, row in merged.iterrows():
        # Check for Call IV inversion
        if row['Call_Inv']:
            # ... calculate profit, breakeven, etc. ...
            trades.append({ ... }) # Add the trade idea to our list
        # Check for Put IV inversion
        if row['Put_Inv']:
            # ... do the same for Puts ...
            trades.append({ ... })
    return trades

Now that the detective has found the clues, it’s time to generate actionable trade ideas.

  • Loop Through Suspects: It goes through each strike that was flagged with an inversion.
  • Create the Plan: For each one, it builds a Calendar Spread trade, which is perfect for this situation. It involves SELLING the expensive near-term option and BUYING the cheaper far-term option.
  • Generate the Report: It calculates all the key stats a trader needs:
    • Est. Profit: A rough guess of the potential reward.
    • Breakeven %: How much the stock needs to move against you before you start losing money.
    • Action: A crystal-clear instruction, like "SELL 28-Aug CE / BUY 25-Sep CE".
  • Return the Findings: The function returns a list of these beautifully detailed trade ideas, ready for a human trader to review. 🎉

🚀 Step 6: Running the Analysis

    """
    Main function to take user input and run the analysis.
    """
    print("Select an analysis option:")
    print("1: Analyze a single symbol")
    print("2: Analyze Nifty and Bank Nifty (--index)")
    print("3: Analyze all Nifty 50 stocks (--nifty50)")
    print("4: Analyze all F&O symbols (--all)")

    choice = input("Enter your choice (1-4): ")

    symbols_to_analyze = []

    if choice == '1':
        symbol = input("Enter the symbol to analyze: ").upper()
        symbols_to_analyze.append(symbol)
    elif choice == '2':
        symbols_to_analyze = index
    elif choice == '3':
        symbols_to_analyze = nifty50
    elif choice == '4':
        symbols_to_analyze = fnolist()
    else:
        print("Invalid choice. Please run the script again and select a number from 1 to 4.")
        return

    start_time = time.time()
    all_trades = []

    # Use ThreadPoolExecutor for parallel analysis
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = {executor.submit(analyze_symbol, sym): sym for sym in symbols_to_analyze}
        for future in futures:
            sym = futures[future]
            try:
                result = future.result()
                if result:
                    all_trades.extend(result)
            except Exception as e:
                logging.error(f"Error in {sym}: {str(e)}")

    if all_trades:
        df_all = pd.DataFrame(all_trades)
        print("\n--- Top 20 Potential Trades ---")
        print(tabulate(df_all.sort_values(by="IV Diff", ascending=False).head(20), headers="keys",
                       tablefmt="fancy_grid"))
    else:
        print("\nNo valid trades found for the selected symbols.")

    end_time = time.time()
    print(f"\nTotal Execution Time: {round(end_time - start_time, 2)} seconds")

Uses multithreading ⚡ to analyze multiple symbols in parallel. Results are tabulated neatly using tabulate.

Output

📜 Full Code

# Imports necessary libraries
import time
import pandas as pd
import logging
from nsepython import expiry_list, nse_fno, fnolist
from datetime import datetime
from tabulate import tabulate
from concurrent.futures import ThreadPoolExecutor

# Sets up basic logging to capture and store errors in a file named "errors.log"
logging.basicConfig(filename="errors.log", level=logging.ERROR)

# ---
# ### Symbol Lists
# Defines lists of symbols for different market segments.
index = ['NIFTY', 'BANKNIFTY']
nifty50 = ['NIFTY', 'BANKNIFTY', 'M&M', 'MARUTI', 'HDFCAMC', 'POLYCAB', 'LUPIN', 'ZYDUSLIFE', 'TVSMOTOR', 'BHARTIARTL',
           'DABUR', 'UPL', 'IGL', 'HAL', 'ASHOKLEY', 'RBLBANK', 'TITAN', 'BAJFINANCE', 'APOLLOHOSP', 'PIDILITIND',
           'BAJAJ-AUTO', 'GODREJPROP', 'SUNPHARMA', 'CIPLA', 'POWERGRID', 'DRREDDY', 'TATACONSUM', 'INDIGO', 'PFC',
           'IRFC', 'LTIM', 'BOSCHLTD', 'TATATECH', 'VEDL', 'WIPRO', 'INFY', 'LT', 'TRENT', 'HINDUNILVR', 'TATAPOWER',
           'BAJAJFINSV', 'VOLTAS', 'LICI', 'TATAMOTORS', 'HINDALCO', 'IOC', 'ICICIBANK', 'INDUSINDBK', 'HDFCLIFE',
           'EICHERMOT', 'AXISBANK', 'YESBANK', 'HINDPETRO', 'GAIL', 'ONGC', 'BPCL', 'COALINDIA', 'JIOFIN', 'RELIANCE',
           'MARICO',
           'SBILIFE', 'SBIN', 'BANKBARODA', 'RECLTD', 'TECHM', 'NHPC', 'UNIONBANK', 'DALBHARAT', 'JINDALSTEL', 'MFSL',
           'HDFCBANK', 'CROMPTON', 'GRANULES', 'HINDZINC', 'FORTIS', 'EXIDEIND', 'NATIONALUM', 'KPITTECH', 'LAURUSLABS',
           'ABB', 'DLF', 'RVNL', 'FEDERALBNK', 'TATAELXSI', 'AMBUJACEM', 'JUBLFOOD', 'ADANIPORTS', 'TCS', 'IIFL',
           'JSWSTEEL',
           'MAXHEALTH', 'AUBANK', 'PNB', 'NESTLEIND', 'NMDC', 'KOTAKBANK', 'HCLTECH', 'ITC', 'IRB', 'ABCAPITAL',
           'SHREECEM',
           'ADANIENSOL', 'TATASTEEL', 'BANDHANBNK', 'ULTRACEMCO', 'HEROMOTOCO', 'NAUKRI', 'ADANIENT', 'COLPAL', 'LODHA',
           'ASIANPAINT', 'MAZDOCK', 'BHARATFORG', 'GRASIM', 'LICHSGFIN', 'ICICIGI', 'MOTHERSON', 'ASHOKLEY', 'HAVELLS',
           'IREDA', 'PERSISTENT',
           'DMART', 'ATGL', 'ASTRAL', 'KFINTECH', 'MUTHOOTFIN', 'BDL', '360ONE', 'APLAPOLLO', 'NYKAA', 'BHEL', 'IRCTC',
           'CYIENT', 'OBEROIRLTY', 'PATANJALI', 'MPHASIS', 'OIL', 'INDIANB', 'TATATECH', 'BANKINDIA', 'PNBHOUSING',
           'IEX',
           'ADANIGREEN', 'BRITANNIA', 'TORNTPOWER', 'NTPC', 'NBCC', 'SHRIRAMFIN', 'SBICARD', 'ANGELONE', 'VOLTAS',
           'LICI',
           'INDHOTEL', 'JSWENERGY', 'OFSS', 'NCC', 'SAIL', 'SIEMENS', 'ONGC', 'CONCOR', 'BPCL', 'ETERNAL', 'CANBK',
           'AMBER',
           'COALINDIA', 'JIOFIN', 'RELIANCE', 'MARICO', 'SBILIFE', 'SBIN', 'BANKBARODA', 'RECLTD', 'TECHM', 'NHPC',
           'UNIONBANK',
           'DALBHARAT', 'JINDALSTEL', 'MFSL', 'HDFCBANK', 'CROMPTON', 'GRANULES', 'HINDZINC', 'FORTIS', 'EXIDEIND',
           'NATIONALUM',
           'KPITTECH', 'LAURUSLABS', 'ABB', 'DLF', 'RVNL', 'FEDERALBNK', 'TATAELXSI', 'AMBUJACEM', 'JUBLFOOD',
           'ADANIPORTS',
           'TCS', 'IIFL', 'JSWSTEEL', 'MAXHEALTH', 'AUBANK', 'PNB', 'NESTLEIND', 'NMDC', 'KOTAKBANK', 'HCLTECH', 'ITC',
           'IRB',
           'ABCAPITAL', 'SHREECEM', 'ADANIENSOL', 'TATASTEEL', 'BANDHANBNK', 'ULTRACEMCO', 'HEROMOTOCO', 'NAUKRI',
           'ADANIENT',
           'COLPAL', 'LODHA', 'ASIANPAINT', 'MAZDOCK', 'BHARATFORG', 'GRASIM', 'LICHSGFIN', 'ICICIGI', 'MOTHERSON',
           'NIFTYIT']


def get_fno_data(symbol):
    """
    Fetches F&O (Futures and Options) data for a given symbol.
    Args:
        symbol (str): The stock or index symbol (e.g., 'NIFTY').
    Returns:
        tuple: A tuple containing the list of option records and the underlying spot price.
    """
    fno_data = nse_fno(symbol)
    return fno_data['stocks'], fno_data['stocks'][0]['underlyingValue']


def get_lot_size(records):
    """
    Retrieves the market lot size for the given symbol from the records.
    Args:
        records (list): A list of option records.
    Returns:
        int: The lot size, or 0 if not found.
    """
    for rec in records:
        try:
            lot_size = rec['marketDeptOrderBook']['tradeInfo']['marketLot']
            if lot_size:
                return lot_size
        except:
            continue
    return 0


def get_flat_data(records):
    """
    Flattens the nested JSON option data into a list of dictionaries.
    Args:
        records (list): A list of nested option records.
    Returns:
        pd.DataFrame: A DataFrame containing key option metrics.
    """
    flat_data = []
    for rec in records:
        meta = rec.get("metadata", {})
        iv = rec.get('marketDeptOrderBook', {}).get('otherInfo', {}).get('impliedVolatility')
        flat = {
            'strikePrice': meta.get('strikePrice'),
            'expiryDate': meta.get('expiryDate'),
            'optionType': meta.get('optionType'),
            'lastPrice': meta.get('lastPrice'),
            'identifier': meta.get('identifier'),
            'instrumentType': meta.get('instrumentType'),
            'impliedVolatility': iv
        }
        flat_data.append(flat)
    return pd.DataFrame(flat_data)


def analyze_symbol(symbol):
    """
    Analyzes option data for a symbol to find IV inversions.
    Args:
        symbol (str): The stock or index symbol.
    Returns:
        list: A list of dictionaries, where each dictionary represents a potential trade.
    """
    try:
        expiry_dates = expiry_list(symbol)
        near_expiry_str, far_expiry_str = expiry_dates[0], expiry_dates[1]
    except Exception as e:
        logging.error(f"Could not get expiry dates for {symbol}: {e}")
        return []

    # Parse and reformat the expiry date strings
    near_expiry_dt = datetime.strptime(near_expiry_str, "%d-%b-%Y")
    far_expiry_dt = datetime.strptime(far_expiry_str, "%d-%b-%Y")
    near_expiry_formatted = near_expiry_dt.strftime("%d-%b")
    far_expiry_formatted = far_expiry_dt.strftime("%d-%b")

    records, spot = get_fno_data(symbol)
    lot_size = get_lot_size(records)
    df = get_flat_data(records)
    df = df.dropna(subset=['strikePrice', 'expiryDate', 'optionType', 'lastPrice']).copy()
    df = df[(df['lastPrice'] > 0) & (df['impliedVolatility'] > 0)]

    unique_strikes = sorted(df['strikePrice'].unique())
    if not unique_strikes:
        return []

    atm_strike = min(unique_strikes, key=lambda x: abs(x - spot))
    atm_index = unique_strikes.index(atm_strike)
    min_index = max(0, atm_index - 5)
    max_index = min(len(unique_strikes), atm_index + 6)
    allowed_strikes = set(unique_strikes[min_index:max_index])
    df = df[df['strikePrice'].isin(allowed_strikes)]

    # Filter for only the two relevant expiries
    filtered_df = df[df['expiryDate'].isin([near_expiry_str, far_expiry_str])].copy()

    # Pivot the DataFrame to consolidate all data into a single table
    pivoted_df = filtered_df.pivot_table(
        index=['strikePrice', 'optionType'],
        columns='expiryDate',
        values=['lastPrice', 'impliedVolatility']
    )

    # Reset columns and flatten MultiIndex for easy access
    pivoted_df.columns = [f"{col[0]}_{col[1].replace('-', '_')}" for col in pivoted_df.columns]
    pivoted_df.reset_index(inplace=True)
    pivoted_df = pivoted_df.rename(columns={'strikePrice': 'Strike', 'optionType': 'Strategy'})

    # Separate Calls and Puts and merge them for final analysis
    calls_df = pivoted_df[pivoted_df['Strategy'] == 'Call'].copy()
    puts_df = pivoted_df[pivoted_df['Strategy'] == 'Put'].copy()

    merged = calls_df.merge(puts_df, on='Strike', suffixes=('_Call', '_Put'))

    # Recalculate IV differences and flags based on new columns
    merged['Call_IV_Diff'] = merged[f'impliedVolatility_{near_expiry_str.replace("-", "_")}_Call'] - merged[
        f'impliedVolatility_{far_expiry_str.replace("-", "_")}_Call']
    merged['Put_IV_Diff'] = merged[f'impliedVolatility_{near_expiry_str.replace("-", "_")}_Put'] - merged[
        f'impliedVolatility_{far_expiry_str.replace("-", "_")}_Put']

    merged['Call_Inv'] = merged['Call_IV_Diff'] > 0
    merged['Put_Inv'] = merged['Put_IV_Diff'] > 0

    expiry_dt = pd.to_datetime(near_expiry_str, dayfirst=True)
    days_to_expiry = max((expiry_dt - pd.to_datetime(datetime.today())).days, 1)

    trades = []
    for _, row in merged.iterrows():
        # Check for Call IV inversion
        if row['Call_Inv']:
            expected_profit = (row['Call_IV_Diff'] / 100) * spot * 0.5 * lot_size
            net_premium = row[f'lastPrice_{far_expiry_str.replace("-", "_")}_Call'] - row[
                f'lastPrice_{near_expiry_str.replace("-", "_")}_Call']
            breakeven_pct = (net_premium / spot) * 100
            target_move_pct = abs(row['Strike'] - spot) / spot * 100
            trades.append({
                "Symbol": symbol,
                "Strike": row['Strike'],
                "Strategy": "CE Calendar",
                "Action": f"SELL {near_expiry_formatted} CE @ {round(row[f'lastPrice_{near_expiry_str.replace("-", "_")}_Call'], 2)} / BUY {far_expiry_formatted} CE @ {round(row[f'lastPrice_{far_expiry_str.replace("-", "_")}_Call'], 2)}",
                "IV Diff": round(row['Call_IV_Diff'], 2),
                "Est. Profit": round(expected_profit, 2),
                "Breakeven %": round(breakeven_pct, 2),
                "Target %": round(target_move_pct, 2),
                "Days to Expiry": days_to_expiry
            })
        # Check for Put IV inversion
        if row['Put_Inv']:
            expected_profit = (row['Put_IV_Diff'] / 100) * spot * 0.5 * lot_size
            net_premium = row[f'lastPrice_{far_expiry_str.replace("-", "_")}_Put'] - row[
                f'lastPrice_{near_expiry_str.replace("-", "_")}_Put']
            breakeven_pct = (net_premium / spot) * 100
            target_move_pct = abs(row['Strike'] - spot) / spot * 100
            trades.append({
                "Symbol": symbol,
                "Strike": row['Strike'],
                "Strategy": "PE Calendar",
                "Action": f"SELL {near_expiry_formatted} PE @ {round(row[f'lastPrice_{near_expiry_str.replace("-", "_")}_Put'], 2)} / BUY {far_expiry_formatted} PE @ {round(row[f'lastPrice_{far_expiry_str.replace("-", "_")}_Put'], 2)}",
                "IV Diff": round(row['Put_IV_Diff'], 2),
                "Est. Profit": round(expected_profit, 2),
                "Breakeven %": round(breakeven_pct, 2),
                "Target %": round(target_move_pct, 2),
                "Days to Expiry": days_to_expiry
            })
    return trades


def main():
    """
    Main function to take user input and run the analysis.
    """
    print("Select an analysis option:")
    print("1: Analyze a single symbol")
    print("2: Analyze Nifty and Bank Nifty (--index)")
    print("3: Analyze all Nifty 50 stocks (--nifty50)")
    print("4: Analyze all F&O symbols (--all)")

    choice = input("Enter your choice (1-4): ")

    symbols_to_analyze = []

    if choice == '1':
        symbol = input("Enter the symbol to analyze: ").upper()
        symbols_to_analyze.append(symbol)
    elif choice == '2':
        symbols_to_analyze = index
    elif choice == '3':
        symbols_to_analyze = nifty50
    elif choice == '4':
        symbols_to_analyze = fnolist()
    else:
        print("Invalid choice. Please run the script again and select a number from 1 to 4.")
        return

    start_time = time.time()
    all_trades = []

    # Use ThreadPoolExecutor for parallel analysis
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = {executor.submit(analyze_symbol, sym): sym for sym in symbols_to_analyze}
        for future in futures:
            sym = futures[future]
            try:
                result = future.result()
                if result:
                    all_trades.extend(result)
            except Exception as e:
                logging.error(f"Error in {sym}: {str(e)}")

    if all_trades:
        df_all = pd.DataFrame(all_trades)
        print("\n--- Top 20 Potential Trades ---")
        print(tabulate(df_all.sort_values(by="IV Diff", ascending=False).head(20), headers="keys",
                       tablefmt="fancy_grid"))
    else:
        print("\nNo valid trades found for the selected symbols.")

    end_time = time.time()
    print(f"\nTotal Execution Time: {round(end_time - start_time, 2)} seconds")


if __name__ == "__main__":
    main()

🎯 Conclusion

This script is like your personal IV scanner 🔍. It crunches through all F&O symbols, spots calendar spread opportunities, and helps traders make smarter, data-backed decisions 📊💡.

Leave a Reply