quantmod 0.1.2__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.2 → 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.2 → quantmod-0.1.3}/quantmod/markets/yahoo.py +73 -68
  5. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/models/optioninputs.py +43 -0
  6. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/timeseries/performance.py +44 -51
  7. quantmod-0.1.3/quantmod/version.py +1 -0
  8. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/PKG-INFO +3 -2
  9. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/SOURCES.txt +2 -0
  10. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/requires.txt +2 -1
  11. quantmod-0.1.2/quantmod/version.py +0 -1
  12. {quantmod-0.1.2 → quantmod-0.1.3}/LICENSE.txt +0 -0
  13. {quantmod-0.1.2 → quantmod-0.1.3}/README.md +0 -0
  14. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/__init__.py +0 -0
  15. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/_version.py +0 -0
  16. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/charts/__init__.py +0 -0
  17. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/charts/plotting.py +0 -0
  18. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/charts/themes.py +0 -0
  19. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/datasets/__init__.py +0 -0
  20. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/datasets/data/nifty50.csv +0 -0
  21. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/datasets/data/spx.csv +0 -0
  22. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/datasets/dataloader.py +0 -0
  23. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/derivatives/__init__.py +0 -0
  24. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/derivatives/nse.py +0 -0
  25. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/indicators/__init__.py +0 -0
  26. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/indicators/indicators.py +0 -0
  27. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/main.py +0 -0
  28. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/markets/__init__.py +0 -0
  29. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/markets/bb.py +0 -0
  30. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/models/__init__.py +0 -0
  31. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/models/binomial.py +0 -0
  32. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/models/blackscholes.py +0 -0
  33. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/models/montecarlo.py +0 -0
  34. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/risk/__init__.py +0 -0
  35. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/risk/var.py +0 -0
  36. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/risk/varbacktest.py +0 -0
  37. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/risk/varinputs.py +0 -0
  38. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/timeseries/__init__.py +0 -0
  39. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/timeseries/timeseries.py +0 -0
  40. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod/utils.py +0 -0
  41. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/dependency_links.txt +0 -0
  42. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/entry_points.txt +0 -0
  43. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/not-zip-safe +0 -0
  44. {quantmod-0.1.2 → quantmod-0.1.3}/quantmod.egg-info/top_level.txt +0 -0
  45. {quantmod-0.1.2 → quantmod-0.1.3}/setup.cfg +0 -0
  46. {quantmod-0.1.2 → 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.2
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
@@ -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.2
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.2"
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