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.
Files changed (46) hide show
  1. {quantmod-0.1.1 → quantmod-0.1.3}/PKG-INFO +3 -2
  2. quantmod-0.1.3/quantmod/db/__init__.py +23 -0
  3. quantmod-0.1.3/quantmod/db/database.py +514 -0
  4. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/derivatives/nse.py +24 -109
  5. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/markets/yahoo.py +73 -68
  6. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/optioninputs.py +43 -0
  7. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/timeseries/performance.py +44 -51
  8. quantmod-0.1.3/quantmod/version.py +1 -0
  9. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/PKG-INFO +3 -2
  10. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/SOURCES.txt +2 -0
  11. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/requires.txt +2 -1
  12. quantmod-0.1.1/quantmod/version.py +0 -1
  13. {quantmod-0.1.1 → quantmod-0.1.3}/LICENSE.txt +0 -0
  14. {quantmod-0.1.1 → quantmod-0.1.3}/README.md +0 -0
  15. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/__init__.py +0 -0
  16. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/_version.py +0 -0
  17. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/charts/__init__.py +0 -0
  18. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/charts/plotting.py +0 -0
  19. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/charts/themes.py +0 -0
  20. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/__init__.py +0 -0
  21. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/data/nifty50.csv +0 -0
  22. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/data/spx.csv +0 -0
  23. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/datasets/dataloader.py +0 -0
  24. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/derivatives/__init__.py +0 -0
  25. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/indicators/__init__.py +0 -0
  26. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/indicators/indicators.py +0 -0
  27. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/main.py +0 -0
  28. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/markets/__init__.py +0 -0
  29. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/markets/bb.py +0 -0
  30. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/__init__.py +0 -0
  31. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/binomial.py +0 -0
  32. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/blackscholes.py +0 -0
  33. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/models/montecarlo.py +0 -0
  34. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/__init__.py +0 -0
  35. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/var.py +0 -0
  36. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/varbacktest.py +0 -0
  37. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/risk/varinputs.py +0 -0
  38. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/timeseries/__init__.py +0 -0
  39. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/timeseries/timeseries.py +0 -0
  40. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod/utils.py +0 -0
  41. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/dependency_links.txt +0 -0
  42. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/entry_points.txt +0 -0
  43. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/not-zip-safe +0 -0
  44. {quantmod-0.1.1 → quantmod-0.1.3}/quantmod.egg-info/top_level.txt +0 -0
  45. {quantmod-0.1.1 → quantmod-0.1.3}/setup.cfg +0 -0
  46. {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.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==0.2.58
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
- # improvised from nsepython
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-indices?symbol="
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-equities?symbol="
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) for last traded price
258
- - 'sell' for bid price
259
- - 'buy' for ask price
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]["bidprice"]
185
+ return self.payload["records"]["data"][x][optionType]["sellPrice1"]
274
186
  if intent == "buy":
275
- return self.payload["records"]["data"][x][optionType]["askPrice"]
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
- if i["expiryDate"] == self.expiry_dt:
290
- try:
291
- ce_oi += i["CE"]["openInterest"]
292
- pe_oi += i["PE"]["openInterest"]
293
- except KeyError:
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 = "1mo",
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
- start : str, optional
23
+ start_date : str, optional
87
24
  start date, by default None
88
- end : str, optional
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 for the specified inputs
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
- Sepcifiy W, M, Q and Y for weekly, monthly, quarterly and annual frequency
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
- resampled dataframe series of log returns
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
- # Initialize variable to hold log returns
30
- if isinstance(data, pd.DataFrame):
31
- temp = pd.DataFrame(dtype=float)
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
- temp = np.log(data).diff()
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 == "M":
43
- data = data.resample("ME").last()
44
- temp = np.log(data).diff()
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
- return temp
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 for the specified inputs."""
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 for the specified inputs."""
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 for the specified inputs."""
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 for the specified inputs."""
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 for the specified inputs."""
87
+ """Calculates annual log returns."""
95
88
  return periodReturn(data, period="A")
96
89
 
97
90
 
98
- def allReturn(data: pd.Series) -> pd.Series:
99
- """Calculates annual returns for the specified inputs."""
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 for the specified inputs."""
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.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==0.2.58
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
@@ -7,6 +7,7 @@ plotly>=6.1.2
7
7
  pydantic>=2.8.2
8
8
  scipy>=1.13.1
9
9
  sqlalchemy>=2.0.38
10
+ supabase>=2.27.2
10
11
  tabulate>=0.9.0
11
12
  urllib3==1.26.15
12
- yfinance==0.2.58
13
+ yfinance>=1.0
@@ -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