-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathstreamlit_app.py
More file actions
3539 lines (2982 loc) · 153 KB
/
Copy pathstreamlit_app.py
File metadata and controls
3539 lines (2982 loc) · 153 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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Upstox API Explorer — Streamlit web app.
All 38 examples from the CLI scripts wrapped in an interactive UI.
Paste your analytics (or daily access) token in the sidebar and explore.
"""
import math
import sys
import os
import time
from datetime import date, datetime, timedelta, timezone
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
sys.path.insert(0, os.path.dirname(__file__))
import upstox_client
from utils import (
get_api_client,
get_futures_sorted,
get_full_quote,
get_historical_candles,
get_ltp,
search_instrument,
)
# ── Page config ───────────────────────────────────────────────────────────────
st.set_page_config(
page_title="Upstox API Explorer",
page_icon="📈",
layout="wide",
initial_sidebar_state="expanded",
)
# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
st.title("📈 Upstox API Explorer")
st.caption("Instrument Search + Analytics Token")
token = st.text_input(
"🔑 Token",
type="password",
placeholder="Paste analytics or access token…",
help="Analytics token: 1-year validity, read-only. "
"Get it from Upstox Developer Apps → Analytics tab.",
)
st.divider()
CATEGORIES = {
"🔍 Instrument Search": [
"Search Equity",
"Search Futures",
"Search Options",
],
"📈 Futures & Basis": [
"NIFTY Futures Spread",
"BankNifty Futures Spread",
"Cash-Futures Basis",
"Futures Roll Cost",
"MCX Crude Spread",
],
"🎯 Options Strategies": [
"Straddle Pricer",
"Strangle Pricer",
"Bull Call Spread",
"Iron Condor",
"Butterfly Spread",
"Calendar Spread",
"Put-Call Parity",
],
"📊 Options Analytics": [
"Options Chain Builder",
"Max Pain Calculator",
"OI Skew",
"Volatility Skew",
"Gamma Exposure",
"Option Chain (Native)",
"Option Greeks",
],
"⚖️ Arbitrage": [
"NSE / BSE Arbitrage",
"ETF vs Index",
"Currency Futures Spread",
],
"📉 Historical Analysis": [
"Historical Candles",
"Moving Average (SMA)",
"Historical Volatility",
"52-Week High / Low",
],
"🗂️ Portfolio & Screening": [
"Sector Index Comparison",
"Top Volume Stocks",
"Futures OI Buildup",
],
"📡 Market Data": [
"Market Status",
"Market Holidays",
"Market Timings",
"Intraday Chart",
"Live Depth (5-level)",
"Live Depth MCX",
"Live Depth USDINR",
],
"📊 Market Information": [
"FII Data",
"DII Data",
"OI",
"Change in OI",
"Max Pain",
"PCR",
],
"🔬 Fundamentals Analysis": [
"Company Profile",
"Key Ratios",
"Balance Sheet",
"Income Statement",
"Cash Flow",
"Corporate Actions",
"Share Holdings",
"Competitors",
],
}
category = st.selectbox("Category", list(CATEGORIES.keys()))
example = st.selectbox("Example", CATEGORIES[category])
st.divider()
st.caption(
"Built with [Streamlit](https://streamlit.io) · "
"[Upstox API Docs](https://upstox.com/developer/api-documentation/)"
)
# ── Shared helpers ────────────────────────────────────────────────────────────
def require_client():
if not token:
st.info("👈 Paste your Upstox token in the sidebar to get started.")
st.stop()
return get_api_client(token)
def lv(obj):
"""last_price from LTP or full-quote object."""
if obj is None:
return 0.0
return obj.last_price if hasattr(obj, "last_price") else obj.get("last_price", 0.0)
def vv(obj):
"""volume."""
if obj is None:
return 0
return obj.volume if hasattr(obj, "volume") else obj.get("volume", 0)
def cv(obj):
"""close / previous-close from LTP object (.cp)."""
if obj is None:
return 0.0
return obj.cp if hasattr(obj, "cp") else obj.get("cp", 0.0)
def ov(obj):
"""open interest from full-quote object."""
if obj is None:
return 0
return obj.oi if hasattr(obj, "oi") else obj.get("oi", 0)
def ohlc(obj):
"""Returns (open, high, low, close) from full-quote object."""
if obj is None:
return 0.0, 0.0, 0.0, 0.0
o = obj.ohlc if hasattr(obj, "ohlc") else obj.get("ohlc", {})
if hasattr(o, "open"):
return o.open, o.high, o.low, o.close
return o.get("open", 0.0), o.get("high", 0.0), o.get("low", 0.0), o.get("close", 0.0)
def dte(expiry_str: str) -> int:
try:
return max((datetime.strptime(expiry_str, "%Y-%m-%d").date() - date.today()).days, 1)
except Exception:
return 30
def fetch_one(client, query, expiry, itype, offset):
resp = search_instrument(
client, query,
exchanges="NSE", segments="FO",
instrument_types=itype, expiry=expiry,
atm_offset=offset, records=1,
)
data = resp.data or []
return data[0] if data else None
def fetch_options_range(client, query, expiry, itype, n, bar=None):
"""Fetch options instruments for offsets -n … +n, deduped by strike."""
instruments, seen, unique = [], set(), []
total = n * 2 + 1
for i, offset in enumerate(range(-n, n + 1)):
resp = search_instrument(
client, query,
exchanges="NSE", segments="FO",
instrument_types=itype, expiry=expiry,
atm_offset=offset, records=1,
)
data = resp.data or []
if data:
instruments.append(data[0])
if bar:
bar.progress((i + 1) / total)
for inst in instruments:
k = inst.get("strike_price", 0)
if k not in seen:
seen.add(k)
unique.append(inst)
return unique
def contango_label(spread):
if spread > 0:
return "🟢 **Contango** — far month at premium. Normal for index/equity futures."
if spread < 0:
return "🔴 **Backwardation** — far month at discount. Unusual — check news."
return "⚪ Spread is zero — contracts at parity."
# ── Page header ───────────────────────────────────────────────────────────────
st.title(example)
st.caption(f"Category: {category}")
st.divider()
# ═════════════════════════════════════════════════════════════════════════════
# 🔍 INSTRUMENT SEARCH
# ═════════════════════════════════════════════════════════════════════════════
if example == "Search Equity":
client = require_client()
c1, c2, c3 = st.columns([2, 1, 1])
query = c1.text_input("Search query", value="RELIANCE")
exch = c2.selectbox("Exchange", ["NSE", "BSE", "NSE,BSE"])
records = c3.number_input("Max results", 1, 30, 10)
if st.button("🔍 Search", type="primary"):
with st.spinner("Searching…"):
resp = search_instrument(client, query, exchanges=exch, segments="EQ", records=records)
insts = resp.data or []
if not insts:
st.warning(f"No equity instruments found for '{query}' on {exch}.")
else:
df = pd.DataFrame([{
"Instrument Key": i.get("instrument_key", ""),
"Symbol": i.get("trading_symbol", ""),
"Name": i.get("name", ""),
"Exchange": i.get("exchange", ""),
"ISIN": i.get("isin", ""),
"Lot Size": i.get("lot_size", 1),
"Tick Size": i.get("tick_size", 0.05),
} for i in insts])
st.success(f"Found {len(df)} result(s)")
st.dataframe(df, use_container_width=True)
elif example == "Search Futures":
client = require_client()
c1, c2, c3 = st.columns([2, 1, 1])
query = c1.text_input("Search query", value="NIFTY")
exch = c2.selectbox("Exchange", ["NSE", "BSE", "MCX"])
exact = c3.checkbox("Exact underlying match", value=True,
help="Filter strictly by underlying_symbol to avoid e.g. NIFTYNXT50 when searching NIFTY")
if st.button("🔍 Search", type="primary"):
with st.spinner("Searching…"):
futures = get_futures_sorted(client, query, exchange=exch, exact_symbol=exact)
if not futures:
st.warning(f"No futures found for '{query}'.")
else:
df = pd.DataFrame([{
"Symbol": i.get("trading_symbol", ""),
"Underlying": i.get("underlying_symbol", ""),
"Expiry": i.get("expiry", ""),
"Lot Size": i.get("lot_size", ""),
"Exchange": i.get("exchange", ""),
"Key": i.get("instrument_key", ""),
} for i in futures])
st.success(f"Found {len(df)} contract(s)")
st.dataframe(df, use_container_width=True)
elif example == "Search Options":
client = require_client()
c1, c2, c3, c4 = st.columns([2, 1, 1, 1])
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
opt_type = c3.selectbox("Option type", ["CE,PE", "CE", "PE"])
strikes_each = c4.number_input("Strikes each side", 1, 15, 5)
if st.button("🔍 Fetch Options", type="primary"):
bar = st.progress(0)
insts = fetch_options_range(client, query, expiry, opt_type, strikes_each, bar)
bar.empty()
if not insts:
st.warning("No options found.")
else:
df = pd.DataFrame([{
"Symbol": i.get("trading_symbol", ""),
"Type": i.get("instrument_type", ""),
"Strike": i.get("strike_price", 0),
"Expiry": i.get("expiry", ""),
"Lot": i.get("lot_size", ""),
"Key": i.get("instrument_key", ""),
} for i in insts]).sort_values(["Strike", "Type"])
st.success(f"Found {len(df)} option(s)")
st.dataframe(df, use_container_width=True)
# ═════════════════════════════════════════════════════════════════════════════
# 📈 FUTURES & BASIS
# ═════════════════════════════════════════════════════════════════════════════
elif example == "NIFTY Futures Spread":
client = require_client()
if st.button("▶ Run", type="primary"):
with st.spinner("Fetching NIFTY futures…"):
futures = get_futures_sorted(client, "NIFTY", exchange="NSE", exact_symbol=True)
if len(futures) < 2:
st.error("Need at least 2 NIFTY futures contracts.")
st.stop()
near, far = futures[0], futures[1]
ltp_data = get_ltp(client, near["instrument_key"], far["instrument_key"])
near_q = ltp_data.get(near["instrument_key"])
far_q = ltp_data.get(far["instrument_key"])
near_ltp = lv(near_q); far_ltp = lv(far_q)
spread = far_ltp - near_ltp
spread_pct = (spread / near_ltp * 100) if near_ltp else 0
lot = near.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("Near Month LTP", f"₹{near_ltp:,.2f}", f"Close: {cv(near_q):,.2f}")
c2.metric("Far Month LTP", f"₹{far_ltp:,.2f}", f"Close: {cv(far_q):,.2f}")
c3.metric("Calendar Spread", f"₹{spread:+,.2f}", f"{spread_pct:+.2f}%")
st.divider()
st.dataframe(pd.DataFrame([
{"Contract": near["trading_symbol"], "Expiry": near["expiry"],
"LTP": near_ltp, "Prev Close": cv(near_q), "Volume": vv(near_q)},
{"Contract": far["trading_symbol"], "Expiry": far["expiry"],
"LTP": far_ltp, "Prev Close": cv(far_q), "Volume": vv(far_q)},
]), use_container_width=True)
st.info(contango_label(spread))
st.caption(f"Spread per lot ({lot} units): ₹{spread * lot:+,.2f}")
st.caption("Arbitrage: Buy near + Sell far if spread > cost-of-carry. Spread collapses at near-month expiry.")
elif example == "BankNifty Futures Spread":
client = require_client()
if st.button("▶ Run", type="primary"):
with st.spinner("Fetching BANKNIFTY futures…"):
futures = get_futures_sorted(client, "BANKNIFTY", exchange="NSE", exact_symbol=True)
if len(futures) < 2:
st.error("Need at least 2 BANKNIFTY contracts.")
st.stop()
near, far = futures[0], futures[1]
ltp_data = get_ltp(client, near["instrument_key"], far["instrument_key"])
near_q = ltp_data.get(near["instrument_key"])
far_q = ltp_data.get(far["instrument_key"])
near_ltp = lv(near_q); far_ltp = lv(far_q)
spread = far_ltp - near_ltp
spread_pct = (spread / near_ltp * 100) if near_ltp else 0
lot = near.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("Near Contract LTP", f"₹{near_ltp:,.2f}", f"Close: {cv(near_q):,.2f}")
c2.metric("Far Contract LTP", f"₹{far_ltp:,.2f}", f"Close: {cv(far_q):,.2f}")
c3.metric("Calendar Spread", f"₹{spread:+,.2f}", f"{spread_pct:+.2f}%")
st.divider()
st.dataframe(pd.DataFrame([
{"Contract": near["trading_symbol"], "Expiry": near["expiry"],
"LTP": near_ltp, "Prev Close": cv(near_q), "Volume": vv(near_q)},
{"Contract": far["trading_symbol"], "Expiry": far["expiry"],
"LTP": far_ltp, "Prev Close": cv(far_q), "Volume": vv(far_q)},
]), use_container_width=True)
st.info(contango_label(spread))
st.caption(f"Spread per lot ({lot} units): ₹{spread * lot:+,.2f}")
st.caption("BankNifty has weekly expiries — near/far may both be in the current month.")
elif example == "Cash-Futures Basis":
client = require_client()
underlying = st.selectbox("Underlying", ["NIFTY 50", "BANKNIFTY", "FINNIFTY", "MIDCPNIFTY"])
if st.button("▶ Run", type="primary"):
fut_q_map = {"NIFTY 50": "NIFTY", "BANKNIFTY": "BANKNIFTY",
"FINNIFTY": "FINNIFTY", "MIDCPNIFTY": "MIDCPNIFTY"}
fut_sym = fut_q_map[underlying]
with st.spinner("Fetching spot and futures…"):
spot_resp = search_instrument(client, underlying, exchanges="NSE",
segments="INDEX", instrument_types="INDEX", records=5)
spot_insts = spot_resp.data or []
spot_inst = next(
(i for i in spot_insts
if underlying.upper().replace(" ", "") in i.get("trading_symbol", "").upper().replace(" ", "")),
spot_insts[0] if spot_insts else None,
)
futures = get_futures_sorted(client, fut_sym, exchange="NSE", exact_symbol=True)
if not spot_inst:
st.error(f"Could not find index for '{underlying}'.")
st.stop()
if not futures:
st.error(f"No futures found for '{fut_sym}'.")
st.stop()
near = futures[0]
data = get_ltp(client, spot_inst["instrument_key"], near["instrument_key"])
spot_ltp = lv(data.get(spot_inst["instrument_key"]))
fut_ltp = lv(data.get(near["instrument_key"]))
basis = fut_ltp - spot_ltp
basis_pct = (basis / spot_ltp * 100) if spot_ltp else 0
d = dte(near.get("expiry", ""))
ann = (basis_pct / d * 365) if d else 0
c1, c2, c3, c4 = st.columns(4)
c1.metric("Spot (Index)", f"₹{spot_ltp:,.2f}")
c2.metric("Futures (Near)", f"₹{fut_ltp:,.2f}")
c3.metric("Basis (Fut − Spot)", f"₹{basis:+,.2f}", f"{basis_pct:+.2f}%")
c4.metric("Annualised Carry", f"{ann:+.2f}% p.a.", f"{d} DTE")
st.divider()
if basis > 0:
st.success("🟢 Futures at premium — positive carry (interest rate > dividend yield).")
else:
st.warning("🔴 Futures at discount — dividend yield > cost of carry, or bearish sentiment.")
elif example == "Futures Roll Cost":
client = require_client()
c1, c2 = st.columns(2)
query = c1.text_input("Underlying", value="NIFTY")
side = c2.selectbox("Position side", ["long", "short"])
if st.button("▶ Run", type="primary"):
with st.spinner("Fetching futures…"):
futures = get_futures_sorted(client, query, exchange="NSE", exact_symbol=True)
if len(futures) < 2:
st.error("Need at least 2 contracts.")
st.stop()
near, far = futures[0], futures[1]
data = get_ltp(client, near["instrument_key"], far["instrument_key"])
near_ltp = lv(data.get(near["instrument_key"]))
far_ltp = lv(data.get(far["instrument_key"]))
roll = (far_ltp - near_ltp) if side == "long" else (near_ltp - far_ltp)
roll_pct = (roll / near_ltp * 100) if near_ltp else 0
lot = near.get("lot_size", 1)
try:
d1 = datetime.strptime(near["expiry"], "%Y-%m-%d").date()
d2 = datetime.strptime(far["expiry"], "%Y-%m-%d").date()
gap = abs((d2 - d1).days)
dte_near = (d1 - date.today()).days
except Exception:
gap = 30; dte_near = 15
ann = (roll_pct / gap * 365) if gap else 0
c1, c2, c3 = st.columns(3)
c1.metric("Roll Cost (pts)", f"{roll:+.2f}")
c2.metric("Roll Cost (%)", f"{roll_pct:+.2f}%")
c3.metric("Annualised Rate", f"{ann:+.2f}% p.a.")
st.divider()
action_near = "Close" if side == "long" else "Open"
action_far = "Open" if side == "long" else "Close"
st.dataframe(pd.DataFrame([
{"Action": action_near, "Contract": near["trading_symbol"], "Expiry": near["expiry"], "LTP": near_ltp},
{"Action": action_far, "Contract": far["trading_symbol"], "Expiry": far["expiry"], "LTP": far_ltp},
]), use_container_width=True)
st.caption(f"Roll cost per lot: ₹{roll * lot:+,.2f} | Days between expiries: {gap} | DTE near: {dte_near}")
elif example == "MCX Crude Spread":
client = require_client()
query = st.text_input("Commodity symbol", value="CRUDEOIL",
help="e.g. CRUDEOIL, NATURALGAS, GOLD, SILVER")
if st.button("▶ Run", type="primary"):
with st.spinner("Fetching MCX futures…"):
futures = get_futures_sorted(client, query, exchange="MCX", exact_symbol=False, segment="COMM")
if len(futures) < 2:
st.error(f"Need at least 2 futures for '{query}'. Try CRUDEOIL or NATURALGAS.")
st.stop()
near, far = futures[0], futures[1]
data = get_ltp(client, near["instrument_key"], far["instrument_key"])
near_ltp = lv(data.get(near["instrument_key"]))
far_ltp = lv(data.get(far["instrument_key"]))
spread = far_ltp - near_ltp
spread_pct = (spread / near_ltp * 100) if near_ltp else 0
lot = near.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("Near LTP", f"₹{near_ltp:,.2f}", f"Close: {cv(data.get(near['instrument_key'])):,.2f}")
c2.metric("Far LTP", f"₹{far_ltp:,.2f}", f"Close: {cv(data.get(far['instrument_key'])):,.2f}")
c3.metric("Spread", f"₹{spread:+,.2f}", f"{spread_pct:+.2f}%")
st.divider()
st.dataframe(pd.DataFrame([
{"Contract": near["trading_symbol"], "Expiry": near["expiry"], "LTP": near_ltp},
{"Contract": far["trading_symbol"], "Expiry": far["expiry"], "LTP": far_ltp},
]), use_container_width=True)
st.info(contango_label(spread))
st.caption(f"Spread per lot ({lot} units): ₹{spread * lot:+,.2f}")
# ═════════════════════════════════════════════════════════════════════════════
# 🎯 OPTIONS STRATEGIES
# ═════════════════════════════════════════════════════════════════════════════
elif example == "Straddle Pricer":
client = require_client()
c1, c2 = st.columns(2)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
if st.button("▶ Price Straddle", type="primary"):
with st.spinner("Fetching ATM options…"):
ce = fetch_one(client, query, expiry, "CE", 0)
pe = fetch_one(client, query, expiry, "PE", 0)
if not ce or not pe:
st.error("Could not find ATM options.")
st.stop()
data = get_ltp(client, ce["instrument_key"], pe["instrument_key"])
ce_ltp = lv(data.get(ce["instrument_key"]))
pe_ltp = lv(data.get(pe["instrument_key"]))
strike = ce.get("strike_price", 0)
premium = ce_ltp + pe_ltp
upper_be = strike + premium
lower_be = strike - premium
lot = ce.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("ATM Strike", f"₹{strike:,.0f}")
c2.metric("Total Premium (CE+PE)", f"₹{premium:,.2f}")
c3.metric("Max Profit (seller)", f"₹{premium * lot:,.2f} / lot")
st.divider()
c1, c2 = st.columns(2)
c1.metric("Upper Breakeven", f"₹{upper_be:,.2f}", f"+{premium:,.2f}")
c2.metric("Lower Breakeven", f"₹{lower_be:,.2f}", f"-{premium:,.2f}")
st.dataframe(pd.DataFrame([
{"Leg": "Buy CE (ATM)", "Strike": strike, "Expiry": ce["expiry"], "LTP": ce_ltp, "Symbol": ce["trading_symbol"]},
{"Leg": "Buy PE (ATM)", "Strike": strike, "Expiry": pe["expiry"], "LTP": pe_ltp, "Symbol": pe["trading_symbol"]},
]), use_container_width=True)
st.caption(f"Buyer profits if underlying moves > ₹{premium:.2f} in either direction.")
st.caption(f"Seller max profit ₹{premium * lot:,.2f}/lot if underlying stays within ₹{lower_be:,.2f}–₹{upper_be:,.2f}.")
elif example == "Strangle Pricer":
client = require_client()
c1, c2, c3 = st.columns(3)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
otm_offset = c3.number_input("OTM offset (strikes)", 1, 10, 2)
if st.button("▶ Price Strangle", type="primary"):
with st.spinner("Fetching OTM options…"):
ce = fetch_one(client, query, expiry, "CE", +otm_offset)
pe = fetch_one(client, query, expiry, "PE", -otm_offset)
if not ce or not pe:
st.error("Could not find OTM options.")
st.stop()
data = get_ltp(client, ce["instrument_key"], pe["instrument_key"])
ce_ltp = lv(data.get(ce["instrument_key"]))
pe_ltp = lv(data.get(pe["instrument_key"]))
ce_strike = ce.get("strike_price", 0)
pe_strike = pe.get("strike_price", 0)
premium = ce_ltp + pe_ltp
lot = ce.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric(f"CE Strike (+{otm_offset})", f"₹{ce_strike:,.0f}", f"LTP: {ce_ltp:.2f}")
c2.metric(f"PE Strike (-{otm_offset})", f"₹{pe_strike:,.0f}", f"LTP: {pe_ltp:.2f}")
c3.metric("Total Premium", f"₹{premium:,.2f}")
st.divider()
c1, c2 = st.columns(2)
c1.metric("Upper Breakeven", f"₹{ce_strike + premium:,.2f}")
c2.metric("Lower Breakeven", f"₹{pe_strike - premium:,.2f}")
st.dataframe(pd.DataFrame([
{"Leg": f"Buy CE +{otm_offset}", "Strike": ce_strike, "LTP": ce_ltp, "Symbol": ce["trading_symbol"]},
{"Leg": f"Buy PE -{otm_offset}", "Strike": pe_strike, "LTP": pe_ltp, "Symbol": pe["trading_symbol"]},
]), use_container_width=True)
st.caption(f"Max loss per lot: ₹{premium * lot:,.2f} (if underlying stays between strikes).")
elif example == "Bull Call Spread":
client = require_client()
c1, c2, c3 = st.columns(3)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
spread_width = c3.number_input("Spread width (strikes)", 1, 10, 2)
if st.button("▶ Price Bull Call Spread", type="primary"):
with st.spinner("Fetching options…"):
buy_ce = fetch_one(client, query, expiry, "CE", 0)
sell_ce = fetch_one(client, query, expiry, "CE", +spread_width)
if not buy_ce or not sell_ce:
st.error("Could not fetch options.")
st.stop()
data = get_ltp(client, buy_ce["instrument_key"], sell_ce["instrument_key"])
buy_ltp = lv(data.get(buy_ce["instrument_key"]))
sell_ltp = lv(data.get(sell_ce["instrument_key"]))
buy_k = buy_ce.get("strike_price", 0)
sell_k = sell_ce.get("strike_price", 0)
debit = buy_ltp - sell_ltp
max_prof = (sell_k - buy_k) - debit
lot = buy_ce.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("Net Debit", f"₹{debit:,.2f}", "Cost per unit")
c2.metric("Max Profit", f"₹{max_prof:,.2f}", f"₹{max_prof * lot:,.2f}/lot")
c3.metric("Breakeven", f"₹{buy_k + debit:,.2f}")
st.divider()
st.dataframe(pd.DataFrame([
{"Leg": "Buy CE (ATM)", "Strike": buy_k, "LTP": buy_ltp, "Symbol": buy_ce["trading_symbol"]},
{"Leg": f"Sell CE (+{spread_width})", "Strike": sell_k, "LTP": sell_ltp, "Symbol": sell_ce["trading_symbol"]},
]), use_container_width=True)
st.caption(f"Max loss: ₹{debit:.2f}/unit if spot < {buy_k:,.0f} at expiry.")
st.caption(f"Max profit: ₹{max_prof:.2f}/unit if spot > {sell_k:,.0f} at expiry.")
elif example == "Iron Condor":
client = require_client()
c1, c2, c3 = st.columns(3)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
short_offset = c3.number_input("Short leg offset", 1, 10, 2,
help="Strikes from ATM for the sold legs")
long_offset = short_offset + 2
if st.button("▶ Price Iron Condor", type="primary"):
with st.spinner("Fetching 4 legs…"):
sell_ce = fetch_one(client, query, expiry, "CE", +short_offset)
buy_ce = fetch_one(client, query, expiry, "CE", +long_offset)
sell_pe = fetch_one(client, query, expiry, "PE", -short_offset)
buy_pe = fetch_one(client, query, expiry, "PE", -long_offset)
legs = [l for l in [sell_ce, buy_ce, sell_pe, buy_pe] if l]
if len(legs) < 4:
st.error("Could not fetch all 4 legs.")
st.stop()
data = get_ltp(client, *[l["instrument_key"] for l in legs])
sell_ce_ltp = lv(data.get(sell_ce["instrument_key"]))
buy_ce_ltp = lv(data.get(buy_ce["instrument_key"]))
sell_pe_ltp = lv(data.get(sell_pe["instrument_key"]))
buy_pe_ltp = lv(data.get(buy_pe["instrument_key"]))
net_credit = (sell_ce_ltp + sell_pe_ltp) - (buy_ce_ltp + buy_pe_ltp)
wing_width = buy_ce["strike_price"] - sell_ce["strike_price"]
max_loss = wing_width - net_credit
lot = sell_ce.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("Net Credit", f"₹{net_credit:,.2f}", f"₹{net_credit * lot:,.2f}/lot")
c2.metric("Max Loss", f"₹{max_loss:,.2f}", f"₹{max_loss * lot:,.2f}/lot")
c3.metric("Upper / Lower BE",
f"{sell_ce['strike_price'] + net_credit:,.0f} / {sell_pe['strike_price'] - net_credit:,.0f}")
st.divider()
st.dataframe(pd.DataFrame([
{"Leg": f"Sell CE +{short_offset}", "Strike": sell_ce["strike_price"], "LTP": sell_ce_ltp, "Symbol": sell_ce["trading_symbol"]},
{"Leg": f"Buy CE +{long_offset}", "Strike": buy_ce["strike_price"], "LTP": buy_ce_ltp, "Symbol": buy_ce["trading_symbol"]},
{"Leg": f"Sell PE -{short_offset}", "Strike": sell_pe["strike_price"], "LTP": sell_pe_ltp, "Symbol": sell_pe["trading_symbol"]},
{"Leg": f"Buy PE -{long_offset}", "Strike": buy_pe["strike_price"], "LTP": buy_pe_ltp, "Symbol": buy_pe["trading_symbol"]},
]), use_container_width=True)
elif example == "Butterfly Spread":
client = require_client()
c1, c2 = st.columns(2)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
if st.button("▶ Price Butterfly", type="primary"):
with st.spinner("Fetching 3 legs…"):
lower_ce = fetch_one(client, query, expiry, "CE", -1)
atm_ce = fetch_one(client, query, expiry, "CE", 0)
upper_ce = fetch_one(client, query, expiry, "CE", +1)
if not all([lower_ce, atm_ce, upper_ce]):
st.error("Could not fetch all legs.")
st.stop()
data = get_ltp(client, lower_ce["instrument_key"], atm_ce["instrument_key"], upper_ce["instrument_key"])
lower_ltp = lv(data.get(lower_ce["instrument_key"]))
atm_ltp = lv(data.get(atm_ce["instrument_key"]))
upper_ltp = lv(data.get(upper_ce["instrument_key"]))
net_debit = lower_ltp - 2 * atm_ltp + upper_ltp
wing_width = atm_ce["strike_price"] - lower_ce["strike_price"]
max_profit = wing_width - net_debit
lot = atm_ce.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric("Net Debit", f"₹{net_debit:,.2f}")
c2.metric("Max Profit", f"₹{max_profit:,.2f}", f"at {atm_ce['strike_price']:,.0f}")
c3.metric("Max Loss", f"₹{net_debit:,.2f}", "at both wings")
st.divider()
st.dataframe(pd.DataFrame([
{"Leg": "Buy CE (-1)", "Strike": lower_ce["strike_price"], "Qty": "+1", "LTP": lower_ltp},
{"Leg": "Sell CE (ATM)","Strike": atm_ce["strike_price"], "Qty": "-2", "LTP": atm_ltp},
{"Leg": "Buy CE (+1)", "Strike": upper_ce["strike_price"], "Qty": "+1", "LTP": upper_ltp},
]), use_container_width=True)
st.caption(f"Max profit per lot: ₹{max_profit * lot:,.2f}. Max loss per lot: ₹{net_debit * lot:,.2f}.")
elif example == "Calendar Spread":
client = require_client()
c1, c2 = st.columns(2)
query = c1.text_input("Underlying", value="NIFTY")
opt_type = c2.selectbox("Option type", ["CE", "PE"])
if st.button("▶ Price Calendar Spread", type="primary"):
with st.spinner("Fetching near + far month options…"):
near_opt = fetch_one(client, query, "current_month", opt_type, 0)
far_opt = fetch_one(client, query, "next_month", opt_type, 0)
if not near_opt or not far_opt:
st.error("Could not find options for both expiries.")
st.stop()
data = get_ltp(client, near_opt["instrument_key"], far_opt["instrument_key"])
near_ltp = lv(data.get(near_opt["instrument_key"]))
far_ltp = lv(data.get(far_opt["instrument_key"]))
net_debit = far_ltp - near_ltp
lot = near_opt.get("lot_size", 1)
c1, c2, c3 = st.columns(3)
c1.metric(f"Near {opt_type}", f"₹{near_ltp:,.2f}", near_opt["expiry"])
c2.metric(f"Far {opt_type}", f"₹{far_ltp:,.2f}", far_opt["expiry"])
c3.metric("Net Debit", f"₹{net_debit:,.2f}", f"₹{net_debit * lot:,.2f}/lot")
st.divider()
st.dataframe(pd.DataFrame([
{"Leg": f"Sell {opt_type} (Near)", "Strike": near_opt["strike_price"],
"Expiry": near_opt["expiry"], "LTP": near_ltp, "Symbol": near_opt["trading_symbol"]},
{"Leg": f"Buy {opt_type} (Far)", "Strike": far_opt["strike_price"],
"Expiry": far_opt["expiry"], "LTP": far_ltp, "Symbol": far_opt["trading_symbol"]},
]), use_container_width=True)
st.caption("Strategy profits from faster time-decay of the near-month leg.")
elif example == "Put-Call Parity":
client = require_client()
c1, c2 = st.columns(2)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
if st.button("▶ Check Parity", type="primary"):
with st.spinner("Fetching options + futures…"):
ce = fetch_one(client, query, expiry, "CE", 0)
pe = fetch_one(client, query, expiry, "PE", 0)
futures = get_futures_sorted(client, query, exchange="NSE", exact_symbol=True)
if not ce or not pe:
st.error("Could not find ATM options.")
st.stop()
if not futures:
st.error("Could not find futures.")
st.stop()
data = get_ltp(client, ce["instrument_key"], pe["instrument_key"], futures[0]["instrument_key"])
ce_ltp = lv(data.get(ce["instrument_key"]))
pe_ltp = lv(data.get(pe["instrument_key"]))
fut_ltp = lv(data.get(futures[0]["instrument_key"]))
strike = ce.get("strike_price", 0)
lhs = ce_ltp - pe_ltp # C − P
rhs = fut_ltp - strike # F − K
dev = lhs - rhs
dev_pct = (dev / strike * 100) if strike else 0
c1, c2, c3 = st.columns(3)
c1.metric("CE − PE (LHS)", f"₹{lhs:+.2f}")
c2.metric("Futures − Strike (RHS)", f"₹{rhs:+.2f}")
c3.metric("Parity Deviation", f"₹{dev:+.2f}", f"{dev_pct:+.4f}%")
st.divider()
if abs(dev) < 2:
st.success("✅ Parity holds — no actionable arbitrage after transaction costs.")
else:
st.warning(f"⚠️ Deviation of ₹{dev:+.2f} detected. May be arbitrageable if spread > transaction costs.")
st.dataframe(pd.DataFrame([
{"Item": "CE (ATM)", "Strike": strike, "LTP": ce_ltp, "Symbol": ce["trading_symbol"]},
{"Item": "PE (ATM)", "Strike": strike, "LTP": pe_ltp, "Symbol": pe["trading_symbol"]},
{"Item": "Futures (Near)", "Strike": "—", "LTP": fut_ltp, "Symbol": futures[0]["trading_symbol"]},
]), use_container_width=True)
# ═════════════════════════════════════════════════════════════════════════════
# 📊 OPTIONS ANALYTICS
# ═════════════════════════════════════════════════════════════════════════════
elif example == "Options Chain Builder":
client = require_client()
c1, c2, c3 = st.columns(3)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
strikes_each = c3.number_input("Strikes each side of ATM", 1, 15, 5)
if st.button("▶ Build Chain", type="primary"):
bar = st.progress(0, text="Fetching chain…")
offsets = list(range(-strikes_each, strikes_each + 1))
ce_map, pe_map = {}, {}
for i, offset in enumerate(offsets):
for itype, store in [("CE", ce_map), ("PE", pe_map)]:
resp = search_instrument(client, query, exchanges="NSE", segments="FO",
instrument_types=itype, expiry=expiry,
atm_offset=offset, records=1)
data = resp.data or []
if data:
inst = data[0]
store[inst.get("strike_price", 0)] = inst
bar.progress((i + 1) / len(offsets), text=f"Offset {offset}…")
bar.empty()
all_strikes = sorted(set(list(ce_map.keys()) + list(pe_map.keys())))
all_keys = []
for k in all_strikes:
if k in ce_map: all_keys.append(ce_map[k]["instrument_key"])
if k in pe_map: all_keys.append(pe_map[k]["instrument_key"])
ltp_data = get_ltp(client, *all_keys)
atm_resp = search_instrument(client, query, exchanges="NSE", segments="FO",
instrument_types="CE", expiry=expiry, atm_offset=0, records=1)
atm_strike = (atm_resp.data or [{}])[0].get("strike_price", 0)
rows = []
for strike in reversed(all_strikes):
ce_inst = ce_map.get(strike)
pe_inst = pe_map.get(strike)
rows.append({
"CE LTP": lv(ltp_data.get(ce_inst["instrument_key"])) if ce_inst else "—",
"Strike": strike,
"PE LTP": lv(ltp_data.get(pe_inst["instrument_key"])) if pe_inst else "—",
"ATM": "◀ ATM" if strike == atm_strike else "",
})
df = pd.DataFrame(rows)
def highlight_atm(row):
return (["background-color: #fff3cd"] * len(row)
if row["ATM"] == "◀ ATM" else [""] * len(row))
st.dataframe(df.style.apply(highlight_atm, axis=1), use_container_width=True)
elif example == "Max Pain Calculator":
client = require_client()
c1, c2, c3 = st.columns(3)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
strikes_each = c3.number_input("Strikes each side", 3, 15, 8)
if st.button("▶ Calculate Max Pain", type="primary"):
bar = st.progress(0, text="Fetching OI data…")
ce_insts, pe_insts = [], []
total = strikes_each * 2 + 1
for i, offset in enumerate(range(-strikes_each, strikes_each + 1)):
for itype, store in [("CE", ce_insts), ("PE", pe_insts)]:
resp = search_instrument(client, query, exchanges="NSE", segments="FO",
instrument_types=itype, expiry=expiry,
atm_offset=offset, records=1)
d = resp.data or []
if d: store.append(d[0])
bar.progress((i + 1) / total)
def dedup(insts):
seen, unique = set(), []
for inst in insts:
k = inst.get("strike_price", 0)
if k not in seen: seen.add(k); unique.append(inst)
return unique
ce_insts = dedup(ce_insts)
pe_insts = dedup(pe_insts)
all_keys = [i["instrument_key"] for i in ce_insts + pe_insts]
quotes = get_full_quote(client, *all_keys)
bar.empty()
ce_oi = {i["strike_price"]: ov(quotes.get(i["instrument_key"])) for i in ce_insts}
pe_oi = {i["strike_price"]: ov(quotes.get(i["instrument_key"])) for i in pe_insts}
all_s = sorted(set(list(ce_oi) + list(pe_oi)))
pain = {}
for candidate in all_s:
pain[candidate] = (
sum(max(0, candidate - s) * (o or 0) for s, o in ce_oi.items()) +
sum(max(0, s - candidate) * (o or 0) for s, o in pe_oi.items())
)
max_pain_strike = min(pain, key=pain.get)
st.metric("🎯 Max Pain Strike", f"₹{max_pain_strike:,.0f}")
df = pd.DataFrame([{
"Strike": s,
"CE OI": ce_oi.get(s, 0),
"PE OI": pe_oi.get(s, 0),
"Pain Value": pain.get(s, 0),
"": "🎯 MAX PAIN" if s == max_pain_strike else "",
} for s in reversed(all_s)])
fig = px.bar(df.sort_values("Strike"), x="Strike", y="Pain Value",
title="Pain Value by Strike (lower = max pain)",
color="Pain Value", color_continuous_scale="RdYlGn_r")
fig.add_vline(x=max_pain_strike, line_dash="dash", line_color="red",
annotation_text="Max Pain")
st.plotly_chart(fig, use_container_width=True)
st.dataframe(df, use_container_width=True)
st.caption("Interpretation: underlying tends to gravitate toward max pain at expiry.")
elif example == "OI Skew":
client = require_client()
c1, c2, c3 = st.columns(3)
query = c1.text_input("Underlying", value="NIFTY")
expiry = c2.selectbox("Expiry", ["current_month", "current_week", "next_month"])
strikes_each = c3.number_input("Strikes each side", 3, 12, 7)
if st.button("▶ Analyse OI Skew", type="primary"):
bar = st.progress(0)
ce_insts, pe_insts = [], []
total = strikes_each * 2 + 1
for i, offset in enumerate(range(-strikes_each, strikes_each + 1)):
for itype, store in [("CE", ce_insts), ("PE", pe_insts)]:
resp = search_instrument(client, query, exchanges="NSE", segments="FO",
instrument_types=itype, expiry=expiry,
atm_offset=offset, records=1)
d = resp.data or []
if d: store.append(d[0])
bar.progress((i + 1) / total)
def dedup(insts):
seen, unique = set(), []
for inst in insts:
k = inst.get("strike_price", 0)
if k not in seen: seen.add(k); unique.append(inst)
return unique
ce_insts = dedup(ce_insts); pe_insts = dedup(pe_insts)
all_keys = [i["instrument_key"] for i in ce_insts + pe_insts]
quotes = get_full_quote(client, *all_keys)
bar.empty()
ce_oi = {i["strike_price"]: ov(quotes.get(i["instrument_key"])) for i in ce_insts}
pe_oi = {i["strike_price"]: ov(quotes.get(i["instrument_key"])) for i in pe_insts}
all_s = sorted(set(list(ce_oi) + list(pe_oi)))
total_ce = sum(ce_oi.values()); total_pe = sum(pe_oi.values())
pcr = total_pe / total_ce if total_ce else 0
c1, c2, c3 = st.columns(3)
c1.metric("Total CE OI", f"{total_ce:,.0f}")
c2.metric("Total PE OI", f"{total_pe:,.0f}")
c3.metric("Overall PCR", f"{pcr:.2f}", ">1.2 bullish | <0.8 bearish")
df = pd.DataFrame([{
"Strike": s,
"CE OI": ce_oi.get(s, 0),
"PE OI": pe_oi.get(s, 0),
"PCR": round(pe_oi.get(s, 0) / ce_oi.get(s, 1), 2) if ce_oi.get(s) else 0,
} for s in all_s])
fig = go.Figure()
fig.add_trace(go.Bar(name="CE OI", x=df["Strike"].astype(str), y=df["CE OI"], marker_color="#e74c3c"))
fig.add_trace(go.Bar(name="PE OI", x=df["Strike"].astype(str), y=df["PE OI"], marker_color="#27ae60"))
fig.update_layout(barmode="group", title="CE vs PE Open Interest by Strike")
st.plotly_chart(fig, use_container_width=True)
max_ce = max(ce_oi, key=ce_oi.get) if ce_oi else 0