-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathmain.py
More file actions
331 lines (268 loc) Β· 9.96 KB
/
Copy pathmain.py
File metadata and controls
331 lines (268 loc) Β· 9.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import argparse
import json
import math
import sys
import requests
# ββ Colour helpers βββββββββββββββββββββββββββββββββββββββββββββββ
_COLORS = {
"red": 91, "green": 92, "yellow": 93, "lpurple": 94,
"purple": 95, "cyan": 96, "lgray": 97, "black": 98, "bold": 1,
}
_ENABLED = True # toggled off by --no-color / piped output
def c(name: str, text: str) -> str:
"""Return *text* wrapped in the ANSI colour *name*."""
if not _ENABLED:
return text
code = _COLORS.get(name)
if code is None:
return text
return f"\033[{code}m{text}\033[0m"
def bold(text: str) -> str:
return c("bold", text)
# ββ Strike-rounding helpers ββββββββββββββββββββββββββββββββββββββ
def round_nearest(x: float, num: int = 50) -> int:
return int(math.ceil(float(x) / num) * num)
def nearest_strike_bnf(x: float) -> int:
return round_nearest(x, 100)
def nearest_strike_nf(x: float) -> int:
return round_nearest(x, 50)
# ββ NSE API constants ββββββββββββββββββββββββββββββββββββββββββββ
BASE_OC_URL = "https://www.nseindia.com/option-chain"
URL_BNF = "https://www.nseindia.com/api/option-chain-indices?symbol=BANKNIFTY"
URL_NF = "https://www.nseindia.com/api/option-chain-indices?symbol=NIFTY"
URL_INDICES = "https://www.nseindia.com/api/allIndices"
HEADERS = {
"user-agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/80.0.3987.149 Safari/537.36"
),
"accept-language": "en,gu;q=0.9,hi;q=0.8",
"accept-encoding": "gzip, deflate, br",
}
# ββ Session (cookie management is handled automatically) βββββββββ
_session = requests.Session()
def _prime_session() -> None:
"""Warm up the session so NSE sets initial cookies."""
try:
_session.get(BASE_OC_URL, headers=HEADERS, timeout=5)
except requests.RequestException:
pass # best-effort; subsequent calls will retry
def fetch_json(url: str) -> dict | None:
"""Fetch *url* and return parsed JSON, or None on failure."""
try:
resp = _session.get(url, headers=HEADERS, timeout=5)
except requests.RequestException as exc:
print(c("red", f"[!] Network error: {exc}"), file=sys.stderr)
return None
if resp.status_code == 401:
# Session may have expired β re-prime and retry once
_prime_session()
try:
resp = _session.get(url, headers=HEADERS, timeout=5)
except requests.RequestException as exc:
print(c("red", f"[!] Network error on retry: {exc}"), file=sys.stderr)
return None
if resp.status_code != 200:
print(
c("red", f"[!] HTTP {resp.status_code} for {url}"),
file=sys.stderr,
)
return None
try:
return resp.json()
except json.JSONDecodeError as exc:
print(c("red", f"[!] Invalid JSON response: {exc}"), file=sys.stderr)
return None
# ββ Index helpers βββββββββββββββββββββββββββββββββββββββββββββββββ
def fetch_indices() -> dict[str, tuple[float, int]]:
"""Return {label: (last_price, nearest_strike)} for NIFTY & BANKNIFTY."""
data = fetch_json(URL_INDICES)
if not data:
return {}
result: dict[str, tuple[float, int]] = {}
for idx in data.get("data", []):
if idx["index"] == "NIFTY 50":
ul = idx["last"]
result["Nifty"] = (ul, nearest_strike_nf(ul))
elif idx["index"] == "NIFTY BANK":
ul = idx["last"]
result["Bank Nifty"] = (ul, nearest_strike_bnf(ul))
return result
# ββ Data layer ββββββββββββββββββββββββββββββββββββββββββββββββββββ
def collect_oi_data(
url: str,
nearest: int,
num_strikes: int,
step: int,
) -> list[dict] | None:
"""Fetch option-chain data and return a list of strike dicts.
Each dict::
{"strike": int, "ce_oi": int, "pe_oi": int, "expiry": str}
Returns None on failure.
"""
data = fetch_json(url)
if not data:
return None
records = data.get("records", {})
expiry_dates = records.get("expiryDates", [])
if not expiry_dates:
return None
curr_expiry = expiry_dates[0]
start_strike = nearest - (step * num_strikes)
end_strike = start_strike + (step * num_strikes * 2)
target_strikes = set(range(start_strike, end_strike, step))
rows: list[dict] = []
for item in records.get("data", []):
if item.get("expiryDate") != curr_expiry:
continue
sp = item["strikePrice"]
if sp not in target_strikes:
continue
rows.append({
"strike": sp,
"ce_oi": item.get("CE", {}).get("openInterest", 0),
"pe_oi": item.get("PE", {}).get("openInterest", 0),
"expiry": curr_expiry,
})
# sort ascending by strike
rows.sort(key=lambda r: r["strike"])
return rows
# ββ Table display βββββββββββββββββββββββββββββββββββββββββββββββββ
def fmt_oi(value: int) -> str:
"""Format Open Interest with comma separators."""
return f"{value:>12,}"
def print_header(label: str, last: float, nearest: int) -> None:
print(c("purple", label.ljust(12, " ") + " => "), end="")
print(
c("lpurple", "Last Price: ")
+ bold(str(last))
+ c("lpurple", " Nearest Strike: ")
+ bold(str(nearest))
)
def print_hr() -> None:
print(c("yellow", "-".rjust(70, "-")))
def print_oi_table(
label: str,
last: float,
nearest: int,
rows: list[dict],
) -> None:
"""Print a coloured OI table from pre-fetched *rows*."""
print_hr()
print_header(label, last, nearest)
print_hr()
if not rows:
print(c("red", f"[!] No data for {label}"), file=sys.stderr)
return
expiry = rows[0]["expiry"]
print(c("purple", "Expiry".ljust(12, " ") + " => ") + c("lpurple", expiry))
print_hr()
for row in rows:
print(
c("cyan", str(row["strike"]).rjust(7))
+ c("green", " CE ")
+ "[ " + bold(fmt_oi(row["ce_oi"])) + " ]"
+ c("red", " PE ")
+ "[ " + bold(fmt_oi(row["pe_oi"])) + " ]"
)
# ββ Index config ββββββββββββββββββββββββββββββββββββββββββββββββββ
INDEX_CONFIG = {
"nifty": {
"url": URL_NF,
"label": "Nifty",
"num_strikes": 5,
"step": 50,
},
"banknifty": {
"url": URL_BNF,
"label": "Bank Nifty",
"num_strikes": 10,
"step": 100,
},
}
# ββ CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Fetch NSE option-chain Open Interest data.",
)
parser.add_argument(
"-s", "--symbol",
choices=["nifty", "banknifty", "both"],
default="both",
help="Index to fetch (default: both)",
)
parser.add_argument(
"-n", "--strikes",
type=int,
default=None,
help="Number of strikes above/below ATM (overrides per-index default)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON instead of the coloured table (implies --no-color)",
)
parser.add_argument(
"--no-color",
action="store_true",
help="Strip ANSI colour codes from output",
)
parser.add_argument(
"--pretty",
action="store_true",
help="When used with --json, indent the output",
)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
global _ENABLED
if args.no_color or args.json:
_ENABLED = False
_prime_session()
indices = fetch_indices()
if not indices:
print(c("red", "[!] Could not fetch index data. Exiting."), file=sys.stderr)
sys.exit(1)
# Pick which indices to process
symbols = ["nifty", "banknifty"] if args.symbol == "both" else [args.symbol]
# Collect data for each symbol
all_data: list[dict] = []
for sym in symbols:
cfg = INDEX_CONFIG[sym]
label = cfg["label"]
ul, near = indices.get(label, (0, 0))
if ul == 0:
print(c("red", f"[!] Could not fetch spot price for {label}"), file=sys.stderr)
continue
num_strikes = args.strikes if args.strikes is not None else cfg["num_strikes"]
rows = collect_oi_data(cfg["url"], near, num_strikes, cfg["step"])
all_data.append({
"symbol": label,
"last_price": ul,
"nearest_strike": near,
"rows": rows or [],
})
# ββ Output ββββββββββββββββββββββββββββββββββββββββββββββββ
if args.json:
print(
json.dumps(
{"data": all_data},
indent=2 if args.pretty else None,
default=str,
)
)
return
# Table mode
print("\033c", end="") # clear screen
for entry in all_data:
print_oi_table(
label=entry["symbol"],
last=entry["last_price"],
nearest=entry["nearest_strike"],
rows=entry["rows"],
)
print_hr()
if __name__ == "__main__":
main()