Nifty OI Profile
Overview
This application generates a comprehensive Options Open Interest (OI) profile for NIFTY futures and options contracts, providing traders with real-time insights into market sentiment and positioning. The tool combines futures price action with options flow analysis to deliver actionable trading intelligence.

Purpose
The OI Profile serves as a critical tool for derivatives traders to:
Identify key support and resistance levels based on option positioning
Gauge market sentiment through CE/PE activity analysis
Monitor institutional flow via daily OI changes
Correlate price movement with options positioning patterns
"""
NIFTY 28 AUG 2025 — Futures (5m, 7 days) + Options OI Profile (DAILY)
Author : OpenAlgo GPT
Updated : 2025-08-19
Notes : • Options OI (both Current OI and 1D Δ) is read ONLY from 1D history
• No option quotes are used for OI; no intraday fallback
• Futures panel is 5m, last 7 calendar days
• Plotly candlestick x-axis uses category type (as required)
• Fixed timezone issues
"""
print("🔁 OpenAlgo Python Bot is running.") # rule 13
import os, sys, re, time, asyncio, numpy as np, pandas as pd
from datetime import datetime, timedelta
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from openalgo import api
# ───────────────────────── CONFIG ─────────────────────────
API_KEY = 'your-openalgo-apikey'
API_HOST = "http://127.0.0.1:5000"
BASE = "NIFTY"
EXPIRY = "28AUG25" # same expiry for FUT + OI profile
EXCHANGE_IDX = "NSE_INDEX" # for ATM reference (quotes printed per rule 14)
EXCHANGE_FUT = "NFO" # futures candles only
EXCHANGE_OPT = "NFO" # options history (DAILY)
CANDLE_INTERVAL = "5m" # futures candles
CANDLE_DAYS = 7 # last 7 calendar days
STEP = 100 # strike step
RADIUS = 10 # ± strikes around ATM (increased for better OI profile)
BATCH_SIZE = 10 # batched daily-history requests
BATCH_PAUSE = 2
MAX_RETRIES = 1
BACKOFF_SEC = 1.0
# ─────────────────────── INIT CLIENT ──────────────────────
client = api(api_key=API_KEY, host=API_HOST)
if sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# ───────────────────── SYMBOL HELPERS ─────────────────────
FUT_SYMBOL = f"{BASE}{EXPIRY}FUT" # e.g., NIFTY28AUG25FUT (for candles only)
_rx_opt = re.compile(r"^([A-Z]+)(\d{2}[A-Z]{3}\d{2})(\d+)(CE|PE)$")
def parse_option(sym: str):
m = _rx_opt.match(sym)
if not m: return None
base, expiry, strike, typ = m.groups()
return base, expiry, int(strike), typ
def get_atm_strike(step: int = STEP) -> int:
"""Use NSE_INDEX quote to compute nearest 100-pt ATM and PRINT the quote (rule 14)."""
q = client.quotes(symbol=BASE, exchange=EXCHANGE_IDX)
print("Underlying Quote :", q) # rule 14
ltp = q["data"]["ltp"]
return int(round(ltp / step) * step)
# ───────────────── HISTORY NORMALIZER ─────────────────────
def _parse_epoch_like(s: pd.Series) -> pd.DatetimeIndex:
"""Detect ms vs s epoch and parse safely; fallback to strings."""
s_num = pd.to_numeric(s, errors="coerce")
if s_num.notna().any():
mx = float(np.nanmax(s_num.values))
if mx > 1e12: # ms
dt = pd.to_datetime(s_num, unit="ms", errors="coerce")
if dt.notna().any(): return dt
if mx > 1e9: # s
dt = pd.to_datetime(s_num, unit="s", errors="coerce")
if dt.notna().any(): return dt
return pd.to_datetime(s.astype(str), errors="coerce")
def _history_as_df(resp) -> pd.DataFrame | None:
"""
Normalize OpenAlgo history response to a DataFrame.
Handles: DataFrame, dict{'data':[...]} or {'data':{'candles':[...]}} or raw list.
"""
if resp is None: return None
if isinstance(resp, pd.DataFrame):
df = resp.copy()
elif isinstance(resp, dict):
data = resp.get("data")
rows = data if isinstance(data, list) else (data.get("candles") if isinstance(data, dict) else None)
if rows is None: return None
if rows and isinstance(rows[0], (list, tuple)):
cols = ["time","open","high","low","close","volume","oi"][:len(rows[0])]
df = pd.DataFrame(rows, columns=cols)
else:
df = pd.DataFrame(rows)
elif isinstance(resp, list):
df = pd.DataFrame(resp)
else:
return None
# index by timestamp if present
ts_col = next((c for c in ["time","timestamp","date","datetime","ts","Time","Date"] if c in df.columns), None)
if ts_col is not None:
vals = df[ts_col]
if pd.api.types.is_numeric_dtype(vals):
idx = _parse_epoch_like(vals)
else:
idx = pd.to_datetime(vals, errors="coerce")
if idx.notna().any():
df.index = idx
return df
def _find_oi_col(df: pd.DataFrame) -> str | None:
names = [c for c in df.columns if isinstance(c, str)]
for key in ("oi","open_interest","oi_close","oi_open","openinterest"):
for c in names:
if c.lower() == key: return c
for c in names:
if "oi" in c.lower(): return c
return None
# ─────────── DAILY OI for a single option (current & prev) ───────────
def fetch_daily_oi_sync(symbol: str) -> dict | None:
"""
Get CURRENT OI (last daily bar) and PREVIOUS OI (bar -1) strictly from DAILY history.
Returns dict with strike, type, CE/PE OI, Daily delta, and (optional) last daily close for hover.
"""
# fetch only daily bars using start/end dates (rule 4)
end = datetime.now().date()
start = end - timedelta(days=14)
for _ in range(MAX_RETRIES + 1):
try:
resp = client.history(
symbol=symbol, exchange=EXCHANGE_OPT, interval="D",
start_date=start.strftime("%Y-%m-%d"),
end_date=end.strftime("%Y-%m-%d")
)
df = _history_as_df(resp)
if df is not None and not df.empty:
break
except Exception as e:
print(f"Error fetching {symbol}: {e}")
time.sleep(BACKOFF_SEC)
continue
time.sleep(BACKOFF_SEC)
if df is None or df.empty:
return None
oi_col = _find_oi_col(df)
if not oi_col:
return None
s = pd.to_numeric(df[oi_col], errors="coerce").dropna()
if not len(s):
return None
cur_oi = float(s.iloc[-1])
prev_oi = float(s.iloc[-2]) if len(s) >= 2 else None
delta_d = (cur_oi - prev_oi) if prev_oi is not None else None
# optional: last daily close for hover
close_col = "close" if "close" in df.columns else ("c" if "c" in df.columns else None)
ltp = float(pd.to_numeric(df[close_col], errors="coerce").dropna().iloc[-1]) if close_col else None
base, expiry, strike, typ = parse_option(symbol)
return {"symbol": symbol, "strike": strike, "type": typ,
"oi": cur_oi, "oi_delta_d": delta_d, "ltp": ltp}
# ─────────────── GATHER DAILY OI for all strikes ───────────────
async def gather_daily_oi_for_expiry() -> pd.DataFrame:
atm = get_atm_strike()
strikes = [atm + i * STEP for i in range(-RADIUS, RADIUS + 1)]
symbols = [f"{BASE}{EXPIRY}{k}{s}" for k in strikes for s in ("CE", "PE")]
rows: list[dict] = []
for i in range(0, len(symbols), BATCH_SIZE):
batch = symbols[i:i + BATCH_SIZE]
res = await asyncio.gather(*[
asyncio.to_thread(fetch_daily_oi_sync, s) for s in batch
])
rows.extend([r for r in res if r])
if i + BATCH_SIZE < len(symbols):
await asyncio.sleep(BATCH_PAUSE)
if not rows:
raise RuntimeError("No DAILY OI retrieved. Check API/expiry.")
df = pd.DataFrame(rows)
piv = (df.pivot(index="strike", columns="type", values=["oi", "oi_delta_d", "ltp"])
.sort_index())
piv.columns = ["CE_OI", "PE_OI", "CE_OI_D", "PE_OI_D", "CE_LTP", "PE_LTP"]
return piv.reset_index().fillna(0) # Fill NaN with 0 for cleaner display
# ───────────── FUTURES HISTORY (5m, LAST 7 DAYS, CATEGORY X) ─────────────
def get_fut_history_5m_7d():
end_dt = datetime.now()
start_dt = end_dt - timedelta(days=CANDLE_DAYS)
resp = client.history(
symbol=FUT_SYMBOL, exchange=EXCHANGE_FUT, interval=CANDLE_INTERVAL,
start_date=start_dt.strftime("%Y-%m-%d"),
end_date=end_dt.strftime("%Y-%m-%d")
)
df = _history_as_df(resp)
if df is None or df.empty:
raise ValueError(f"History fetch failed for {FUT_SYMBOL}")
# Standardize OHLC field names
rename = {}
for k in ("o","h","l","c"):
if k in df.columns: rename[k] = {"o":"open","h":"high","l":"low","c":"close"}[k]
df = df.rename(columns=rename)
for need in ("open","high","low","close"):
if need not in df.columns and need.capitalize() in df.columns:
df = df.rename(columns={need.capitalize(): need})
# Ensure datetime index and filter window
if not isinstance(df.index, pd.DatetimeIndex):
df.index = pd.to_datetime(df.index, errors="coerce")
df = df.sort_index()
# FIX: Handle timezone-aware comparison properly
cutoff_time = pd.Timestamp(end_dt) - pd.Timedelta(days=CANDLE_DAYS)
# Convert cutoff to match DataFrame timezone if needed
if df.index.tz is not None and cutoff_time.tz is None:
cutoff_time = cutoff_time.tz_localize(df.index.tz)
elif df.index.tz is None and cutoff_time.tz is not None:
cutoff_time = cutoff_time.tz_localize(None)
df = df.loc[df.index >= cutoff_time]
# x as category strings (Plotly rule)
x_cat = df.index.strftime('%d-%b<br>%H:%M').tolist()
total = len(x_cat)
tick_step = max(1, total // 12)
tick_vals = [x_cat[i] for i in range(0, total, tick_step)]
return df, x_cat, tick_vals
# ────────────────────── PLOTTING (OI Profile Style) ────────────────────────
def plot_oi_profile_style(fut_df: pd.DataFrame, fut_x: list[str], fut_ticks: list[str], oi_df: pd.DataFrame):
"""
Create an OI profile with:
Column 1: Candlestick Charts
Column 2: Current OI
Column 3: Change in OI (Daily)
"""
atm = get_atm_strike()
# Filter strikes closer to ATM for better visualization
oi_df_filtered = oi_df[
(oi_df['strike'] >= atm - 1000) &
(oi_df['strike'] <= atm + 1000)
].copy()
# Create subplots with 3 columns
fig = make_subplots(
rows=1, cols=3,
shared_yaxes=True,
horizontal_spacing=0.02,
column_widths=[0.5, 0.25, 0.25],
specs=[[{"type": "candlestick"}, {"type": "bar"}, {"type": "bar"}]],
subplot_titles=["Futures 5m", "Current OI", "Change in OI (D)"]
)
# COLUMN 1: Futures candlestick
fig.add_trace(
go.Candlestick(
x=fut_x,
open=fut_df["open"],
high=fut_df["high"],
low=fut_df["low"],
close=fut_df["close"],
name=f"{BASE} {EXPIRY} FUT",
showlegend=False
),
row=1, col=1
)
# COLUMN 2: Current OI
fig.add_trace(
go.Bar(
y=oi_df_filtered["strike"],
x=oi_df_filtered["CE_OI"],
orientation="h",
name="CE OI",
marker_color="green",
hovertemplate="<b>%{y} CE</b><br>Current OI: %{x:,.0f}<extra></extra>",
showlegend=False
),
row=1, col=2
)
fig.add_trace(
go.Bar(
y=oi_df_filtered["strike"],
x=-oi_df_filtered["PE_OI"], # Negative for left side
orientation="h",
name="PE OI",
marker_color="red",
hovertemplate="<b>%{y} PE</b><br>Current OI: %{customdata:,.0f}<extra></extra>",
customdata=oi_df_filtered["PE_OI"],
showlegend=False
),
row=1, col=2
)
# COLUMN 3: Change in OI (Daily)
fig.add_trace(
go.Bar(
y=oi_df_filtered["strike"],
x=oi_df_filtered["CE_OI_D"],
orientation="h",
name="CE Change (D)",
marker_color="lightgreen",
hovertemplate="<b>%{y} CE</b><br>Δ OI (D): %{x:,.0f}<extra></extra>",
showlegend=False
),
row=1, col=3
)
fig.add_trace(
go.Bar(
y=oi_df_filtered["strike"],
x=-oi_df_filtered["PE_OI_D"], # Negative for left side
orientation="h",
name="PE Change (D)",
marker_color="lightcoral",
hovertemplate="<b>%{y} PE</b><br>Δ OI (D): %{customdata:,.0f}<extra></extra>",
customdata=oi_df_filtered["PE_OI_D"],
showlegend=False
),
row=1, col=3
)
# Layout updates
fig.update_layout(
template="plotly_dark",
height=800,
width=1400,
title=f"{BASE} {EXPIRY} - Futures with Options OI Profile (Daily)",
barmode="overlay",
bargap=0.1,
font=dict(size=10),
# Remove bottom slider/rangeslider
xaxis=dict(rangeslider=dict(visible=False)),
xaxis2=dict(rangeslider=dict(visible=False)),
xaxis3=dict(rangeslider=dict(visible=False))
)
# Update axes
fig.update_xaxes(title_text="Time", type="category",
tickmode="array", tickvals=fut_ticks,
rangeslider=dict(visible=False), row=1, col=1)
fig.update_xaxes(title_text="CE ←→ PE OI", row=1, col=2)
fig.update_xaxes(title_text="CE ←→ PE Change (D)", row=1, col=3)
# Y-axes labels
fig.update_yaxes(title_text="Price / Strike", row=1, col=1)
fig.update_yaxes(title_text="Strike", row=1, col=2)
fig.update_yaxes(title_text="", row=1, col=3)
# Add ATM line across all subplots
fig.add_hline(
y=atm,
line_dash="dash",
line_color="yellow",
line_width=2,
annotation_text=f"ATM {atm}",
annotation_position="top left"
)
# Add grid lines for better readability
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
fig.show()
# ─────────────────────── RUNNER ────────────────────────
async def _main():
try:
print("Fetching futures data...")
fut_df, x_cat, ticks = get_fut_history_5m_7d() # 5m, 7 days
print(f"Got {len(fut_df)} futures candles")
print("Fetching options OI data...")
oi_df = await gather_daily_oi_for_expiry() # DAILY options OI ONLY
print(f"Got OI data for {len(oi_df)} strikes")
print("Creating OI profile chart...")
plot_oi_profile_style(fut_df, x_cat, ticks, oi_df)
print("Chart displayed successfully!")
except Exception as e:
print(f"Error in main execution: {e}")
import traceback
traceback.print_exc()
def _in_nb() -> bool:
try:
import IPython; return IPython.get_ipython() is not None
except ImportError:
return False
if _in_nb():
await _main()
else:
asyncio.run(_main())
Data Sources
Futures Data: 5-minute candlestick charts spanning the last 7 calendar days
Options Data: Daily Open Interest data for both Call (CE) and Put (PE) options
Strike Range: 20 strikes above and below At-The-Money (ATM), with 100-point intervals
Expiry: Focused on current monthly expiry (28AUG25)
Visual Layout
The application presents data in a three-column dashboard:
Column 1 (50% width): Futures candlestick chart showing 5-minute price action over 7 days, providing context for recent price movement and trend analysis.
Column 2 (25% width): Current Open Interest levels displaying existing market positions, with Call options extending right (green) and Put options extending left (red) for easy visual comparison.
Column 3 (25% width): Daily OI changes showing new positioning activity, highlighting where fresh money is entering or exiting the market on a daily basis.
Key Features
Real-time ATM calculation using live NIFTY index quotes
Timezone-aware data processing for accurate historical comparisons
Batch processing for efficient API utilization with rate limiting
Interactive hover details showing exact OI values and changes
Professional dark theme optimized for trading environments
ATM highlighting with yellow reference line across all panels
Last updated