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-exceptblock 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, andIV_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
Strikeprice. 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 withTrue.
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 📊💡.
