quantmod 0.1.1__tar.gz → 0.1.3__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.
- {quantmod-0.1.1 → quantmod-0.1.3}/PKG-INFO +3 -2
- quantmod-0.1.3/quantmod/db/__init__.py +23 -0
- quantmod-0.1.3/quantmod/db/database.py +514 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/derivatives/nse.py +24 -109
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/markets/yahoo.py +73 -68
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/optioninputs.py +43 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/timeseries/performance.py +44 -51
- quantmod-0.1.3/quantmod/version.py +1 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/PKG-INFO +3 -2
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/SOURCES.txt +2 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/requires.txt +2 -1
- quantmod-0.1.1/quantmod/version.py +0 -1
- {quantmod-0.1.1 → quantmod-0.1.3}/LICENSE.txt +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/README.md +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/_version.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/charts/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/charts/plotting.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/charts/themes.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/data/nifty50.csv +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/data/spx.csv +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/dataloader.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/derivatives/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/indicators/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/indicators/indicators.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/main.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/markets/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/markets/bb.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/binomial.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/blackscholes.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/montecarlo.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/var.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/varbacktest.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/varinputs.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/timeseries/__init__.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/timeseries/timeseries.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/utils.py +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/dependency_links.txt +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/entry_points.txt +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/not-zip-safe +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/top_level.txt +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/setup.cfg +0 -0
- {quantmod-0.1.1 → quantmod-0.1.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: quantmod
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Quantmod Python Package
|
|
5
5
|
Home-page: https://kannansingaravelu.com/
|
|
6
6
|
Author: Kannan Singaravelu
|
|
@@ -28,9 +28,10 @@ Requires-Dist: plotly>=6.1.2
|
|
|
28
28
|
Requires-Dist: pydantic>=2.8.2
|
|
29
29
|
Requires-Dist: scipy>=1.13.1
|
|
30
30
|
Requires-Dist: sqlalchemy>=2.0.38
|
|
31
|
+
Requires-Dist: supabase>=2.27.2
|
|
31
32
|
Requires-Dist: tabulate>=0.9.0
|
|
32
33
|
Requires-Dist: urllib3==1.26.15
|
|
33
|
-
Requires-Dist: yfinance
|
|
34
|
+
Requires-Dist: yfinance>=1.0
|
|
34
35
|
Dynamic: author
|
|
35
36
|
Dynamic: author-email
|
|
36
37
|
Dynamic: classifier
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Quantmod Python Package
|
|
2
|
+
# https://kannansingaravelu.com/
|
|
3
|
+
|
|
4
|
+
# Copyright 2024 Kannan Singaravelu
|
|
5
|
+
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
from .database import QuantmodDB
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"QuantmodDB",
|
|
23
|
+
]
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from supabase import create_client, Client
|
|
4
|
+
from typing import Union, List, Optional, Dict
|
|
5
|
+
from quantmod.markets import getData
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
# Configure logging
|
|
9
|
+
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Suppress verbose logging from dependencies
|
|
13
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
14
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QuantmodDB:
|
|
19
|
+
"""
|
|
20
|
+
QuantmodDB
|
|
21
|
+
----------------
|
|
22
|
+
Python SDK for registering instruments, ingesting OHLCV price data,
|
|
23
|
+
and retrieving historical market data from Supabase.
|
|
24
|
+
|
|
25
|
+
Assumptions:
|
|
26
|
+
- Supabase tables `instruments` and `prices` already exist
|
|
27
|
+
- UNIQUE constraint on instruments.symbol
|
|
28
|
+
- UNIQUE constraint on prices (instrument_id, date)
|
|
29
|
+
- RLS disabled OR service-role key is used
|
|
30
|
+
- getData() returns DataFrame indexed by datetime with
|
|
31
|
+
Open, High, Low, Close, Volume columns
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, supabase_url: str, supabase_key: str):
|
|
35
|
+
if not supabase_url or not supabase_key:
|
|
36
|
+
raise ValueError("Supabase URL or Key missing")
|
|
37
|
+
|
|
38
|
+
self.supabase: Client = create_client(supabase_url, supabase_key)
|
|
39
|
+
self._instrument_cache: Dict[str, int] = {}
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Instrument Registration
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def register(self, instruments: Union[dict, List[dict]]) -> List[int]:
|
|
46
|
+
"""
|
|
47
|
+
Register one or multiple instruments with metadata.
|
|
48
|
+
Idempotent: existing symbols are updated with new metadata.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
instruments : dict | list[dict]
|
|
53
|
+
Instrument(s) to register. Each dict must contain 'symbol'.
|
|
54
|
+
Optional keys: name, exchange, asset_class, instrument_type
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
list[int]
|
|
59
|
+
Instrument IDs (existing or newly created)
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
ValueError
|
|
64
|
+
If instrument dict is missing 'symbol' key
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(instruments, dict):
|
|
67
|
+
instruments = [instruments]
|
|
68
|
+
|
|
69
|
+
if not instruments:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
# Validate all instruments have symbols
|
|
73
|
+
for inst in instruments:
|
|
74
|
+
if "symbol" not in inst:
|
|
75
|
+
raise ValueError("Each instrument must have a 'symbol' key")
|
|
76
|
+
|
|
77
|
+
symbols = [inst["symbol"] for inst in instruments]
|
|
78
|
+
|
|
79
|
+
# Batch fetch existing instruments
|
|
80
|
+
try:
|
|
81
|
+
resp = (
|
|
82
|
+
self.supabase.table("instruments")
|
|
83
|
+
.select("id, symbol")
|
|
84
|
+
.in_("symbol", symbols)
|
|
85
|
+
.execute()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
existing = {row["symbol"]: row["id"] for row in resp.data} if resp.data else {}
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to fetch existing instruments: {e}")
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
ids: List[int] = []
|
|
94
|
+
to_insert: List[dict] = []
|
|
95
|
+
to_update: List[dict] = []
|
|
96
|
+
|
|
97
|
+
for inst in instruments:
|
|
98
|
+
symbol = inst["symbol"]
|
|
99
|
+
|
|
100
|
+
payload = {
|
|
101
|
+
"symbol": symbol,
|
|
102
|
+
"name": inst.get("name", symbol),
|
|
103
|
+
"exchange": inst.get("exchange"),
|
|
104
|
+
"asset_class": inst.get("asset_class"),
|
|
105
|
+
"instrument_type": inst.get("instrument_type"),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if symbol in existing:
|
|
109
|
+
# Update existing instrument
|
|
110
|
+
inst_id = existing[symbol]
|
|
111
|
+
ids.append(inst_id)
|
|
112
|
+
to_update.append({"id": inst_id, **payload})
|
|
113
|
+
# Update cache
|
|
114
|
+
self._instrument_cache[symbol] = inst_id
|
|
115
|
+
else:
|
|
116
|
+
# New instrument
|
|
117
|
+
to_insert.append(payload)
|
|
118
|
+
|
|
119
|
+
# Batch insert new instruments
|
|
120
|
+
if to_insert:
|
|
121
|
+
try:
|
|
122
|
+
resp = self.supabase.table("instruments").insert(to_insert).execute()
|
|
123
|
+
for row in resp.data:
|
|
124
|
+
ids.append(row["id"])
|
|
125
|
+
self._instrument_cache[row["symbol"]] = row["id"]
|
|
126
|
+
logger.info(f"Registered {len(to_insert)} new instruments")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to insert instruments: {e}")
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
# Batch update existing instruments
|
|
132
|
+
if to_update:
|
|
133
|
+
try:
|
|
134
|
+
for update in to_update:
|
|
135
|
+
self.supabase.table("instruments").update(update).eq("id", update["id"]).execute()
|
|
136
|
+
logger.info(f"Updated {len(to_update)} existing instruments")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Failed to update instruments: {e}")
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
return ids
|
|
142
|
+
|
|
143
|
+
def get_instrument_id(self, symbol: str) -> Optional[int]:
|
|
144
|
+
"""
|
|
145
|
+
Get instrument ID for a symbol.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
symbol : str
|
|
150
|
+
Instrument symbol
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
int | None
|
|
155
|
+
Instrument ID if found, None otherwise
|
|
156
|
+
"""
|
|
157
|
+
# Check cache first
|
|
158
|
+
if symbol in self._instrument_cache:
|
|
159
|
+
return self._instrument_cache[symbol]
|
|
160
|
+
|
|
161
|
+
# Query database
|
|
162
|
+
try:
|
|
163
|
+
resp = (
|
|
164
|
+
self.supabase.table("instruments")
|
|
165
|
+
.select("id")
|
|
166
|
+
.eq("symbol", symbol)
|
|
167
|
+
.maybe_single()
|
|
168
|
+
.execute()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if resp.data:
|
|
172
|
+
inst_id = resp.data["id"]
|
|
173
|
+
self._instrument_cache[symbol] = inst_id
|
|
174
|
+
return inst_id
|
|
175
|
+
return None
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to fetch instrument ID for {symbol}: {e}")
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
def list_instruments(
|
|
181
|
+
self,
|
|
182
|
+
symbols: Optional[Union[str, List[str]]] = None,
|
|
183
|
+
exchange: Optional[str] = None,
|
|
184
|
+
asset_class: Optional[str] = None
|
|
185
|
+
) -> pd.DataFrame:
|
|
186
|
+
"""
|
|
187
|
+
List registered instruments with optional filters.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
symbols : str | list[str], optional
|
|
192
|
+
Filter by specific symbol(s)
|
|
193
|
+
exchange : str, optional
|
|
194
|
+
Filter by exchange
|
|
195
|
+
asset_class : str, optional
|
|
196
|
+
Filter by asset class
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
DataFrame
|
|
201
|
+
Registered instruments with metadata
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
query = self.supabase.table("instruments").select("*")
|
|
205
|
+
|
|
206
|
+
if symbols is not None:
|
|
207
|
+
if isinstance(symbols, str):
|
|
208
|
+
symbols = [symbols]
|
|
209
|
+
query = query.in_("symbol", symbols)
|
|
210
|
+
if exchange:
|
|
211
|
+
query = query.eq("exchange", exchange)
|
|
212
|
+
if asset_class:
|
|
213
|
+
query = query.eq("asset_class", asset_class)
|
|
214
|
+
|
|
215
|
+
resp = query.order("symbol").execute()
|
|
216
|
+
|
|
217
|
+
if not resp.data:
|
|
218
|
+
return pd.DataFrame()
|
|
219
|
+
|
|
220
|
+
return pd.DataFrame(resp.data)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Failed to list instruments: {e}")
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
# Historical Data Ingestion
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def load_history(
|
|
230
|
+
self,
|
|
231
|
+
symbols: Union[str, List[str]],
|
|
232
|
+
start_date: str,
|
|
233
|
+
end_date: Optional[str] = None,
|
|
234
|
+
) -> Dict[str, int]:
|
|
235
|
+
"""
|
|
236
|
+
Load historical OHLCV data for one or multiple symbols.
|
|
237
|
+
|
|
238
|
+
All symbols MUST be registered before loading data.
|
|
239
|
+
Use register() first to add instruments with metadata.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
symbols : str | list[str]
|
|
244
|
+
Symbol(s) to load data for (must be pre-registered)
|
|
245
|
+
start_date : str
|
|
246
|
+
Start date in YYYY-MM-DD format
|
|
247
|
+
end_date : str, optional
|
|
248
|
+
End date in YYYY-MM-DD format (defaults to today)
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
dict
|
|
253
|
+
{symbol: num_records_loaded}
|
|
254
|
+
|
|
255
|
+
Raises
|
|
256
|
+
------
|
|
257
|
+
ValueError
|
|
258
|
+
If any symbol is not registered
|
|
259
|
+
"""
|
|
260
|
+
if end_date is None:
|
|
261
|
+
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
262
|
+
|
|
263
|
+
# Normalize to list
|
|
264
|
+
if isinstance(symbols, str):
|
|
265
|
+
symbols = [symbols]
|
|
266
|
+
|
|
267
|
+
# Validate all symbols are registered
|
|
268
|
+
symbol_to_id: Dict[str, int] = {}
|
|
269
|
+
missing_symbols: List[str] = []
|
|
270
|
+
|
|
271
|
+
for symbol in symbols:
|
|
272
|
+
inst_id = self.get_instrument_id(symbol)
|
|
273
|
+
if inst_id is None:
|
|
274
|
+
missing_symbols.append(symbol)
|
|
275
|
+
else:
|
|
276
|
+
symbol_to_id[symbol] = inst_id
|
|
277
|
+
|
|
278
|
+
if missing_symbols:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"Instruments not registered: {missing_symbols}. "
|
|
281
|
+
f"Use register() to add them first."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Load data for each symbol
|
|
285
|
+
results: Dict[str, int] = {}
|
|
286
|
+
|
|
287
|
+
for symbol, inst_id in symbol_to_id.items():
|
|
288
|
+
try:
|
|
289
|
+
# Fetch market data
|
|
290
|
+
df = getData(symbol, start_date=start_date, end_date=end_date)
|
|
291
|
+
|
|
292
|
+
if df.empty:
|
|
293
|
+
logger.warning(f"No data available for {symbol}")
|
|
294
|
+
results[symbol] = 0
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Prepare rows with NaN handling
|
|
298
|
+
rows = []
|
|
299
|
+
for idx, row in df.iterrows():
|
|
300
|
+
try:
|
|
301
|
+
rows.append({
|
|
302
|
+
"instrument_id": inst_id,
|
|
303
|
+
"date": idx.strftime("%Y-%m-%d"),
|
|
304
|
+
"open": float(row["Open"]) if pd.notna(row["Open"]) else None,
|
|
305
|
+
"high": float(row["High"]) if pd.notna(row["High"]) else None,
|
|
306
|
+
"low": float(row["Low"]) if pd.notna(row["Low"]) else None,
|
|
307
|
+
"close": float(row["Close"]) if pd.notna(row["Close"]) else None,
|
|
308
|
+
"volume": int(row["Volume"]) if pd.notna(row["Volume"]) else 0,
|
|
309
|
+
})
|
|
310
|
+
except (ValueError, KeyError) as e:
|
|
311
|
+
logger.warning(f"Skipping invalid row for {symbol} at {idx}: {e}")
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
if not rows:
|
|
315
|
+
logger.warning(f"No valid data rows for {symbol}")
|
|
316
|
+
results[symbol] = 0
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Upsert OHLCV data
|
|
320
|
+
self.supabase.table("prices").upsert(
|
|
321
|
+
rows, on_conflict="instrument_id,date"
|
|
322
|
+
).execute()
|
|
323
|
+
|
|
324
|
+
results[symbol] = len(rows)
|
|
325
|
+
logger.info(f"Loaded {len(rows)} records for {symbol}")
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"Failed to load data for {symbol}: {e}")
|
|
329
|
+
results[symbol] = -1 # Indicate failure
|
|
330
|
+
|
|
331
|
+
return results
|
|
332
|
+
|
|
333
|
+
# ------------------------------------------------------------------
|
|
334
|
+
# Data Retrieval
|
|
335
|
+
# ------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def get_prices(
|
|
338
|
+
self,
|
|
339
|
+
symbols: Optional[Union[str, List[str]]] = None,
|
|
340
|
+
start_date: Optional[str] = None,
|
|
341
|
+
end_date: Optional[str] = None,
|
|
342
|
+
limit: Optional[int] = None,
|
|
343
|
+
) -> pd.DataFrame:
|
|
344
|
+
"""
|
|
345
|
+
Retrieve OHLCV data in long format.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
symbols : str | list[str], optional
|
|
350
|
+
Filter by symbol(s). If None, returns all.
|
|
351
|
+
start_date : str, optional
|
|
352
|
+
Start date filter (YYYY-MM-DD)
|
|
353
|
+
end_date : str, optional
|
|
354
|
+
End date filter (YYYY-MM-DD)
|
|
355
|
+
limit : int, optional
|
|
356
|
+
Maximum number of records to return
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
DataFrame
|
|
361
|
+
Columns: date, open, high, low, close, volume, symbol
|
|
362
|
+
Sorted by date ascending
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
query = self.supabase.table("prices").select(
|
|
366
|
+
"date, open, high, low, close, volume, instruments!inner(symbol)"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if symbols is not None:
|
|
370
|
+
if isinstance(symbols, str):
|
|
371
|
+
symbols = [symbols]
|
|
372
|
+
query = query.in_("instruments.symbol", symbols)
|
|
373
|
+
|
|
374
|
+
if start_date:
|
|
375
|
+
query = query.gte("date", start_date)
|
|
376
|
+
if end_date:
|
|
377
|
+
query = query.lte("date", end_date)
|
|
378
|
+
|
|
379
|
+
query = query.order("date", desc=False)
|
|
380
|
+
|
|
381
|
+
if limit:
|
|
382
|
+
query = query.limit(limit)
|
|
383
|
+
|
|
384
|
+
resp = query.execute()
|
|
385
|
+
|
|
386
|
+
if not resp.data:
|
|
387
|
+
return pd.DataFrame(
|
|
388
|
+
columns=["date", "open", "high", "low", "close", "volume", "symbol"]
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
df = pd.DataFrame(resp.data)
|
|
392
|
+
|
|
393
|
+
# Extract symbol from joined table
|
|
394
|
+
df["symbol"] = df["instruments"].apply(lambda x: x["symbol"])
|
|
395
|
+
df = df.drop(columns=["instruments"])
|
|
396
|
+
|
|
397
|
+
# Convert date to datetime
|
|
398
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
399
|
+
|
|
400
|
+
return df
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"Failed to retrieve prices: {e}")
|
|
404
|
+
raise
|
|
405
|
+
|
|
406
|
+
def get_asset_prices(
|
|
407
|
+
self,
|
|
408
|
+
symbols: Optional[Union[str, List[str]]] = None,
|
|
409
|
+
start_date: Optional[str] = None,
|
|
410
|
+
end_date: Optional[str] = None,
|
|
411
|
+
column: str = "close",
|
|
412
|
+
) -> pd.DataFrame:
|
|
413
|
+
"""
|
|
414
|
+
Retrieve OHLCV data in wide format (symbols as columns).
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
symbols : str | list[str], optional
|
|
419
|
+
Filter by symbol(s)
|
|
420
|
+
start_date : str, optional
|
|
421
|
+
Start date filter (YYYY-MM-DD)
|
|
422
|
+
end_date : str, optional
|
|
423
|
+
End date filter (YYYY-MM-DD)
|
|
424
|
+
column : str
|
|
425
|
+
Price column to pivot ('open', 'high', 'low', 'close', 'volume')
|
|
426
|
+
|
|
427
|
+
Returns
|
|
428
|
+
-------
|
|
429
|
+
DataFrame
|
|
430
|
+
Index: date, Columns: symbols
|
|
431
|
+
"""
|
|
432
|
+
df = self.get_prices(symbols, start_date, end_date)
|
|
433
|
+
|
|
434
|
+
if df.empty:
|
|
435
|
+
return pd.DataFrame()
|
|
436
|
+
|
|
437
|
+
# Pivot to wide format
|
|
438
|
+
df_wide = df.pivot(index="date", columns="symbol", values=column)
|
|
439
|
+
|
|
440
|
+
return df_wide
|
|
441
|
+
|
|
442
|
+
def get_latest_prices(
|
|
443
|
+
self, symbols: Optional[Union[str, List[str]]] = None
|
|
444
|
+
) -> pd.DataFrame:
|
|
445
|
+
"""
|
|
446
|
+
Get the most recent price for each symbol.
|
|
447
|
+
|
|
448
|
+
Parameters
|
|
449
|
+
----------
|
|
450
|
+
symbols : str | list[str], optional
|
|
451
|
+
Filter by symbol(s)
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
DataFrame
|
|
456
|
+
Latest price record for each symbol
|
|
457
|
+
"""
|
|
458
|
+
try:
|
|
459
|
+
# Build subquery for max date per instrument
|
|
460
|
+
if symbols:
|
|
461
|
+
if isinstance(symbols, str):
|
|
462
|
+
symbols = [symbols]
|
|
463
|
+
|
|
464
|
+
# Get instrument IDs
|
|
465
|
+
inst_resp = (
|
|
466
|
+
self.supabase.table("instruments")
|
|
467
|
+
.select("id, symbol")
|
|
468
|
+
.in_("symbol", symbols)
|
|
469
|
+
.execute()
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if not inst_resp.data:
|
|
473
|
+
return pd.DataFrame()
|
|
474
|
+
|
|
475
|
+
instrument_ids = [row["id"] for row in inst_resp.data]
|
|
476
|
+
symbol_map = {row["id"]: row["symbol"] for row in inst_resp.data}
|
|
477
|
+
else:
|
|
478
|
+
# Get all instruments
|
|
479
|
+
inst_resp = self.supabase.table("instruments").select("id, symbol").execute()
|
|
480
|
+
if not inst_resp.data:
|
|
481
|
+
return pd.DataFrame()
|
|
482
|
+
|
|
483
|
+
instrument_ids = [row["id"] for row in inst_resp.data]
|
|
484
|
+
symbol_map = {row["id"]: row["symbol"] for row in inst_resp.data}
|
|
485
|
+
|
|
486
|
+
# Get latest price for each instrument
|
|
487
|
+
all_latest = []
|
|
488
|
+
for inst_id in instrument_ids:
|
|
489
|
+
resp = (
|
|
490
|
+
self.supabase.table("prices")
|
|
491
|
+
.select("*")
|
|
492
|
+
.eq("instrument_id", inst_id)
|
|
493
|
+
.order("date", desc=True)
|
|
494
|
+
.limit(1)
|
|
495
|
+
.execute()
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if resp.data:
|
|
499
|
+
record = resp.data[0]
|
|
500
|
+
record["symbol"] = symbol_map[inst_id]
|
|
501
|
+
all_latest.append(record)
|
|
502
|
+
|
|
503
|
+
if not all_latest:
|
|
504
|
+
return pd.DataFrame()
|
|
505
|
+
|
|
506
|
+
df = pd.DataFrame(all_latest)
|
|
507
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
508
|
+
df = df.drop(columns=["instrument_id"])
|
|
509
|
+
|
|
510
|
+
return df[["symbol", "date", "open", "high", "low", "close", "volume"]]
|
|
511
|
+
|
|
512
|
+
except Exception as e:
|
|
513
|
+
logger.error(f"Failed to retrieve latest prices: {e}")
|
|
514
|
+
raise
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
# to be used only for tutorial purposes
|
|
3
|
-
# for production use, please reach out to NSE India
|
|
1
|
+
# updated to new nse api changes on 10th Dec 2025
|
|
4
2
|
import os, sys
|
|
5
3
|
import requests
|
|
6
4
|
import numpy as np
|
|
@@ -83,89 +81,6 @@ def nsefetch(payload: str):
|
|
|
83
81
|
# --- Utility constants ---
|
|
84
82
|
indices = ["NIFTY", "FINNIFTY", "BANKNIFTY"]
|
|
85
83
|
|
|
86
|
-
# # Constants
|
|
87
|
-
# indices = ["NIFTY", "FINNIFTY", "BANKNIFTY"]
|
|
88
|
-
|
|
89
|
-
# headers = {
|
|
90
|
-
# "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
91
|
-
# "accept-language": "en-US,en;q=0.9,en-IN;q=0.8,en-GB;q=0.7",
|
|
92
|
-
# "cache-control": "max-age=0",
|
|
93
|
-
# "priority": "u=0, i",
|
|
94
|
-
# "sec-ch-ua": '"Microsoft Edge";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
|
|
95
|
-
# "sec-ch-ua-mobile": "?0",
|
|
96
|
-
# "sec-ch-ua-platform": '"Windows"',
|
|
97
|
-
# "sec-fetch-dest": "document",
|
|
98
|
-
# "sec-fetch-mode": "navigate",
|
|
99
|
-
# "sec-fetch-site": "none",
|
|
100
|
-
# "sec-fetch-user": "?1",
|
|
101
|
-
# "upgrade-insecure-requests": "1",
|
|
102
|
-
# "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
|
|
103
|
-
# }
|
|
104
|
-
|
|
105
|
-
# # Curl headers
|
|
106
|
-
# curl_headers = """ -H "authority: beta.nseindia.com" -H "cache-control: max-age=0" -H "dnt: 1" -H "upgrade-insecure-requests: 1" -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36" -H "sec-fetch-user: ?1" -H "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" -H "sec-fetch-site: none" -H "sec-fetch-mode: navigate" -H "accept-encoding: gzip, deflate, br" -H "accept-language: en-US,en;q=0.9,hi;q=0.8" --compressed"""
|
|
107
|
-
|
|
108
|
-
# # https://ipapi.co/json
|
|
109
|
-
# # https://ipinfo.io/json
|
|
110
|
-
|
|
111
|
-
# try:
|
|
112
|
-
# # Try ipapi.co
|
|
113
|
-
# response = requests.get("https://ipapi.co/json/", timeout=5)
|
|
114
|
-
# if response.status_code == 200:
|
|
115
|
-
# data = response.json()
|
|
116
|
-
# country_code = data.get('country_code', '').upper()
|
|
117
|
-
# mode = "local" if country_code == "IN" else "vpn"
|
|
118
|
-
# else:
|
|
119
|
-
# # Fallback to ipinfo.io
|
|
120
|
-
# response = requests.get("https://ipinfo.io/json", timeout=5)
|
|
121
|
-
# if response.status_code == 200:
|
|
122
|
-
# data = response.json()
|
|
123
|
-
# country_code = data.get('country', '').upper()
|
|
124
|
-
# mode = "local" if country_code == "IN" else "vpn"
|
|
125
|
-
# else:
|
|
126
|
-
# mode = "local"
|
|
127
|
-
|
|
128
|
-
# except Exception:
|
|
129
|
-
# mode = "local"
|
|
130
|
-
|
|
131
|
-
# # Force local mode only if you’re sure your machine is in India
|
|
132
|
-
# mode = "local"
|
|
133
|
-
# def nsefetch(payload):
|
|
134
|
-
# if mode == "vpn":
|
|
135
|
-
# if ("%26" in payload) or ("%20" in payload):
|
|
136
|
-
# encoded_url = payload
|
|
137
|
-
# else:
|
|
138
|
-
# encoded_url = urllib.parse.quote(payload, safe=":/?&=")
|
|
139
|
-
# payload_var = 'curl -b cookies.txt "' + encoded_url + '"' + curl_headers + ""
|
|
140
|
-
# try:
|
|
141
|
-
# output = os.popen(payload_var).read()
|
|
142
|
-
# output = json.loads(output)
|
|
143
|
-
# except ValueError: # includes simplejson.decoder.JSONDecodeError:
|
|
144
|
-
# payload2 = "https://www.nseindia.com"
|
|
145
|
-
# output2 = os.popen(
|
|
146
|
-
# 'curl -c cookies.txt "' + payload2 + '"' + curl_headers + ""
|
|
147
|
-
# ).read()
|
|
148
|
-
|
|
149
|
-
# output = os.popen(payload_var).read()
|
|
150
|
-
# output = json.loads(output)
|
|
151
|
-
# return output
|
|
152
|
-
|
|
153
|
-
# else: # mode == "local":
|
|
154
|
-
# try:
|
|
155
|
-
# output = requests.get(payload, headers=headers).json()
|
|
156
|
-
# # print(output)
|
|
157
|
-
# except ValueError:
|
|
158
|
-
# s = requests.Session()
|
|
159
|
-
# try:
|
|
160
|
-
# output = s.get("http://nseindia.com/option-chain", headers=headers)
|
|
161
|
-
# output = s.get(payload, headers=headers).json()
|
|
162
|
-
# except ValueError:
|
|
163
|
-
# output = s.get("https://www.nseindia.com", headers=headers)
|
|
164
|
-
# output = output.json()
|
|
165
|
-
# # output = s.get("https://www.nseindia.com/option-chain", headers=headers) # replaced http://nseindia.com with https://www.nseindia.com/option-chain
|
|
166
|
-
# output = s.get(payload, headers=headers).json()
|
|
167
|
-
# return output
|
|
168
|
-
|
|
169
84
|
|
|
170
85
|
class OptionData:
|
|
171
86
|
"""
|
|
@@ -232,16 +147,15 @@ class OptionData:
|
|
|
232
147
|
"""
|
|
233
148
|
if any(x in self.symbol for x in indices):
|
|
234
149
|
payload = nsefetch(
|
|
235
|
-
"https://www.nseindia.com/api/option-chain-
|
|
236
|
-
+ self.symbol
|
|
150
|
+
f"https://www.nseindia.com/api/option-chain-v3?type=Indices&symbol={self.symbol}&expiry={self.expiry_dt}"
|
|
237
151
|
)
|
|
238
152
|
else:
|
|
239
153
|
payload = nsefetch(
|
|
240
|
-
"https://www.nseindia.com/api/option-chain-
|
|
241
|
-
+ self.symbol
|
|
154
|
+
f"https://www.nseindia.com/api/option-chain-v3?type=Equity&symbol={self.symbol}&expiry={self.expiry_dt}"
|
|
242
155
|
)
|
|
243
156
|
return payload
|
|
244
157
|
|
|
158
|
+
|
|
245
159
|
def get_option_quote(self, strikePrice, optionType, intent=""):
|
|
246
160
|
"""
|
|
247
161
|
Get option quote for specific strike price and option type.
|
|
@@ -254,9 +168,9 @@ class OptionData:
|
|
|
254
168
|
Type of option, either 'CE' (Call) or 'PE' (Put)
|
|
255
169
|
intent : str, optional
|
|
256
170
|
Quote type:
|
|
257
|
-
- '' (default)
|
|
258
|
-
- 'sell' for
|
|
259
|
-
- 'buy' for
|
|
171
|
+
- '' (default) lastPrice
|
|
172
|
+
- 'sell' for sellPrice1
|
|
173
|
+
- 'buy' for buyPrice1
|
|
260
174
|
|
|
261
175
|
Returns
|
|
262
176
|
-------
|
|
@@ -264,16 +178,15 @@ class OptionData:
|
|
|
264
178
|
Option price based on the specified intent
|
|
265
179
|
"""
|
|
266
180
|
for x in range(len(self.payload["records"]["data"])):
|
|
267
|
-
if (self.payload["records"]["data"][x]["strikePrice"] == strikePrice)
|
|
268
|
-
self.payload["records"]["data"][x]["expiryDate"] == self.expiry_dt
|
|
269
|
-
):
|
|
181
|
+
if (self.payload["records"]["data"][x]["strikePrice"] == strikePrice):
|
|
270
182
|
if intent == "":
|
|
271
183
|
return self.payload["records"]["data"][x][optionType]["lastPrice"]
|
|
272
184
|
if intent == "sell":
|
|
273
|
-
return self.payload["records"]["data"][x][optionType]["
|
|
185
|
+
return self.payload["records"]["data"][x][optionType]["sellPrice1"]
|
|
274
186
|
if intent == "buy":
|
|
275
|
-
return self.payload["records"]["data"][x][optionType]["
|
|
187
|
+
return self.payload["records"]["data"][x][optionType]["buyPrice1"]
|
|
276
188
|
|
|
189
|
+
|
|
277
190
|
def _get_option_pcr(self):
|
|
278
191
|
"""
|
|
279
192
|
Calculate Put-Call Ratio based on open interest.
|
|
@@ -286,14 +199,14 @@ class OptionData:
|
|
|
286
199
|
ce_oi = 0
|
|
287
200
|
pe_oi = 0
|
|
288
201
|
for i in self.payload["records"]["data"]:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
pass
|
|
202
|
+
try:
|
|
203
|
+
ce_oi += i["CE"]["openInterest"]
|
|
204
|
+
pe_oi += i["PE"]["openInterest"]
|
|
205
|
+
except KeyError:
|
|
206
|
+
pass
|
|
295
207
|
return round(pe_oi / ce_oi, 2)
|
|
296
208
|
|
|
209
|
+
|
|
297
210
|
def get_synthetic_future_price(self, strike):
|
|
298
211
|
"""
|
|
299
212
|
Calculate synthetic futures price using put-call parity.
|
|
@@ -315,6 +228,7 @@ class OptionData:
|
|
|
315
228
|
)
|
|
316
229
|
return synthetic_futures
|
|
317
230
|
|
|
231
|
+
|
|
318
232
|
def _get_call_option_data(self):
|
|
319
233
|
"""
|
|
320
234
|
Get call options data for current expiry.
|
|
@@ -327,10 +241,11 @@ class OptionData:
|
|
|
327
241
|
ce_values = [
|
|
328
242
|
data["CE"]
|
|
329
243
|
for data in self.payload["records"]["data"]
|
|
330
|
-
if "CE" in data and data["expiryDate"] == self.expiry_dt
|
|
331
244
|
]
|
|
332
245
|
return pd.DataFrame(ce_values).sort_values(["strikePrice"])
|
|
333
|
-
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
334
249
|
def _get_put_option_data(self):
|
|
335
250
|
"""
|
|
336
251
|
Get put options data for current expiry.
|
|
@@ -343,10 +258,10 @@ class OptionData:
|
|
|
343
258
|
pe_values = [
|
|
344
259
|
data["PE"]
|
|
345
260
|
for data in self.payload["records"]["data"]
|
|
346
|
-
if "PE" in data and data["expiryDate"] == self.expiry_dt
|
|
347
261
|
]
|
|
348
262
|
return pd.DataFrame(pe_values).sort_values(["strikePrice"])
|
|
349
|
-
|
|
263
|
+
|
|
264
|
+
|
|
350
265
|
def _get_maximum_pain_strike(self):
|
|
351
266
|
"""
|
|
352
267
|
Calculate maximum pain strike price.
|
|
@@ -367,4 +282,4 @@ class OptionData:
|
|
|
367
282
|
for expiry_price in strikes
|
|
368
283
|
]
|
|
369
284
|
|
|
370
|
-
return strikes[np.argmin(total_pain)]
|
|
285
|
+
return strikes[np.argmin(total_pain)]
|
|
@@ -5,75 +5,12 @@ import hashlib
|
|
|
5
5
|
import os
|
|
6
6
|
from typing import Union, List
|
|
7
7
|
|
|
8
|
-
# # Directory to store cache files
|
|
9
|
-
# CACHE_DIR = '.cache'
|
|
10
|
-
|
|
11
|
-
# def _get_cache_file_name(tickers: Union[str, List[str]], start_date: str, end_date: str, period: str, interval: str) -> str:
|
|
12
|
-
# """
|
|
13
|
-
# Generate a cache file name based on the parameters.
|
|
14
|
-
# """
|
|
15
|
-
# if isinstance(tickers, str):
|
|
16
|
-
# tickers = [tickers]
|
|
17
|
-
# key = f"{','.join(tickers)}_{start_date}_{end_date}_{period}_{interval}"
|
|
18
|
-
# cache_key = hashlib.md5(key.encode()).hexdigest()
|
|
19
|
-
# return os.path.join(CACHE_DIR, f"{cache_key}.pkl")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# def getData(tickers: Union[str, List[str]], start_date: str = None, end_date: str = None, period: str = '1mo', interval: str = '1d') -> pd.DataFrame:
|
|
23
|
-
# """
|
|
24
|
-
# Retrieve data from yfinance library for specified tickers, with caching.
|
|
25
|
-
|
|
26
|
-
# Parameters
|
|
27
|
-
# ----------
|
|
28
|
-
# tickers : str or list
|
|
29
|
-
# Symbol or list of symbols.
|
|
30
|
-
# start_date : str, optional
|
|
31
|
-
# Start date, by default None.
|
|
32
|
-
# end_date : str, optional
|
|
33
|
-
# End date, by default None.
|
|
34
|
-
# period : str, optional
|
|
35
|
-
# Period, by default '1mo'.
|
|
36
|
-
# Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
|
|
37
|
-
# interval : str, optional
|
|
38
|
-
# Interval, by default '1d', max 60 days.
|
|
39
|
-
# Valid intervals: 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo.
|
|
40
|
-
|
|
41
|
-
# Returns
|
|
42
|
-
# -------
|
|
43
|
-
# pd.DataFrame
|
|
44
|
-
# DataFrame with OHLC[A]V (Open, High, Low, Close, Adj Close, Volume).
|
|
45
|
-
# """
|
|
46
|
-
# # Ensure cache directory exists
|
|
47
|
-
# if not os.path.exists(CACHE_DIR):
|
|
48
|
-
# os.makedirs(CACHE_DIR)
|
|
49
|
-
|
|
50
|
-
# cache_file = _get_cache_file_name(tickers, start_date, end_date, period, interval)
|
|
51
|
-
|
|
52
|
-
# # Check if cached file exists
|
|
53
|
-
# if os.path.exists(cache_file):
|
|
54
|
-
# # print(f"Loading data from cache: {cache_file}")
|
|
55
|
-
# return joblib.load(cache_file)
|
|
56
|
-
|
|
57
|
-
# try:
|
|
58
|
-
# # Download data from yfinance
|
|
59
|
-
# data = yf.download(tickers, start=start_date, end=end_date, auto_adjust=True, progress=False, period=period, interval=interval)
|
|
60
|
-
|
|
61
|
-
# # Save data to cache
|
|
62
|
-
# joblib.dump(data, cache_file)
|
|
63
|
-
# # print(f"Data cached to: {cache_file}")
|
|
64
|
-
|
|
65
|
-
# except Exception as e:
|
|
66
|
-
# print(f"Error downloading data: {e}")
|
|
67
|
-
# raise
|
|
68
|
-
|
|
69
|
-
# return data
|
|
70
|
-
|
|
71
8
|
|
|
72
9
|
def getData(
|
|
73
10
|
tickers: Union[str, List[str]],
|
|
74
|
-
start_date: str = None,
|
|
75
|
-
end_date: str = None,
|
|
76
|
-
period: str =
|
|
11
|
+
start_date: str | None = None,
|
|
12
|
+
end_date: str | None = None,
|
|
13
|
+
period: str | None = None,
|
|
77
14
|
interval: str = "1d",
|
|
78
15
|
) -> pd.DataFrame:
|
|
79
16
|
"""
|
|
@@ -83,9 +20,9 @@ def getData(
|
|
|
83
20
|
----------
|
|
84
21
|
tickers : str or list
|
|
85
22
|
symbol or list of symbols
|
|
86
|
-
|
|
23
|
+
start_date : str, optional
|
|
87
24
|
start date, by default None
|
|
88
|
-
|
|
25
|
+
end_date : str, optional
|
|
89
26
|
end date, by default None
|
|
90
27
|
period : str, optional
|
|
91
28
|
period, by default '1mo'
|
|
@@ -99,6 +36,9 @@ def getData(
|
|
|
99
36
|
pd.DataFrame
|
|
100
37
|
DataFrame with OHLC[A]V (Open, High, Low, Close, Adj Close, Volume).
|
|
101
38
|
"""
|
|
39
|
+
|
|
40
|
+
if period and (start_date or end_date):
|
|
41
|
+
raise ValueError("Use either period OR start_date/end_date, not both.")
|
|
102
42
|
|
|
103
43
|
cols = ["Open", "High", "Low", "Close", "Volume"]
|
|
104
44
|
data = yf.download(
|
|
@@ -157,3 +97,68 @@ def getTicker(ticker: str) -> yf.Ticker:
|
|
|
157
97
|
# .news
|
|
158
98
|
|
|
159
99
|
return yf.Ticker(ticker)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# # Directory to store cache files
|
|
104
|
+
# CACHE_DIR = '.cache'
|
|
105
|
+
|
|
106
|
+
# def _get_cache_file_name(tickers: Union[str, List[str]], start_date: str, end_date: str, period: str, interval: str) -> str:
|
|
107
|
+
# """
|
|
108
|
+
# Generate a cache file name based on the parameters.
|
|
109
|
+
# """
|
|
110
|
+
# if isinstance(tickers, str):
|
|
111
|
+
# tickers = [tickers]
|
|
112
|
+
# key = f"{','.join(tickers)}_{start_date}_{end_date}_{period}_{interval}"
|
|
113
|
+
# cache_key = hashlib.md5(key.encode()).hexdigest()
|
|
114
|
+
# return os.path.join(CACHE_DIR, f"{cache_key}.pkl")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# def getData(tickers: Union[str, List[str]], start_date: str = None, end_date: str = None, period: str = '1mo', interval: str = '1d') -> pd.DataFrame:
|
|
118
|
+
# """
|
|
119
|
+
# Retrieve data from yfinance library for specified tickers, with caching.
|
|
120
|
+
|
|
121
|
+
# Parameters
|
|
122
|
+
# ----------
|
|
123
|
+
# tickers : str or list
|
|
124
|
+
# Symbol or list of symbols.
|
|
125
|
+
# start_date : str, optional
|
|
126
|
+
# Start date, by default None.
|
|
127
|
+
# end_date : str, optional
|
|
128
|
+
# End date, by default None.
|
|
129
|
+
# period : str, optional
|
|
130
|
+
# Period, by default '1mo'.
|
|
131
|
+
# Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
|
|
132
|
+
# interval : str, optional
|
|
133
|
+
# Interval, by default '1d', max 60 days.
|
|
134
|
+
# Valid intervals: 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo.
|
|
135
|
+
|
|
136
|
+
# Returns
|
|
137
|
+
# -------
|
|
138
|
+
# pd.DataFrame
|
|
139
|
+
# DataFrame with OHLC[A]V (Open, High, Low, Close, Adj Close, Volume).
|
|
140
|
+
# """
|
|
141
|
+
# # Ensure cache directory exists
|
|
142
|
+
# if not os.path.exists(CACHE_DIR):
|
|
143
|
+
# os.makedirs(CACHE_DIR)
|
|
144
|
+
|
|
145
|
+
# cache_file = _get_cache_file_name(tickers, start_date, end_date, period, interval)
|
|
146
|
+
|
|
147
|
+
# # Check if cached file exists
|
|
148
|
+
# if os.path.exists(cache_file):
|
|
149
|
+
# # print(f"Loading data from cache: {cache_file}")
|
|
150
|
+
# return joblib.load(cache_file)
|
|
151
|
+
|
|
152
|
+
# try:
|
|
153
|
+
# # Download data from yfinance
|
|
154
|
+
# data = yf.download(tickers, start=start_date, end=end_date, auto_adjust=True, progress=False, period=period, interval=interval)
|
|
155
|
+
|
|
156
|
+
# # Save data to cache
|
|
157
|
+
# joblib.dump(data, cache_file)
|
|
158
|
+
# # print(f"Data cached to: {cache_file}")
|
|
159
|
+
|
|
160
|
+
# except Exception as e:
|
|
161
|
+
# print(f"Error downloading data: {e}")
|
|
162
|
+
# raise
|
|
163
|
+
|
|
164
|
+
# return data
|
|
@@ -3,17 +3,60 @@ from enum import Enum
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
class OptionType(str, Enum):
|
|
6
|
+
"""Enum for option types"""
|
|
6
7
|
CALL = "call"
|
|
7
8
|
PUT = "put"
|
|
8
9
|
|
|
10
|
+
@classmethod
|
|
11
|
+
def from_string(cls, s: str):
|
|
12
|
+
"""Convert string to OptionType"""
|
|
13
|
+
s = s.upper()
|
|
14
|
+
if s in ("CE", "CALL", "C"):
|
|
15
|
+
return cls.CALL
|
|
16
|
+
elif s in ("PE", "PUT", "P"):
|
|
17
|
+
return cls.PUT
|
|
18
|
+
raise ValueError(f"Invalid option type: {s}")
|
|
19
|
+
|
|
9
20
|
class ExerciseStyle(str, Enum):
|
|
10
21
|
ASIAN = "asian"
|
|
11
22
|
BARRIER = "barrier"
|
|
12
23
|
EUROPEAN = "european"
|
|
13
24
|
AMERICAN = "american"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_string(cls, s: str):
|
|
28
|
+
"""Convert string to OptionType"""
|
|
29
|
+
s = s.upper()
|
|
30
|
+
if s in ("asian", "Asian", "ASIAN"):
|
|
31
|
+
return cls.ASIAN
|
|
32
|
+
elif s in ("barrier", "Barrier", "BARRIER"):
|
|
33
|
+
return cls.BARRIER
|
|
34
|
+
elif s in ("european", "European", "EUROPEAN"):
|
|
35
|
+
return cls.EUROPEAN
|
|
36
|
+
elif s in ("american", "American", "AMERICAN"):
|
|
37
|
+
return cls.AMERICAN
|
|
38
|
+
raise ValueError(f"Invalid Exercise Style: {s}")
|
|
14
39
|
|
|
15
40
|
class BarrierType(str, Enum):
|
|
16
41
|
UP_AND_OUT = "up_and_out"
|
|
42
|
+
UP_AND_IN = "up_and_in"
|
|
43
|
+
DOWN_AND_OUT = "down_and_out"
|
|
44
|
+
DOWN_AND_IN = "down_and_in"
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_string(cls, s: str):
|
|
48
|
+
"""Convert string to BarrierType"""
|
|
49
|
+
s = s.upper()
|
|
50
|
+
if s in ("up_and_out", "Up_and_out", "UP_AND_OUT"):
|
|
51
|
+
return cls.UP_AND_OUT
|
|
52
|
+
elif s in ("up_and_in", "Up_and_in", "UP_AND_IN"):
|
|
53
|
+
return cls.UP_AND_IN
|
|
54
|
+
elif s in ("down_and_out", "Down_and_out", "DOWN_AND_OUT"):
|
|
55
|
+
return cls.DOWN_AND_OUT
|
|
56
|
+
elif s in ("down_and_in", "Down_and_in", "DOWN_AND_IN"):
|
|
57
|
+
return cls.DOWN_AND_IN
|
|
58
|
+
raise ValueError(f"Invalid Barrier Type: {s}")
|
|
59
|
+
|
|
17
60
|
|
|
18
61
|
class OptionInputs(BaseModel):
|
|
19
62
|
"""
|
|
@@ -6,104 +6,97 @@ def periodReturn(
|
|
|
6
6
|
data: pd.DataFrame | pd.Series, period: str = None
|
|
7
7
|
) -> pd.DataFrame | pd.Series:
|
|
8
8
|
"""
|
|
9
|
-
Calculates periodic returns
|
|
10
|
-
|
|
9
|
+
Calculates periodic log returns.
|
|
10
|
+
Updated for 2026 pandas frequency aliases (ME, QE, YE).
|
|
11
|
+
|
|
11
12
|
Parameters
|
|
12
13
|
----------
|
|
13
14
|
data : pd.DataFrame | pd.Series
|
|
14
15
|
price data
|
|
15
16
|
period : str, optional
|
|
16
17
|
None, defaults to daily frequency
|
|
17
|
-
|
|
18
|
+
Specify W, M, Q, or A for weekly, monthly, quarterly, and annual frequency
|
|
18
19
|
|
|
19
20
|
Returns
|
|
20
21
|
-------
|
|
21
|
-
pd.Series
|
|
22
|
-
|
|
22
|
+
pd.Series or pd.DataFrame
|
|
23
|
+
Log returns at the specified frequency. If period="all",
|
|
24
|
+
returns a single-row DataFrame summarizing multiple horizons.
|
|
23
25
|
"""
|
|
24
|
-
|
|
25
|
-
# Check if the input is a Series or DataFrame
|
|
26
26
|
if not isinstance(data, (pd.DataFrame, pd.Series)):
|
|
27
27
|
raise ValueError("Input must be a pandas DataFrame or Series")
|
|
28
|
+
|
|
29
|
+
# Resampling requires a time-based index
|
|
30
|
+
if period is not None and period != "all":
|
|
31
|
+
if not isinstance(data.index, (pd.DatetimeIndex, pd.PeriodIndex)):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Data must have a DatetimeIndex or PeriodIndex for resampling"
|
|
34
|
+
)
|
|
28
35
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
else:
|
|
33
|
-
temp = pd.Series(dtype=float)
|
|
36
|
+
# Mapping of input period to pandas frequency aliases
|
|
37
|
+
# Using 'ME', 'QE', 'YE' to follow modern pandas standards
|
|
38
|
+
freq_map = {"W": "W", "M": "ME", "Q": "QE", "A": "YE", "Y": "YE"}
|
|
34
39
|
|
|
35
40
|
if period is None:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
elif period == "W":
|
|
39
|
-
data = data.resample("W").last()
|
|
40
|
-
temp = np.log(data).diff()
|
|
41
|
+
# Standard daily log return
|
|
42
|
+
return np.log(data).diff()
|
|
41
43
|
|
|
42
|
-
elif period
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
elif period == "Q":
|
|
47
|
-
data = data.resample("QE").last()
|
|
48
|
-
temp = np.log(data).diff()
|
|
49
|
-
|
|
50
|
-
elif period == "A":
|
|
51
|
-
data = data.resample("YE").last()
|
|
52
|
-
temp = np.log(data).diff()
|
|
44
|
+
elif period in freq_map:
|
|
45
|
+
# Resample to the end of the period, then calculate log return
|
|
46
|
+
resampled_data = data.resample(freq_map[period]).last()
|
|
47
|
+
return np.log(resampled_data).diff()
|
|
53
48
|
|
|
54
49
|
elif period == "all":
|
|
55
|
-
if isinstance(data, pd.Series):
|
|
56
|
-
temp = pd.DataFrame(
|
|
57
|
-
{
|
|
58
|
-
"daily": dailyReturn(data).iloc[-1],
|
|
59
|
-
"weekly": weeklyReturn(data).iloc[-1],
|
|
60
|
-
"monthly": monthlyReturn(data).iloc[-1],
|
|
61
|
-
"quarterly": quarterlyReturn(data).iloc[-1],
|
|
62
|
-
"annual": annualReturn(data).iloc[-1],
|
|
63
|
-
},
|
|
64
|
-
index=[data.index[-1]],
|
|
65
|
-
)
|
|
66
|
-
return temp
|
|
67
|
-
else:
|
|
50
|
+
if not isinstance(data, pd.Series):
|
|
68
51
|
raise ValueError("Please pass a Series for period 'all'")
|
|
52
|
+
|
|
53
|
+
# Construct summary using recursive calls or defined helpers
|
|
54
|
+
return pd.DataFrame({
|
|
55
|
+
"daily": periodReturn(data).iloc[-1],
|
|
56
|
+
"weekly": periodReturn(data, "W").iloc[-1],
|
|
57
|
+
"monthly": periodReturn(data, "M").iloc[-1],
|
|
58
|
+
"quarterly": periodReturn(data, "Q").iloc[-1],
|
|
59
|
+
"annual": periodReturn(data, "A").iloc[-1],
|
|
60
|
+
}, index=[data.index[-1]])
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(f"Invalid period '{period}' provided.")
|
|
64
|
+
|
|
72
65
|
|
|
73
66
|
def dailyReturn(data: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series:
|
|
74
|
-
"""Calculates daily returns
|
|
67
|
+
"""Calculates daily log returns."""
|
|
75
68
|
return periodReturn(data)
|
|
76
69
|
|
|
77
70
|
|
|
78
71
|
def weeklyReturn(data: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series:
|
|
79
|
-
"""Calculates weekly returns
|
|
72
|
+
"""Calculates weekly log returns."""
|
|
80
73
|
return periodReturn(data, period="W")
|
|
81
74
|
|
|
82
75
|
|
|
83
76
|
def monthlyReturn(data: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series:
|
|
84
|
-
"""Calculates monthly returns
|
|
77
|
+
"""Calculates monthly log returns."""
|
|
85
78
|
return periodReturn(data, period="M")
|
|
86
79
|
|
|
87
80
|
|
|
88
81
|
def quarterlyReturn(data: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series:
|
|
89
|
-
"""Calculates quarterly returns
|
|
82
|
+
"""Calculates quarterly log returns."""
|
|
90
83
|
return periodReturn(data, period="Q")
|
|
91
84
|
|
|
92
85
|
|
|
93
86
|
def annualReturn(data: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series:
|
|
94
|
-
"""Calculates annual returns
|
|
87
|
+
"""Calculates annual log returns."""
|
|
95
88
|
return periodReturn(data, period="A")
|
|
96
89
|
|
|
97
90
|
|
|
98
|
-
def allReturn(data: pd.Series) -> pd.
|
|
99
|
-
"""
|
|
91
|
+
def allReturn(data: pd.Series) -> pd.DataFrame:
|
|
92
|
+
"""Returns a snapshot of latest log returns across multiple horizons."""
|
|
100
93
|
return periodReturn(data, period="all")
|
|
101
94
|
|
|
102
95
|
|
|
103
96
|
def rollingReturn(
|
|
104
97
|
data: pd.DataFrame | pd.Series, window: int = 10
|
|
105
98
|
) -> pd.DataFrame | pd.Series:
|
|
106
|
-
"""Calculates rolling returns
|
|
99
|
+
"""Calculates rolling log returns over the specified window."""
|
|
107
100
|
return dailyReturn(data).rolling(window).sum()
|
|
108
101
|
|
|
109
102
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.1.3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: quantmod
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Quantmod Python Package
|
|
5
5
|
Home-page: https://kannansingaravelu.com/
|
|
6
6
|
Author: Kannan Singaravelu
|
|
@@ -28,9 +28,10 @@ Requires-Dist: plotly>=6.1.2
|
|
|
28
28
|
Requires-Dist: pydantic>=2.8.2
|
|
29
29
|
Requires-Dist: scipy>=1.13.1
|
|
30
30
|
Requires-Dist: sqlalchemy>=2.0.38
|
|
31
|
+
Requires-Dist: supabase>=2.27.2
|
|
31
32
|
Requires-Dist: tabulate>=0.9.0
|
|
32
33
|
Requires-Dist: urllib3==1.26.15
|
|
33
|
-
Requires-Dist: yfinance
|
|
34
|
+
Requires-Dist: yfinance>=1.0
|
|
34
35
|
Dynamic: author
|
|
35
36
|
Dynamic: author-email
|
|
36
37
|
Dynamic: classifier
|
|
@@ -21,6 +21,8 @@ quantmod/datasets/__init__.py
|
|
|
21
21
|
quantmod/datasets/dataloader.py
|
|
22
22
|
quantmod/datasets/data/nifty50.csv
|
|
23
23
|
quantmod/datasets/data/spx.csv
|
|
24
|
+
quantmod/db/__init__.py
|
|
25
|
+
quantmod/db/database.py
|
|
24
26
|
quantmod/derivatives/__init__.py
|
|
25
27
|
quantmod/derivatives/nse.py
|
|
26
28
|
quantmod/indicators/__init__.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version = "0.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|