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.
- nubra_ref_data-0.1.0/LICENSE +21 -0
- nubra_ref_data-0.1.0/PKG-INFO +93 -0
- nubra_ref_data-0.1.0/README.md +79 -0
- nubra_ref_data-0.1.0/pyproject.toml +25 -0
- nubra_ref_data-0.1.0/setup.cfg +4 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data/__init__.py +3 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data/refdata.py +253 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data.egg-info/PKG-INFO +93 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data.egg-info/SOURCES.txt +11 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data.egg-info/dependency_links.txt +1 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data.egg-info/requires.txt +1 -0
- nubra_ref_data-0.1.0/src/nubra_ref_data.egg-info/top_level.txt +1 -0
- nubra_ref_data-0.1.0/tests/test_refdata.py +168 -0
|
@@ -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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pandas>=2.0
|
|
@@ -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()
|