oq-core 0.1.0__py3-none-any.whl

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.
oq_core/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """oq-core: shared primitives for the OpenQuant India ecosystem."""
2
+
3
+ from oq_core.calendar import TradingCalendar
4
+ from oq_core.instrument import Exchange, Instrument, Segment
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = [
9
+ "Exchange",
10
+ "Instrument",
11
+ "Segment",
12
+ "TradingCalendar",
13
+ "__version__",
14
+ ]
oq_core/calendar.py ADDED
@@ -0,0 +1,192 @@
1
+ """NSE trading calendar.
2
+
3
+ This module implements a minimal-but-correct NSE equity trading calendar:
4
+
5
+ * Mon-Fri are trading days.
6
+ * Saturday and Sunday are non-trading days.
7
+ * A curated set of NSE trading holidays is loaded from
8
+ :data:`HOLIDAYS_BY_YEAR`.
9
+ * Muhurat (Diwali) sessions are special trading days where the date would
10
+ otherwise be a holiday or where only an evening session is open.
11
+
12
+ The calendar API is intentionally small and deterministic so that ``oq-data``
13
+ and ``oq-backtest`` can rely on it without pulling pandas at import time.
14
+
15
+ Holiday lists are maintained by year. They are best-effort and should be
16
+ verified against the NSE annual circular for any production usage.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from collections.abc import Iterator
22
+ from dataclasses import dataclass
23
+ from datetime import date, datetime, time, timedelta
24
+
25
+ NSE_OPEN = time(9, 15)
26
+ NSE_CLOSE = time(15, 30)
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class MuhuratSession:
31
+ """A Diwali muhurat trading session (evening, typically ~1 hour)."""
32
+
33
+ session_date: date
34
+ open_time: time
35
+ close_time: time
36
+
37
+
38
+ # NSE trading holidays. Source: NSE annual holiday circulars.
39
+ # Only includes fully-closed equity trading days (not settlement holidays).
40
+ # This list is best-effort; consumers should verify against the NSE circular.
41
+ HOLIDAYS_BY_YEAR: dict[int, frozenset[date]] = {
42
+ 2023: frozenset(
43
+ {
44
+ date(2023, 1, 26), # Republic Day
45
+ date(2023, 3, 7), # Holi
46
+ date(2023, 3, 30), # Ram Navami
47
+ date(2023, 4, 4), # Mahavir Jayanti
48
+ date(2023, 4, 7), # Good Friday
49
+ date(2023, 4, 14), # Dr. Ambedkar Jayanti
50
+ date(2023, 5, 1), # Maharashtra Day
51
+ date(2023, 6, 28), # Bakri Id
52
+ date(2023, 8, 15), # Independence Day
53
+ date(2023, 9, 19), # Ganesh Chaturthi
54
+ date(2023, 10, 2), # Gandhi Jayanti
55
+ date(2023, 10, 24), # Dussehra
56
+ date(2023, 11, 14), # Diwali Balipratipada
57
+ date(2023, 11, 27), # Guru Nanak Jayanti
58
+ date(2023, 12, 25), # Christmas
59
+ }
60
+ ),
61
+ 2024: frozenset(
62
+ {
63
+ date(2024, 1, 26), # Republic Day
64
+ date(2024, 3, 8), # Mahashivratri
65
+ date(2024, 3, 25), # Holi
66
+ date(2024, 3, 29), # Good Friday
67
+ date(2024, 4, 11), # Id-Ul-Fitr
68
+ date(2024, 4, 17), # Ram Navami
69
+ date(2024, 5, 1), # Maharashtra Day
70
+ date(2024, 5, 20), # General Elections (Mumbai)
71
+ date(2024, 6, 17), # Bakri Id
72
+ date(2024, 7, 17), # Muharram
73
+ date(2024, 8, 15), # Independence Day
74
+ date(2024, 10, 2), # Gandhi Jayanti
75
+ date(2024, 11, 1), # Diwali Laxmi Pujan (full holiday; muhurat session in evening)
76
+ date(2024, 11, 15), # Guru Nanak Jayanti
77
+ date(2024, 12, 25), # Christmas
78
+ }
79
+ ),
80
+ 2025: frozenset(
81
+ {
82
+ date(2025, 2, 26), # Mahashivratri
83
+ date(2025, 3, 14), # Holi
84
+ date(2025, 3, 31), # Id-Ul-Fitr
85
+ date(2025, 4, 10), # Mahavir Jayanti
86
+ date(2025, 4, 14), # Dr. Ambedkar Jayanti
87
+ date(2025, 4, 18), # Good Friday
88
+ date(2025, 5, 1), # Maharashtra Day
89
+ date(2025, 8, 15), # Independence Day
90
+ date(2025, 8, 27), # Ganesh Chaturthi
91
+ date(2025, 10, 2), # Gandhi Jayanti / Dussehra
92
+ date(2025, 10, 21), # Diwali Laxmi Pujan (muhurat session in evening)
93
+ date(2025, 10, 22), # Diwali Balipratipada
94
+ date(2025, 11, 5), # Guru Nanak Jayanti
95
+ date(2025, 12, 25), # Christmas
96
+ }
97
+ ),
98
+ }
99
+
100
+
101
+ # Muhurat (Diwali) trading sessions. These are evening sessions on what would
102
+ # otherwise be a non-trading day.
103
+ MUHURAT_SESSIONS: tuple[MuhuratSession, ...] = (
104
+ MuhuratSession(date(2023, 11, 12), time(18, 15), time(19, 15)),
105
+ MuhuratSession(date(2024, 11, 1), time(18, 0), time(19, 0)),
106
+ MuhuratSession(date(2025, 10, 21), time(13, 45), time(14, 45)),
107
+ )
108
+
109
+
110
+ class TradingCalendar:
111
+ """NSE equity trading calendar.
112
+
113
+ Parameters
114
+ ----------
115
+ holidays:
116
+ Optional override mapping of year to holiday set. Defaults to the
117
+ bundled :data:`HOLIDAYS_BY_YEAR`.
118
+ muhurat:
119
+ Optional override of muhurat sessions. Defaults to
120
+ :data:`MUHURAT_SESSIONS`.
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ holidays: dict[int, frozenset[date]] | None = None,
126
+ muhurat: tuple[MuhuratSession, ...] | None = None,
127
+ ) -> None:
128
+ self._holidays: dict[int, frozenset[date]] = (
129
+ dict(holidays) if holidays is not None else dict(HOLIDAYS_BY_YEAR)
130
+ )
131
+ self._muhurat: dict[date, MuhuratSession] = {
132
+ session.session_date: session
133
+ for session in (muhurat if muhurat is not None else MUHURAT_SESSIONS)
134
+ }
135
+
136
+ def is_weekend(self, day: date) -> bool:
137
+ return day.weekday() >= 5
138
+
139
+ def is_holiday(self, day: date) -> bool:
140
+ """True if ``day`` is on the published NSE holiday list."""
141
+ return day in self._holidays.get(day.year, frozenset())
142
+
143
+ def muhurat_session(self, day: date) -> MuhuratSession | None:
144
+ """Return the muhurat session for ``day``, if any."""
145
+ return self._muhurat.get(day)
146
+
147
+ def is_session(self, day: date) -> bool:
148
+ """True if regular trading happens on ``day`` (excludes muhurat-only days)."""
149
+ if self.is_weekend(day):
150
+ return False
151
+ return not self.is_holiday(day)
152
+
153
+ def is_trading_day(self, day: date) -> bool:
154
+ """True if any trading occurs on ``day`` (regular session OR muhurat)."""
155
+ if self.is_session(day):
156
+ return True
157
+ return day in self._muhurat
158
+
159
+ def next_session(self, day: date) -> date:
160
+ """Smallest ``d > day`` that is a regular session."""
161
+ candidate = day + timedelta(days=1)
162
+ while not self.is_session(candidate):
163
+ candidate += timedelta(days=1)
164
+ return candidate
165
+
166
+ def previous_session(self, day: date) -> date:
167
+ """Largest ``d < day`` that is a regular session."""
168
+ candidate = day - timedelta(days=1)
169
+ while not self.is_session(candidate):
170
+ candidate -= timedelta(days=1)
171
+ return candidate
172
+
173
+ def sessions(self, start: date, end: date) -> Iterator[date]:
174
+ """Yield regular trading sessions in ``[start, end]`` inclusive."""
175
+ if end < start:
176
+ return
177
+ current = start
178
+ while current <= end:
179
+ if self.is_session(current):
180
+ yield current
181
+ current += timedelta(days=1)
182
+
183
+ def session_count(self, start: date, end: date) -> int:
184
+ """Number of regular trading sessions in ``[start, end]`` inclusive."""
185
+ return sum(1 for _ in self.sessions(start, end))
186
+
187
+ def is_market_open(self, when: datetime) -> bool:
188
+ """True if the regular cash market is open at ``when`` (naive local IST)."""
189
+ day = when.date()
190
+ if not self.is_session(day):
191
+ return False
192
+ return NSE_OPEN <= when.time() <= NSE_CLOSE
oq_core/instrument.py ADDED
@@ -0,0 +1,92 @@
1
+ """Instrument model: a typed representation of an exchange-listed instrument."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from enum import StrEnum
8
+
9
+ _ISIN_PATTERN = re.compile(r"^[A-Z]{2}[A-Z0-9]{9}\d$")
10
+ _SYMBOL_PATTERN = re.compile(r"^[A-Z0-9][A-Z0-9&\-\.]{0,49}$")
11
+
12
+
13
+ class Exchange(StrEnum):
14
+ """Indian exchanges supported by OpenQuant."""
15
+
16
+ NSE = "NSE"
17
+ BSE = "BSE"
18
+
19
+
20
+ class Segment(StrEnum):
21
+ """Market segments within an exchange."""
22
+
23
+ EQ = "EQ"
24
+ FUT = "FUT"
25
+ OPT = "OPT"
26
+ CDS = "CDS"
27
+ COM = "COM"
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class Instrument:
32
+ """A single exchange-listed instrument.
33
+
34
+ Identity is ``(exchange, segment, symbol)``. ``isin`` is the canonical
35
+ cross-exchange identifier used to follow corporate actions and renames
36
+ (e.g. the HDFC/HDFC Bank merger).
37
+
38
+ Parameters
39
+ ----------
40
+ symbol:
41
+ Trading symbol as listed on the exchange (uppercase). For NSE EQ this
42
+ is the tradingsymbol (e.g. ``"RELIANCE"``).
43
+ isin:
44
+ 12-character ISIN (e.g. ``"INE002A01018"``). Required for equities;
45
+ optional for derivatives.
46
+ exchange:
47
+ Listing exchange.
48
+ segment:
49
+ Market segment (EQ, FUT, OPT, CDS, COM).
50
+ lot_size:
51
+ Minimum tradable quantity. ``1`` for cash equities, varies for F&O.
52
+ tick_size:
53
+ Minimum price increment in INR. Defaults to ``0.05``.
54
+ name:
55
+ Human-readable company / contract name.
56
+ """
57
+
58
+ symbol: str
59
+ exchange: Exchange = Exchange.NSE
60
+ segment: Segment = Segment.EQ
61
+ isin: str | None = None
62
+ lot_size: int = 1
63
+ tick_size: float = 0.05
64
+ name: str | None = None
65
+
66
+ def __post_init__(self) -> None:
67
+ if not isinstance(self.symbol, str) or not self.symbol:
68
+ raise ValueError("symbol must be a non-empty string")
69
+ if not _SYMBOL_PATTERN.match(self.symbol):
70
+ raise ValueError(
71
+ f"invalid symbol {self.symbol!r}: must be uppercase alphanumeric "
72
+ "(plus & - .), up to 50 chars"
73
+ )
74
+ if self.isin is not None and not _ISIN_PATTERN.match(self.isin):
75
+ raise ValueError(
76
+ f"invalid ISIN {self.isin!r}: expected 12 chars matching "
77
+ "country(2) + alphanumeric(9) + checksum(1)"
78
+ )
79
+ if self.segment is Segment.EQ and self.isin is None:
80
+ raise ValueError("equity instruments require an ISIN")
81
+ if self.lot_size < 1:
82
+ raise ValueError(f"lot_size must be >= 1, got {self.lot_size}")
83
+ if self.tick_size <= 0:
84
+ raise ValueError(f"tick_size must be > 0, got {self.tick_size}")
85
+
86
+ @property
87
+ def key(self) -> tuple[str, str, str]:
88
+ """Stable identity tuple suitable for dict keys."""
89
+ return (self.exchange.value, self.segment.value, self.symbol)
90
+
91
+ def __str__(self) -> str:
92
+ return f"{self.exchange.value}:{self.segment.value}:{self.symbol}"
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: oq-core
3
+ Version: 0.1.0
4
+ Summary: Shared primitives for the OpenQuant India ecosystem: Instrument, TradingCalendar, config.
5
+ Project-URL: Homepage, https://github.com/revorhq/openquant
6
+ Project-URL: Repository, https://github.com/revorhq/openquant
7
+ Project-URL: Issues, https://github.com/revorhq/openquant/issues
8
+ Author: OpenQuant India Contributors
9
+ License: Apache-2.0
10
+ Keywords: backtesting,finance,india,nse,quant,trading
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+
23
+ # oq-core
24
+
25
+ Shared primitives for the OpenQuant India ecosystem.
26
+
27
+ - `Instrument` — typed model for an exchange-listed instrument (symbol, ISIN, exchange, segment, lot size).
28
+ - `TradingCalendar` — NSE trading calendar with holidays, weekends, and muhurat sessions.
29
+
30
+ Install:
31
+
32
+ ```bash
33
+ pip install oq-core
34
+ ```
35
+
36
+ See the [main repository](https://github.com/openquant-india/openquant) for the full project, license, and disclaimers.
@@ -0,0 +1,6 @@
1
+ oq_core/__init__.py,sha256=p2Q_VxgUTijbNtvJUsMbrlbzqJrUzO270K48YGfyPlY,304
2
+ oq_core/calendar.py,sha256=56xPuwsMsUKGcho2ywZGtV4zNZiK7X46gjzlUSmTTGY,7180
3
+ oq_core/instrument.py,sha256=gLez-KM5bsUj51rypSI64TEsN1mYMGhYH__AkW_PEMQ,2963
4
+ oq_core-0.1.0.dist-info/METADATA,sha256=5OtMJmE9h9yVmGshctWmVh6f5FPbGOYcdg-6X3HiqtY,1422
5
+ oq_core-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ oq_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any