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 +14 -0
- oq_core/calendar.py +192 -0
- oq_core/instrument.py +92 -0
- oq_core-0.1.0.dist-info/METADATA +36 -0
- oq_core-0.1.0.dist-info/RECORD +6 -0
- oq_core-0.1.0.dist-info/WHEEL +4 -0
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,,
|