nubra-ref-data 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akshay N
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: nubra_ref_data
3
+ Version: 0.1.0
4
+ Summary: Reference-data helpers for Nubra InstrumentData option, future, and underlying DataFrames
5
+ Author: Akshay N
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/akshayn-spec/nubra_ref_data
8
+ Project-URL: Repository, https://github.com/akshayn-spec/nubra_ref_data
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pandas>=2.0
13
+ Dynamic: license-file
14
+
15
+ # nubra_ref_data
16
+
17
+ `nubra_ref_data` is a small helper package for Nubra `InstrumentData` users who want filtered pandas DataFrames for:
18
+
19
+ - the underlying row
20
+ - futures rows
21
+ - option rows by expiry bucket and strike levels
22
+ - one combined DataFrame containing all of the above
23
+
24
+ It is designed to work with:
25
+
26
+ ```python
27
+ from nubra_python_sdk.refdata.instruments import InstrumentData
28
+
29
+ instruments = InstrumentData(nubra)
30
+ ```
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install nubra_ref_data
36
+ ```
37
+
38
+ ## Functions
39
+
40
+ - `underlying_data(instruments, underlying, exchange="NSE")`
41
+ - `futures_data(instruments, underlying, exchange="NSE")`
42
+ - `options_data(instruments, underlying, exchange="NSE", expiry_bucket="week0", levels=10, option_side="BOTH")`
43
+ - `all_data(instruments, underlying, exchange="NSE", expiry_bucket="week0", levels=10, option_side="BOTH")`
44
+
45
+ ## Example
46
+
47
+ ```python
48
+ from nubra_python_sdk.start_sdk import InitNubraSdk, NubraEnv
49
+ from nubra_python_sdk.refdata.instruments import InstrumentData
50
+
51
+ from nubra_ref_data import all_data, options_data
52
+
53
+
54
+ nubra = InitNubraSdk(NubraEnv.UAT, env_creds=True)
55
+ instruments = InstrumentData(nubra)
56
+
57
+ df_all = all_data(
58
+ instruments=instruments,
59
+ underlying="NIFTY",
60
+ exchange="NSE",
61
+ expiry_bucket="week0",
62
+ levels=8,
63
+ option_side="BOTH",
64
+ )
65
+
66
+ df_ce = options_data(
67
+ instruments=instruments,
68
+ underlying="NIFTY",
69
+ exchange="NSE",
70
+ expiry_bucket="month",
71
+ levels=5,
72
+ option_side="CE",
73
+ )
74
+
75
+ df_sensex = all_data(
76
+ instruments=instruments,
77
+ underlying="SENSEX",
78
+ exchange="BSE",
79
+ expiry_bucket="week0",
80
+ levels=6,
81
+ option_side="BOTH",
82
+ )
83
+ ```
84
+
85
+ ## Behavior
86
+
87
+ - `week0`, `week1`, `week2`, `week3`, and `week4` resolve against the sorted available option expiries for that underlying.
88
+ - `month` resolves to the first month-end expiry available in the option data.
89
+ - `levels` picks the nearest strikes using `underlying_prev_close`.
90
+ - `option_side="BOTH"` only selects strikes where both `CE` and `PE` exist.
91
+ - `all_data()` returns rows in this order: underlying, futures, then options.
92
+ - If the underlying cash/index row is missing from the instruments master, a placeholder `UNDERLYING` row is added.
93
+ - The returned DataFrames preserve the original instrument columns only. No helper columns are added.
@@ -0,0 +1,79 @@
1
+ # nubra_ref_data
2
+
3
+ `nubra_ref_data` is a small helper package for Nubra `InstrumentData` users who want filtered pandas DataFrames for:
4
+
5
+ - the underlying row
6
+ - futures rows
7
+ - option rows by expiry bucket and strike levels
8
+ - one combined DataFrame containing all of the above
9
+
10
+ It is designed to work with:
11
+
12
+ ```python
13
+ from nubra_python_sdk.refdata.instruments import InstrumentData
14
+
15
+ instruments = InstrumentData(nubra)
16
+ ```
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install nubra_ref_data
22
+ ```
23
+
24
+ ## Functions
25
+
26
+ - `underlying_data(instruments, underlying, exchange="NSE")`
27
+ - `futures_data(instruments, underlying, exchange="NSE")`
28
+ - `options_data(instruments, underlying, exchange="NSE", expiry_bucket="week0", levels=10, option_side="BOTH")`
29
+ - `all_data(instruments, underlying, exchange="NSE", expiry_bucket="week0", levels=10, option_side="BOTH")`
30
+
31
+ ## Example
32
+
33
+ ```python
34
+ from nubra_python_sdk.start_sdk import InitNubraSdk, NubraEnv
35
+ from nubra_python_sdk.refdata.instruments import InstrumentData
36
+
37
+ from nubra_ref_data import all_data, options_data
38
+
39
+
40
+ nubra = InitNubraSdk(NubraEnv.UAT, env_creds=True)
41
+ instruments = InstrumentData(nubra)
42
+
43
+ df_all = all_data(
44
+ instruments=instruments,
45
+ underlying="NIFTY",
46
+ exchange="NSE",
47
+ expiry_bucket="week0",
48
+ levels=8,
49
+ option_side="BOTH",
50
+ )
51
+
52
+ df_ce = options_data(
53
+ instruments=instruments,
54
+ underlying="NIFTY",
55
+ exchange="NSE",
56
+ expiry_bucket="month",
57
+ levels=5,
58
+ option_side="CE",
59
+ )
60
+
61
+ df_sensex = all_data(
62
+ instruments=instruments,
63
+ underlying="SENSEX",
64
+ exchange="BSE",
65
+ expiry_bucket="week0",
66
+ levels=6,
67
+ option_side="BOTH",
68
+ )
69
+ ```
70
+
71
+ ## Behavior
72
+
73
+ - `week0`, `week1`, `week2`, `week3`, and `week4` resolve against the sorted available option expiries for that underlying.
74
+ - `month` resolves to the first month-end expiry available in the option data.
75
+ - `levels` picks the nearest strikes using `underlying_prev_close`.
76
+ - `option_side="BOTH"` only selects strikes where both `CE` and `PE` exist.
77
+ - `all_data()` returns rows in this order: underlying, futures, then options.
78
+ - If the underlying cash/index row is missing from the instruments master, a placeholder `UNDERLYING` row is added.
79
+ - The returned DataFrames preserve the original instrument columns only. No helper columns are added.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nubra_ref_data"
7
+ version = "0.1.0"
8
+ description = "Reference-data helpers for Nubra InstrumentData option, future, and underlying DataFrames"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "Akshay N" }]
13
+ dependencies = [
14
+ "pandas>=2.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/akshayn-spec/nubra_ref_data"
19
+ Repository = "https://github.com/akshayn-spec/nubra_ref_data"
20
+
21
+ [tool.setuptools]
22
+ package-dir = { "" = "src" }
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .refdata import all_data, futures_data, options_data, underlying_data
2
+
3
+ __all__ = ["underlying_data", "futures_data", "options_data", "all_data"]
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import re
5
+ from typing import Any
6
+
7
+ import pandas as pd
8
+
9
+
10
+ WEEK_PATTERN = re.compile(r"^week(?P<index>\d+)$")
11
+ OPTION_TYPES = {"CE", "PE"}
12
+ BASE_COLUMNS = [
13
+ "stock_name",
14
+ "ref_id",
15
+ "exchange",
16
+ "asset",
17
+ "asset_type",
18
+ "derivative_type",
19
+ "expiry",
20
+ "strike_price",
21
+ "option_type",
22
+ "lot_size",
23
+ "tick_size",
24
+ "token",
25
+ "nubra_name",
26
+ "isin",
27
+ "underlying_prev_close",
28
+ ]
29
+ @dataclass(frozen=True)
30
+ class ResolvedExpiry:
31
+ bucket: str
32
+ expiry: int
33
+
34
+
35
+ def _normalize_text(value: Any) -> str:
36
+ if value is None:
37
+ return ""
38
+ return str(value).strip().upper()
39
+
40
+
41
+ def _to_numeric(series: pd.Series) -> pd.Series:
42
+ return pd.to_numeric(series, errors="coerce")
43
+
44
+
45
+ def _normalize_option_side(option_side: str | None) -> str:
46
+ side = _normalize_text(option_side or "BOTH")
47
+ aliases = {
48
+ "BOTH": "BOTH",
49
+ "ALL": "BOTH",
50
+ "CE": "CE",
51
+ "CALL": "CE",
52
+ "CALLS": "CE",
53
+ "PE": "PE",
54
+ "PUT": "PE",
55
+ "PUTS": "PE",
56
+ }
57
+ normalized = aliases.get(side)
58
+ if normalized is None:
59
+ raise ValueError("option_side must be one of BOTH, CE, PE, CALL, or PUT.")
60
+ return normalized
61
+
62
+
63
+ def _ensure_dataframe(instruments: Any, exchange: str) -> pd.DataFrame:
64
+ if not hasattr(instruments, "get_instruments_dataframe"):
65
+ raise TypeError("instruments must expose get_instruments_dataframe(exchange=...).")
66
+
67
+ df = instruments.get_instruments_dataframe(exchange=exchange)
68
+ if not isinstance(df, pd.DataFrame):
69
+ raise TypeError("get_instruments_dataframe(exchange=...) must return a pandas DataFrame.")
70
+
71
+ missing = [column for column in BASE_COLUMNS if column not in df.columns]
72
+ if missing:
73
+ raise ValueError(f"Instrument DataFrame is missing required columns: {missing}")
74
+
75
+ df = df.copy()
76
+ df["exchange"] = df["exchange"].astype(str).str.upper()
77
+ df["asset"] = df["asset"].astype(str).str.upper()
78
+ df["derivative_type"] = df["derivative_type"].astype(str).str.upper()
79
+ df["option_type"] = df["option_type"].astype(str).str.upper()
80
+ df["stock_name"] = df["stock_name"].astype(str)
81
+ df["expiry"] = _to_numeric(df["expiry"]).astype("Int64")
82
+ df["strike_price"] = _to_numeric(df["strike_price"])
83
+ df["underlying_prev_close"] = _to_numeric(df["underlying_prev_close"])
84
+ return df
85
+
86
+
87
+ def _asset_dataframe(instruments: Any, underlying: str, exchange: str) -> pd.DataFrame:
88
+ exchange_code = _normalize_text(exchange)
89
+ underlying_name = _normalize_text(underlying)
90
+ df = _ensure_dataframe(instruments, exchange=exchange_code)
91
+ asset_df = df[(df["exchange"] == exchange_code) & (df["asset"] == underlying_name)].copy()
92
+ if asset_df.empty:
93
+ raise ValueError(f"No instruments found for underlying={underlying_name!r} on exchange={exchange_code!r}.")
94
+ return asset_df
95
+
96
+
97
+ def _resolve_expiry_bucket(option_expiries: list[int], expiry_bucket: str) -> ResolvedExpiry:
98
+ if not option_expiries:
99
+ raise ValueError("No option expiries are available for the selected underlying and exchange.")
100
+
101
+ bucket = _normalize_text(expiry_bucket)
102
+ if bucket == "MONTH":
103
+ by_month: dict[tuple[int, int], list[int]] = {}
104
+ for expiry in option_expiries:
105
+ expiry_str = str(expiry)
106
+ month_key = (int(expiry_str[:4]), int(expiry_str[4:6]))
107
+ by_month.setdefault(month_key, []).append(expiry)
108
+ first_month_key = sorted(by_month)[0]
109
+ return ResolvedExpiry(bucket="MONTH", expiry=max(by_month[first_month_key]))
110
+
111
+ match = WEEK_PATTERN.match(bucket.lower())
112
+ if match is None:
113
+ raise ValueError("expiry_bucket must look like week0, week1, week2, week3, week4, or month.")
114
+
115
+ index = int(match.group("index"))
116
+ if index >= len(option_expiries):
117
+ raise ValueError(
118
+ f"expiry_bucket={expiry_bucket!r} is out of range for the available expiries: {option_expiries}"
119
+ )
120
+ return ResolvedExpiry(bucket=f"WEEK{index}", expiry=option_expiries[index])
121
+
122
+
123
+ def underlying_data(instruments: Any, underlying: str, exchange: str = "NSE") -> pd.DataFrame:
124
+ asset_df = _asset_dataframe(instruments=instruments, underlying=underlying, exchange=exchange)
125
+ stock_name = asset_df["stock_name"].astype(str).str.upper()
126
+ derivative_type = asset_df["derivative_type"].astype(str).str.upper()
127
+ underlying_name = _normalize_text(underlying)
128
+ rows = asset_df[(stock_name == underlying_name) & (~derivative_type.isin(["OPT", "FUT"]))].copy()
129
+
130
+ if rows.empty:
131
+ underlying_prev_close = asset_df["underlying_prev_close"].dropna()
132
+ placeholder = {
133
+ "stock_name": underlying_name,
134
+ "ref_id": pd.NA,
135
+ "exchange": _normalize_text(exchange),
136
+ "asset": underlying_name,
137
+ "asset_type": pd.NA,
138
+ "derivative_type": "SPOT",
139
+ "expiry": pd.NA,
140
+ "strike_price": pd.NA,
141
+ "option_type": pd.NA,
142
+ "lot_size": pd.NA,
143
+ "tick_size": pd.NA,
144
+ "token": pd.NA,
145
+ "nubra_name": pd.NA,
146
+ "isin": pd.NA,
147
+ "underlying_prev_close": underlying_prev_close.iloc[0] if not underlying_prev_close.empty else pd.NA,
148
+ }
149
+ rows = pd.DataFrame([placeholder], columns=BASE_COLUMNS)
150
+
151
+ return rows[BASE_COLUMNS].reset_index(drop=True)
152
+
153
+
154
+ def futures_data(instruments: Any, underlying: str, exchange: str = "NSE") -> pd.DataFrame:
155
+ asset_df = _asset_dataframe(instruments=instruments, underlying=underlying, exchange=exchange)
156
+ rows = asset_df[asset_df["derivative_type"] == "FUT"].copy()
157
+ rows = rows.sort_values(by=["expiry", "stock_name"], kind="stable")
158
+ return rows[BASE_COLUMNS].reset_index(drop=True)
159
+
160
+
161
+ def options_data(
162
+ instruments: Any,
163
+ underlying: str,
164
+ exchange: str = "NSE",
165
+ expiry_bucket: str = "week0",
166
+ levels: int = 10,
167
+ option_side: str = "BOTH",
168
+ ) -> pd.DataFrame:
169
+ if levels <= 0:
170
+ raise ValueError("levels must be greater than 0.")
171
+
172
+ asset_df = _asset_dataframe(instruments=instruments, underlying=underlying, exchange=exchange)
173
+ option_rows = asset_df[asset_df["derivative_type"] == "OPT"].copy()
174
+ if option_rows.empty:
175
+ raise ValueError(f"No option instruments found for underlying={_normalize_text(underlying)!r}.")
176
+
177
+ side = _normalize_option_side(option_side)
178
+ option_expiries = sorted(int(value) for value in option_rows["expiry"].dropna().unique().tolist())
179
+ resolved = _resolve_expiry_bucket(option_expiries, expiry_bucket)
180
+
181
+ rows = option_rows[option_rows["expiry"] == resolved.expiry].copy()
182
+ if rows.empty:
183
+ raise ValueError(f"No option instruments found for expiry={resolved.expiry}.")
184
+
185
+ ce_rows = rows[rows["option_type"] == "CE"].copy()
186
+ pe_rows = rows[rows["option_type"] == "PE"].copy()
187
+ ce_strikes = set(ce_rows["strike_price"].dropna().tolist())
188
+ pe_strikes = set(pe_rows["strike_price"].dropna().tolist())
189
+
190
+ if side == "BOTH":
191
+ valid_strikes = sorted(ce_strikes & pe_strikes)
192
+ elif side == "CE":
193
+ valid_strikes = sorted(ce_strikes)
194
+ else:
195
+ valid_strikes = sorted(pe_strikes)
196
+
197
+ if not valid_strikes:
198
+ raise ValueError("No matching option rows were found for the selected filters.")
199
+
200
+ underlying_prev_close_series = rows["underlying_prev_close"].dropna()
201
+ underlying_prev_close = (
202
+ float(underlying_prev_close_series.iloc[0])
203
+ if not underlying_prev_close_series.empty
204
+ else float(valid_strikes[len(valid_strikes) // 2])
205
+ )
206
+
207
+ ranked_strikes = sorted(valid_strikes, key=lambda strike: (abs(strike - underlying_prev_close), strike))
208
+ selected_strikes = ranked_strikes[: min(levels, len(ranked_strikes))]
209
+ rank_map = {strike: index + 1 for index, strike in enumerate(selected_strikes)}
210
+
211
+ rows = rows[rows["strike_price"].isin(selected_strikes)].copy()
212
+ if side in OPTION_TYPES:
213
+ rows = rows[rows["option_type"] == side].copy()
214
+ else:
215
+ rows = rows[rows["option_type"].isin(["CE", "PE"])].copy()
216
+
217
+ if side == "CE":
218
+ rows = rows.sort_values(by=["strike_price"], ascending=[True], kind="stable")
219
+ return rows[BASE_COLUMNS].reset_index(drop=True)
220
+
221
+ if side == "PE":
222
+ rows = rows.sort_values(by=["strike_price"], ascending=[False], kind="stable")
223
+ return rows[BASE_COLUMNS].reset_index(drop=True)
224
+
225
+ ce_output = rows[rows["option_type"] == "CE"].sort_values(by=["strike_price"], ascending=[True], kind="stable")
226
+ pe_output = rows[rows["option_type"] == "PE"].sort_values(by=["strike_price"], ascending=[False], kind="stable")
227
+ final_rows = pd.concat([ce_output, pe_output], ignore_index=True, sort=False)
228
+ return final_rows[BASE_COLUMNS].reset_index(drop=True)
229
+
230
+
231
+ def all_data(
232
+ instruments: Any,
233
+ underlying: str,
234
+ exchange: str = "NSE",
235
+ expiry_bucket: str = "week0",
236
+ levels: int = 10,
237
+ option_side: str = "BOTH",
238
+ ) -> pd.DataFrame:
239
+ frames = [
240
+ underlying_data(instruments=instruments, underlying=underlying, exchange=exchange),
241
+ futures_data(instruments=instruments, underlying=underlying, exchange=exchange),
242
+ options_data(
243
+ instruments=instruments,
244
+ underlying=underlying,
245
+ exchange=exchange,
246
+ expiry_bucket=expiry_bucket,
247
+ levels=levels,
248
+ option_side=option_side,
249
+ ),
250
+ ]
251
+ concat_frames = [frame.dropna(axis=1, how="all") for frame in frames if not frame.empty]
252
+ final_df = pd.concat(concat_frames, ignore_index=True, sort=False)
253
+ return final_df[BASE_COLUMNS].reset_index(drop=True)
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: nubra_ref_data
3
+ Version: 0.1.0
4
+ Summary: Reference-data helpers for Nubra InstrumentData option, future, and underlying DataFrames
5
+ Author: Akshay N
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/akshayn-spec/nubra_ref_data
8
+ Project-URL: Repository, https://github.com/akshayn-spec/nubra_ref_data
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pandas>=2.0
13
+ Dynamic: license-file
14
+
15
+ # nubra_ref_data
16
+
17
+ `nubra_ref_data` is a small helper package for Nubra `InstrumentData` users who want filtered pandas DataFrames for:
18
+
19
+ - the underlying row
20
+ - futures rows
21
+ - option rows by expiry bucket and strike levels
22
+ - one combined DataFrame containing all of the above
23
+
24
+ It is designed to work with:
25
+
26
+ ```python
27
+ from nubra_python_sdk.refdata.instruments import InstrumentData
28
+
29
+ instruments = InstrumentData(nubra)
30
+ ```
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install nubra_ref_data
36
+ ```
37
+
38
+ ## Functions
39
+
40
+ - `underlying_data(instruments, underlying, exchange="NSE")`
41
+ - `futures_data(instruments, underlying, exchange="NSE")`
42
+ - `options_data(instruments, underlying, exchange="NSE", expiry_bucket="week0", levels=10, option_side="BOTH")`
43
+ - `all_data(instruments, underlying, exchange="NSE", expiry_bucket="week0", levels=10, option_side="BOTH")`
44
+
45
+ ## Example
46
+
47
+ ```python
48
+ from nubra_python_sdk.start_sdk import InitNubraSdk, NubraEnv
49
+ from nubra_python_sdk.refdata.instruments import InstrumentData
50
+
51
+ from nubra_ref_data import all_data, options_data
52
+
53
+
54
+ nubra = InitNubraSdk(NubraEnv.UAT, env_creds=True)
55
+ instruments = InstrumentData(nubra)
56
+
57
+ df_all = all_data(
58
+ instruments=instruments,
59
+ underlying="NIFTY",
60
+ exchange="NSE",
61
+ expiry_bucket="week0",
62
+ levels=8,
63
+ option_side="BOTH",
64
+ )
65
+
66
+ df_ce = options_data(
67
+ instruments=instruments,
68
+ underlying="NIFTY",
69
+ exchange="NSE",
70
+ expiry_bucket="month",
71
+ levels=5,
72
+ option_side="CE",
73
+ )
74
+
75
+ df_sensex = all_data(
76
+ instruments=instruments,
77
+ underlying="SENSEX",
78
+ exchange="BSE",
79
+ expiry_bucket="week0",
80
+ levels=6,
81
+ option_side="BOTH",
82
+ )
83
+ ```
84
+
85
+ ## Behavior
86
+
87
+ - `week0`, `week1`, `week2`, `week3`, and `week4` resolve against the sorted available option expiries for that underlying.
88
+ - `month` resolves to the first month-end expiry available in the option data.
89
+ - `levels` picks the nearest strikes using `underlying_prev_close`.
90
+ - `option_side="BOTH"` only selects strikes where both `CE` and `PE` exist.
91
+ - `all_data()` returns rows in this order: underlying, futures, then options.
92
+ - If the underlying cash/index row is missing from the instruments master, a placeholder `UNDERLYING` row is added.
93
+ - The returned DataFrames preserve the original instrument columns only. No helper columns are added.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/nubra_ref_data/__init__.py
5
+ src/nubra_ref_data/refdata.py
6
+ src/nubra_ref_data.egg-info/PKG-INFO
7
+ src/nubra_ref_data.egg-info/SOURCES.txt
8
+ src/nubra_ref_data.egg-info/dependency_links.txt
9
+ src/nubra_ref_data.egg-info/requires.txt
10
+ src/nubra_ref_data.egg-info/top_level.txt
11
+ tests/test_refdata.py
@@ -0,0 +1 @@
1
+ nubra_ref_data
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import unittest
4
+
5
+ import pandas as pd
6
+
7
+ from nubra_ref_data import all_data, futures_data, options_data, underlying_data
8
+
9
+
10
+ class FakeInstruments:
11
+ def __init__(self, dataframe: pd.DataFrame) -> None:
12
+ self._dataframe = dataframe
13
+
14
+ def get_instruments_dataframe(self, exchange: str | None = None) -> pd.DataFrame:
15
+ df = self._dataframe.copy()
16
+ if exchange is None:
17
+ return df
18
+ return df[df["exchange"].astype(str).str.upper() == str(exchange).upper()].reset_index(drop=True)
19
+
20
+
21
+ def _sample_df() -> pd.DataFrame:
22
+ rows = [
23
+ {
24
+ "stock_name": "RELIANCE",
25
+ "ref_id": 1,
26
+ "exchange": "NSE",
27
+ "asset": "RELIANCE",
28
+ "asset_type": "STOCKS",
29
+ "derivative_type": "",
30
+ "expiry": pd.NA,
31
+ "strike_price": pd.NA,
32
+ "option_type": pd.NA,
33
+ "lot_size": 1,
34
+ "tick_size": 5,
35
+ "token": 101,
36
+ "nubra_name": "STOCK_RELIANCE.NSECM",
37
+ "isin": "INE002A01018",
38
+ "underlying_prev_close": 201000.0,
39
+ },
40
+ {
41
+ "stock_name": "RELIANCE26MARFUT",
42
+ "ref_id": 2,
43
+ "exchange": "NSE",
44
+ "asset": "RELIANCE",
45
+ "asset_type": "STOCK_FO",
46
+ "derivative_type": "FUT",
47
+ "expiry": 20260326,
48
+ "strike_price": -1,
49
+ "option_type": pd.NA,
50
+ "lot_size": 250,
51
+ "tick_size": 5,
52
+ "token": 102,
53
+ "nubra_name": "FUT_RELIANCE_20260326",
54
+ "isin": pd.NA,
55
+ "underlying_prev_close": 201000.0,
56
+ },
57
+ {
58
+ "stock_name": "RELIANCE26APRFUT",
59
+ "ref_id": 3,
60
+ "exchange": "NSE",
61
+ "asset": "RELIANCE",
62
+ "asset_type": "STOCK_FO",
63
+ "derivative_type": "FUT",
64
+ "expiry": 20260430,
65
+ "strike_price": -1,
66
+ "option_type": pd.NA,
67
+ "lot_size": 250,
68
+ "tick_size": 5,
69
+ "token": 103,
70
+ "nubra_name": "FUT_RELIANCE_20260430",
71
+ "isin": pd.NA,
72
+ "underlying_prev_close": 201000.0,
73
+ },
74
+ ]
75
+ expiries = [20260319, 20260326, 20260430]
76
+ strikes = [199000, 200000, 201000, 202000]
77
+ ref_id = 100
78
+ for expiry in expiries:
79
+ for strike in strikes:
80
+ for option_type in ["CE", "PE"]:
81
+ rows.append(
82
+ {
83
+ "stock_name": f"RELIANCE{expiry}{strike}{option_type}",
84
+ "ref_id": ref_id,
85
+ "exchange": "NSE",
86
+ "asset": "RELIANCE",
87
+ "asset_type": "STOCK_FO",
88
+ "derivative_type": "OPT",
89
+ "expiry": expiry,
90
+ "strike_price": strike,
91
+ "option_type": option_type,
92
+ "lot_size": 250,
93
+ "tick_size": 5,
94
+ "token": 1000 + ref_id,
95
+ "nubra_name": f"OPT_RELIANCE_{expiry}_{option_type}_{strike}",
96
+ "isin": pd.NA,
97
+ "underlying_prev_close": 201000.0,
98
+ }
99
+ )
100
+ ref_id += 1
101
+ return pd.DataFrame(rows)
102
+
103
+
104
+ class NubraRefDataTests(unittest.TestCase):
105
+ def setUp(self) -> None:
106
+ self.instruments = FakeInstruments(_sample_df())
107
+
108
+ def test_underlying_data_returns_stock_row(self) -> None:
109
+ result = underlying_data(self.instruments, underlying="RELIANCE", exchange="NSE")
110
+ self.assertEqual(len(result), 1)
111
+ self.assertEqual(result.iloc[0]["stock_name"], "RELIANCE")
112
+ self.assertEqual(list(result.columns), [
113
+ "stock_name", "ref_id", "exchange", "asset", "asset_type", "derivative_type",
114
+ "expiry", "strike_price", "option_type", "lot_size", "tick_size", "token",
115
+ "nubra_name", "isin", "underlying_prev_close",
116
+ ])
117
+
118
+ def test_futures_data_returns_all_futures(self) -> None:
119
+ result = futures_data(self.instruments, underlying="RELIANCE", exchange="NSE")
120
+ self.assertEqual(len(result), 2)
121
+ self.assertEqual(set(result["derivative_type"].tolist()), {"FUT"})
122
+
123
+ def test_options_data_resolves_week_bucket_and_levels(self) -> None:
124
+ result = options_data(
125
+ self.instruments,
126
+ underlying="RELIANCE",
127
+ exchange="NSE",
128
+ expiry_bucket="week0",
129
+ levels=2,
130
+ option_side="BOTH",
131
+ )
132
+ self.assertEqual(len(result), 4)
133
+ self.assertEqual(set(result["option_type"].tolist()), {"CE", "PE"})
134
+ self.assertEqual(result["expiry"].dropna().astype(int).unique().tolist(), [20260319])
135
+ self.assertEqual(result["option_type"].tolist(), ["CE", "CE", "PE", "PE"])
136
+ self.assertEqual(result["strike_price"].tolist(), [200000, 201000, 201000, 200000])
137
+
138
+ def test_ce_and_pe_are_separated_with_requested_ordering(self) -> None:
139
+ result = options_data(
140
+ self.instruments,
141
+ underlying="RELIANCE",
142
+ exchange="NSE",
143
+ expiry_bucket="week0",
144
+ levels=3,
145
+ option_side="BOTH",
146
+ )
147
+ ce_rows = result[result["option_type"] == "CE"]
148
+ pe_rows = result[result["option_type"] == "PE"]
149
+ self.assertEqual(ce_rows["strike_price"].tolist(), [200000, 201000, 202000])
150
+ self.assertEqual(pe_rows["strike_price"].tolist(), [202000, 201000, 200000])
151
+
152
+ def test_all_data_combines_underlying_futures_and_options(self) -> None:
153
+ result = all_data(
154
+ self.instruments,
155
+ underlying="RELIANCE",
156
+ exchange="NSE",
157
+ expiry_bucket="month",
158
+ levels=1,
159
+ option_side="PE",
160
+ )
161
+ self.assertEqual(result.iloc[0]["stock_name"], "RELIANCE")
162
+ option_rows = result[result["derivative_type"] == "OPT"]
163
+ self.assertEqual(len(option_rows), 1)
164
+ self.assertEqual(option_rows["expiry"].dropna().astype(int).unique().tolist(), [20260326])
165
+
166
+
167
+ if __name__ == "__main__":
168
+ unittest.main()