Skip to main content

Market trap detector for Indian index derivatives (NIFTY, BANKNIFTY)

Project description

nubra_trap_detector

A market-trap detection library for Indian index derivatives (NIFTY, BANKNIFTY).
Identifies bull traps, bear traps, short squeezes, and long squeezes by combining price-action signals with live options-chain data from the Nubra SDK.


What is a "trap"?

A trap is a price move that looks like a breakout but has no genuine conviction behind it — so it reverses sharply, catching trend-followers on the wrong side.

Trap type What happens Who gets hurt
Bull trap Price breaks above a resistance level on weak volume / no OI expansion Breakout buyers
Bear trap Price breaks below support on weak volume / no OI expansion Short sellers
Short squeeze Extreme put-loading + delta imbalance forces trapped shorts to cover Short sellers
Long squeeze Extreme call-loading + delta imbalance forces trapped longs to exit Long holders

Installation

pip install nubra-trap-detector

Requires Python 3.10+ and the Nubra SDK (installed automatically):

pip install nubra-sdk>=0.3.8

Quick start

from nubra_python_sdk.start_sdk import InitNubraSdk, NubraEnv
from nubra_trap_detector import TrapEngine
from nubra_trap_detector.formatters import compact

nubra  = InitNubraSdk(NubraEnv.PROD, env_creds=True)
engine = TrapEngine(nubra_client=nubra, symbols=["NIFTY"], interval="5m")
engine.on_result(compact)
engine.start()
engine.wait()   # blocks; Ctrl+C to stop

Sample output (one line printed every 5 minutes at candle close):

13:20:02  NIFTY       5m  | P: 30.0%  S:  0.0% | [GRN]  16.5% bull_trap           down | bars=21 [09:15->13:20]
13:25:02  NIFTY       5m  | P:  0.0%  S: 65.0% | [ORG]  29.3% short_squeeze        up  | bars=22 [09:15->13:25]

Try without a live API (walkthrough mode)

The run_walkthrough() function takes raw candle / option-chain data and prints every formula and intermediate value — no API connection needed:

from nubra_trap_detector import run_walkthrough

result = run_walkthrough(
    symbol         = "NIFTY",
    candles        = [(open, high, low, close, volume), ...],   # 21 tuples
    ce_chain       = [(strike, oi, delta), ...],
    pe_chain       = [(strike, oi, delta), ...],
    prev_ce_oi     = [...],   # OI from previous chain snapshot (same order)
    prev_pe_oi     = [...],
    orderbook_asks = [{"price": int, "quantity": int}, ...],
    spot           = 24020,
    verbose        = True,    # False = return dict only, no printed output
)
print(result["total"]["trap_probability"])   # 0 – 100

Run the bundled example:

python calc_walkthrough.py

Architecture

Raw market data (Nubra WebSocket + REST)
        |
        v
  StateStore  (thread-safe ring buffers)
  |-- OHLCV candles   per symbol   (25-bar ring)
  |-- Option chain    per symbol   (refreshed every 60 s via REST)
  `-- Orderbook       per ref_id   (live WebSocket)
        |
        |---> PriceTrapDetector   (weight 0.55)
        |       Breakout check (20-bar high/low)
        |       Volume ratio
        |       Chain OI expansion
        |       Orderbook thinness
        |
        |---> SqueezeDetector     (weight 0.45)
        |       PCR (Put-Call Ratio)
        |       Delta-weighted OI imbalance
        |       OI unwind speed
        |
        v
  SignalAggregator
  trap_probability = (price x 0.55 + squeeze x 0.45) x 100   ->   0 – 100

How the score is calculated

PriceTrapDetector (weight: 0.55)

Fires when price breaks a key level but the move lacks conviction.

Sub-check Formula Score added
Breakout close > max(high, last 20 bars) or close < min(low, last 20 bars) Gate (required)
Volume ratio vol_ratio = current_vol / mean(vol, last 20 bars) — fires when vol_ratio < 1.0 +0.30
Chain OI change oi_growth = (curr_chain_OI - prev_chain_OI) / prev_chain_OI — fires when oi_growth < 0.01 +0.40
Orderbook thin total_qty = sum(ask qty within 1% of breakout level) — fires when total_qty == 0 +0.20

Max score: 0.90 (capped at 1.0)

SqueezeDetector (weight: 0.45)

Fires when one side of the options market is dangerously overloaded.

Sub-check Formula Score added
PCR PCR = put_OI / call_OI — fires when PCR > 1.2 (short squeeze) or PCR < 0.75 (long squeeze) +0.35
Delta imbalance `imbalance = call_delta_OI - put_delta_OI
OI unwind Top-3 OI strikes: drop = (prev_OI - curr_OI) / prev_OI — fires when any drop > 0.08 +0.25

Max score: 0.90 (capped at 1.0)

Final aggregation

trap_probability = (price_score x 0.55 + squeeze_score x 0.45) x 100

Clamped to [0, 100]. The detector with the highest score x weight product becomes the dominant detector and sets direction and dominant_trap_type.

Score interpretation

Range Meaning Suggested action
0 – 10 No trap Trade normally
10 – 30 Low risk Monitor
30 – 50 Moderate risk Reduce size, tighten stop
50 – 75 High risk Block new entries in trap direction
75 – 100 Very high conviction Hard gate — avoid entry, consider hedge

API reference

TrapEngine

TrapEngine(
    nubra_client,                        # authenticated Nubra SDK client
    symbols          = ["NIFTY"],        # list of index symbols
    interval         = "5m",            # "1m" | "3m" | "5m" | "10m" | "15m" | "30m" | "1h" | "realtime"
    exchange         = "NSE",
    detector_weights = None,             # dict or None (uses config defaults)
)
Method Returns Description
engine.start() None Discover expiries, start feed, begin polling. Non-blocking.
engine.wait() None Block until Ctrl+C. Call after start().
engine.stop() None Gracefully stop all threads.
engine.on_result(fn) None Register a (dict) -> None callback, called on every poll.
engine.get_result(symbol) dict Run detectors now and return full result dict.
engine.get_score(symbol) TrapScore Lightweight: return TrapScore dataclass only.
engine.refresh_chain_snapshot(symbol) None Manually trigger a REST chain refresh.

get_result() — result dict schema

result = engine.get_result("NIFTY")
{
  "timestamp":    "2026-05-20T13:20:02+0530",
  "symbol":       "NIFTY",
  "interval":     "5m",
  "spot":         24020.0,
  "bars":         21,
  "candle_range": ["09:15", "13:20"],

  "price_trap": {
    "score":           30.0,        # 0–100
    "close":           24020.0,
    "high_n":          24010.0,     # 20-bar high (bull breakout level)
    "low_n":           23480.0,     # 20-bar low  (bear breakout level)
    "breakout":        "bull",      # "bull" | "bear" | "none"
    "vol_ratio":       0.740,       # current vol / 20-bar avg vol
    "chain_oi_change": 0.046,       # fractional chain OI change vs prior snapshot
    "orderbook_thin":  False,       # True = no liquidity near breakout level
    "factors": ["..."],             # plain-English reasons
  },

  "squeeze_trap": {
    "score":           0.0,         # 0–100
    "pcr":             1.047,       # put_OI / call_OI
    "delta_imbalance": 0.015,       # |call_dOI - put_dOI| / total_dOI
    "dominant_side":   "call",      # "call" | "put"
    "oi_unwinding":    False,       # True = top-3 strikes losing OI fast
    "factors": [],
  },

  "total": {
    "trap_probability": 16.5,       # 0–100 — the main signal
    "direction":        "bearish_trap",   # "bearish_trap" | "bullish_trap" | "neutral"
    "dominant_type":    "bull_trap",      # "bull_trap" | "bear_trap" | "short_squeeze" | "long_squeeze" | "none"
    "weights":          {"price_trap": 0.55, "squeeze_trap": 0.45},
  },
}

TrapScore dataclass

Returned by engine.get_score().

@dataclass
class TrapScore:
    trap_probability:  float        # 0.0 – 100.0
    dominant_trap_type: str         # "bull_trap" | "short_squeeze" | ...
    direction:         str          # "bearish_trap" | "bullish_trap" | "neutral"
    breakdown:         dict         # {"price_trap": 0.30, "squeeze_trap": 0.0}
    factors:           list[str]
    timestamp:         datetime

run_walkthrough()

Step-by-step formula trace — no API needed.

from nubra_trap_detector import run_walkthrough

result = run_walkthrough(
    symbol         = "NIFTY",
    candles        = [(open, high, low, close, volume), ...],  # min 21 tuples
    ce_chain       = [(strike, oi, delta), ...],
    pe_chain       = [(strike, oi, delta), ...],
    prev_ce_oi     = [int, ...],     # previous OI snapshot, same order as ce_chain
    prev_pe_oi     = [int, ...],
    orderbook_asks = [{"price": int, "quantity": int}, ...],
    spot           = 24020,
    weights        = None,           # optional override (default from config)
    verbose        = True,           # False = silent, return dict only
)

Formatters

Three built-in formatters ship with the library. Register them with engine.on_result().

from nubra_trap_detector.formatters import compact, table, json_lines
Formatter Output Best for
compact One line per symbol per poll Live monitoring terminal
table Multi-line box with all metrics Debugging
json_lines Full result as a single JSON line Log files, Grafana, dashboards

Write your own:

def my_formatter(result: dict) -> None:
    prob = result["total"]["trap_probability"]
    if prob > 50:
        send_telegram(f"Trap alert: {result['symbol']} {prob:.1f}%")

engine.on_result(my_formatter)

Configuration

All thresholds live in nubra_trap_detector/config.py. Modify the file to tune sensitivity for different market conditions.

# Price trap
BREAKOUT_BARS             = 20     # N-bar high/low lookback
VOLUME_RATIO_THRESHOLD    = 1.0    # below-average volume threshold
CHAIN_OI_EXPAND_THRESHOLD = 0.01   # 1% minimum OI growth to count as "expanding"
ORDERBOOK_THIN_PCT        = 0.01   # "near breakout" = within 1% of level

# Squeeze trap
PCR_UPPER                 = 1.2    # PCR above this -> short squeeze signal
PCR_LOWER                 = 0.75   # PCR below this -> long squeeze signal
DELTA_IMBALANCE_THRESHOLD = 0.20   # 20% imbalance threshold
OI_UNWIND_THRESHOLD       = 0.08   # 8% OI drop = rapid unwind

# Ensemble weights (must sum to 1.0)
DETECTOR_WEIGHTS = {
    "price_trap":   0.55,
    "squeeze_trap": 0.45,
}

Environment variables

Variable Description
NUBRA_API_KEY Your Nubra API key
NUBRA_API_SECRET Your Nubra API secret

The SDK reads these automatically when you pass env_creds=True to InitNubraSdk.


Logging

The library uses Python's standard logging module under the nubra_trap_detector namespace.

import logging
logging.basicConfig(level=logging.DEBUG)   # see all internal messages
logging.getLogger("nubra_trap_detector").setLevel(logging.WARNING)   # suppress

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nubra_trap_detector-0.1.0.tar.gz (34.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nubra_trap_detector-0.1.0-py3-none-any.whl (35.0 kB view details)

Uploaded Python 3

File details

Details for the file nubra_trap_detector-0.1.0.tar.gz.

File metadata

  • Download URL: nubra_trap_detector-0.1.0.tar.gz
  • Upload date:
  • Size: 34.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for nubra_trap_detector-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3661cb70cbde76e333c185deecea63b37d851bf77f7c13dd193c84de59c457e0
MD5 0398a695b26cff76efedad7befa35857
BLAKE2b-256 ae0a17afe04c59cfd6ff91c432b6e8d0407548050a7790639892eb10d1ae04d1

See more details on using hashes here.

File details

Details for the file nubra_trap_detector-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for nubra_trap_detector-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0786d0aedd612b66c902d50504aec2dabd8021496b874cba5da24ec2ab9dc8cd
MD5 9f7b243efda968dd58e4edf98f7f3042
BLAKE2b-256 43dadf6a9a632a928425fc635d46bc79cceed3dc31b7edaea73444a3cc369f8a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page