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