@lacspace/market-clock 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lacspace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function toMinutes(hhmm) {
5
+ const [h, m] = hhmm.split(":").map(Number);
6
+ return (h || 0) * 60 + (m || 0);
7
+ }
8
+ function pad(n) {
9
+ return n < 10 ? "0" + n : String(n);
10
+ }
11
+ var MarketClock = class {
12
+ constructor(spec) {
13
+ this.spec = spec;
14
+ this.regOpen = toMinutes(spec.regular.open);
15
+ this.regClose = toMinutes(spec.regular.close);
16
+ this.preOpen = spec.preOpen ? toMinutes(spec.preOpen.open) : null;
17
+ this.preClose = spec.preOpen ? toMinutes(spec.preOpen.close) : null;
18
+ }
19
+ /** Exchange-local calendar parts for an instant. */
20
+ parts(date) {
21
+ const local = new Date(date.getTime() + this.spec.offsetMinutes * 6e4);
22
+ return {
23
+ y: local.getUTCFullYear(),
24
+ m: local.getUTCMonth() + 1,
25
+ d: local.getUTCDate(),
26
+ day: local.getUTCDay(),
27
+ minute: local.getUTCHours() * 60 + local.getUTCMinutes()
28
+ };
29
+ }
30
+ /** UTC epoch millis for an exchange-local calendar day + minute-of-day. */
31
+ utcFor(y, m, d, minute) {
32
+ return Date.UTC(y, m - 1, d) + minute * 6e4 - this.spec.offsetMinutes * 6e4;
33
+ }
34
+ isoDate(p) {
35
+ return `${p.y}-${pad(p.m)}-${pad(p.d)}`;
36
+ }
37
+ isWeekend(at = /* @__PURE__ */ new Date()) {
38
+ return this.spec.weekend.includes(this.parts(at).day);
39
+ }
40
+ isHoliday(at = /* @__PURE__ */ new Date()) {
41
+ return this.spec.holidays.includes(this.isoDate(this.parts(at)));
42
+ }
43
+ /** A day the exchange trades (not weekend, not a holiday). */
44
+ isTradingDay(at = /* @__PURE__ */ new Date()) {
45
+ return !this.isWeekend(at) && !this.isHoliday(at);
46
+ }
47
+ isOpen(at = /* @__PURE__ */ new Date()) {
48
+ if (!this.isTradingDay(at)) return false;
49
+ const { minute } = this.parts(at);
50
+ return minute >= this.regOpen && minute < this.regClose;
51
+ }
52
+ isPreOpen(at = /* @__PURE__ */ new Date()) {
53
+ if (this.preOpen === null || this.preClose === null) return false;
54
+ if (!this.isTradingDay(at)) return false;
55
+ const { minute } = this.parts(at);
56
+ return minute >= this.preOpen && minute < this.preClose;
57
+ }
58
+ status(at = /* @__PURE__ */ new Date()) {
59
+ if (this.isOpen(at)) return "open";
60
+ if (this.isPreOpen(at)) return "pre-open";
61
+ return "closed";
62
+ }
63
+ /** The next moment the regular session opens, at or after `from`. */
64
+ nextOpen(from = /* @__PURE__ */ new Date()) {
65
+ const start = this.parts(from);
66
+ for (let i = 0; i < 500; i++) {
67
+ const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 864e5;
68
+ const dp = new Date(dayMs);
69
+ const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };
70
+ if (this.spec.weekend.includes(dp.getUTCDay())) continue;
71
+ if (this.spec.holidays.includes(this.isoDate(p))) continue;
72
+ const openUtc = this.utcFor(p.y, p.m, p.d, this.regOpen);
73
+ if (openUtc > from.getTime()) return new Date(openUtc);
74
+ }
75
+ throw new Error("no trading day found within 500 days");
76
+ }
77
+ /** The next moment the regular session closes, at or after `from`. */
78
+ nextClose(from = /* @__PURE__ */ new Date()) {
79
+ const start = this.parts(from);
80
+ for (let i = 0; i < 500; i++) {
81
+ const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 864e5;
82
+ const dp = new Date(dayMs);
83
+ const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };
84
+ if (this.spec.weekend.includes(dp.getUTCDay())) continue;
85
+ if (this.spec.holidays.includes(this.isoDate(p))) continue;
86
+ const closeUtc = this.utcFor(p.y, p.m, p.d, this.regClose);
87
+ if (closeUtc > from.getTime()) return new Date(closeUtc);
88
+ }
89
+ throw new Error("no trading day found within 500 days");
90
+ }
91
+ /** Milliseconds until the session closes, or 0 if not currently open. */
92
+ msToClose(at = /* @__PURE__ */ new Date()) {
93
+ if (!this.isOpen(at)) return 0;
94
+ return this.nextClose(at).getTime() - at.getTime();
95
+ }
96
+ /** Milliseconds until the next open, or 0 if already open. */
97
+ msToOpen(at = /* @__PURE__ */ new Date()) {
98
+ if (this.isOpen(at)) return 0;
99
+ return this.nextOpen(at).getTime() - at.getTime();
100
+ }
101
+ };
102
+ function createClock(spec) {
103
+ return new MarketClock(spec);
104
+ }
105
+ var NSE_HOLIDAYS = [
106
+ // 2025
107
+ "2025-02-26",
108
+ "2025-03-14",
109
+ "2025-03-31",
110
+ "2025-04-10",
111
+ "2025-04-14",
112
+ "2025-04-18",
113
+ "2025-05-01",
114
+ "2025-08-15",
115
+ "2025-08-27",
116
+ "2025-10-02",
117
+ "2025-10-21",
118
+ "2025-10-22",
119
+ "2025-11-05",
120
+ "2025-12-25",
121
+ // 2026 (nationally-fixed observances; confirm exchange circular for the rest)
122
+ "2026-01-26",
123
+ "2026-03-04",
124
+ "2026-04-01",
125
+ "2026-05-01",
126
+ "2026-10-02",
127
+ "2026-11-09",
128
+ "2026-12-25"
129
+ ];
130
+ var NSE = {
131
+ name: "NSE",
132
+ offsetMinutes: 330,
133
+ preOpen: { open: "09:00", close: "09:15" },
134
+ regular: { open: "09:15", close: "15:30" },
135
+ weekend: [0, 6],
136
+ holidays: NSE_HOLIDAYS
137
+ };
138
+ var BSE = {
139
+ ...NSE,
140
+ name: "BSE"
141
+ };
142
+
143
+ exports.BSE = BSE;
144
+ exports.MarketClock = MarketClock;
145
+ exports.NSE = NSE;
146
+ exports.createClock = createClock;
147
+ //# sourceMappingURL=index.cjs.map
148
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AA+BA,SAAS,UAAU,IAAA,EAAsB;AACvC,EAAA,MAAM,CAAC,GAAG,CAAC,CAAA,GAAI,KAAK,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,MAAM,CAAA;AACzC,EAAA,OAAA,CAAQ,CAAA,IAAK,CAAA,IAAK,EAAA,IAAM,CAAA,IAAK,CAAA,CAAA;AAC/B;AAEA,SAAS,IAAI,CAAA,EAAmB;AAC9B,EAAA,OAAO,CAAA,GAAI,EAAA,GAAK,GAAA,GAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AACpC;AAUO,IAAM,cAAN,MAAkB;AAAA,EAMvB,YAA4B,IAAA,EAAoB;AAApB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,SAAA,CAAU,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAA;AAC1C,IAAA,IAAA,CAAK,QAAA,GAAW,SAAA,CAAU,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA;AAC5C,IAAA,IAAA,CAAK,UAAU,IAAA,CAAK,OAAA,GAAU,UAAU,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAA,GAAI,IAAA;AAC7D,IAAA,IAAA,CAAK,WAAW,IAAA,CAAK,OAAA,GAAU,UAAU,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA,GAAI,IAAA;AAAA,EACjE;AAAA;AAAA,EAGQ,MAAM,IAAA,EAAwB;AACpC,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,IAAA,CAAK,SAAQ,GAAI,IAAA,CAAK,IAAA,CAAK,aAAA,GAAgB,GAAK,CAAA;AACvE,IAAA,OAAO;AAAA,MACL,CAAA,EAAG,MAAM,cAAA,EAAe;AAAA,MACxB,CAAA,EAAG,KAAA,CAAM,WAAA,EAAY,GAAI,CAAA;AAAA,MACzB,CAAA,EAAG,MAAM,UAAA,EAAW;AAAA,MACpB,GAAA,EAAK,MAAM,SAAA,EAAU;AAAA,MACrB,QAAQ,KAAA,CAAM,WAAA,EAAY,GAAI,EAAA,GAAK,MAAM,aAAA;AAAc,KACzD;AAAA,EACF;AAAA;AAAA,EAGQ,MAAA,CAAO,CAAA,EAAW,CAAA,EAAW,CAAA,EAAW,MAAA,EAAwB;AACtE,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAC,CAAA,GAAI,MAAA,GAAS,GAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,aAAA,GAAgB,GAAA;AAAA,EAC5E;AAAA,EAEQ,QAAQ,CAAA,EAAgD;AAC9D,IAAA,OAAO,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,CAAA,CAAE,CAAC,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,CAAA,CAAE,CAAC,CAAC,CAAA,CAAA;AAAA,EACvC;AAAA,EAEA,SAAA,CAAU,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAY;AACxC,IAAA,OAAO,IAAA,CAAK,KAAK,OAAA,CAAQ,QAAA,CAAS,KAAK,KAAA,CAAM,EAAE,EAAE,GAAG,CAAA;AAAA,EACtD;AAAA,EAEA,SAAA,CAAU,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAY;AACxC,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,QAAA,CAAS,QAAA,CAAS,IAAA,CAAK,QAAQ,IAAA,CAAK,KAAA,CAAM,EAAE,CAAC,CAAC,CAAA;AAAA,EACjE;AAAA;AAAA,EAGA,YAAA,CAAa,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAY;AAC3C,IAAA,OAAO,CAAC,KAAK,SAAA,CAAU,EAAE,KAAK,CAAC,IAAA,CAAK,UAAU,EAAE,CAAA;AAAA,EAClD;AAAA,EAEA,MAAA,CAAO,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAY;AACrC,IAAA,IAAI,CAAC,IAAA,CAAK,YAAA,CAAa,EAAE,GAAG,OAAO,KAAA;AACnC,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,MAAM,EAAE,CAAA;AAChC,IAAA,OAAO,MAAA,IAAU,IAAA,CAAK,OAAA,IAAW,MAAA,GAAS,IAAA,CAAK,QAAA;AAAA,EACjD;AAAA,EAEA,SAAA,CAAU,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAY;AACxC,IAAA,IAAI,KAAK,OAAA,KAAY,IAAA,IAAQ,IAAA,CAAK,QAAA,KAAa,MAAM,OAAO,KAAA;AAC5D,IAAA,IAAI,CAAC,IAAA,CAAK,YAAA,CAAa,EAAE,GAAG,OAAO,KAAA;AACnC,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,MAAM,EAAE,CAAA;AAChC,IAAA,OAAO,MAAA,IAAU,IAAA,CAAK,OAAA,IAAW,MAAA,GAAS,IAAA,CAAK,QAAA;AAAA,EACjD;AAAA,EAEA,MAAA,CAAO,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAiB;AAC1C,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,EAAE,CAAA,EAAG,OAAO,MAAA;AAC5B,IAAA,IAAI,IAAA,CAAK,SAAA,CAAU,EAAE,CAAA,EAAG,OAAO,UAAA;AAC/B,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,QAAA,CAAS,IAAA,mBAAa,IAAI,IAAA,EAAK,EAAS;AACtC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC7B,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,EAAK,CAAA,EAAA,EAAK;AAC5B,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAA,GAAI,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA,GAAI,CAAA,GAAI,KAAA;AAC5D,MAAA,MAAM,EAAA,GAAK,IAAI,IAAA,CAAK,KAAK,CAAA;AACzB,MAAA,MAAM,CAAA,GAAI,EAAE,CAAA,EAAG,EAAA,CAAG,gBAAe,EAAG,CAAA,EAAG,EAAA,CAAG,WAAA,EAAY,GAAI,CAAA,EAAG,CAAA,EAAG,EAAA,CAAG,YAAW,EAAE;AAChF,MAAA,IAAI,KAAK,IAAA,CAAK,OAAA,CAAQ,SAAS,EAAA,CAAG,SAAA,EAAW,CAAA,EAAG;AAChD,MAAA,IAAI,IAAA,CAAK,KAAK,QAAA,CAAS,QAAA,CAAS,KAAK,OAAA,CAAQ,CAAC,CAAC,CAAA,EAAG;AAClD,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,CAAA,CAAE,CAAA,EAAG,EAAE,CAAA,EAAG,CAAA,CAAE,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA;AACvD,MAAA,IAAI,UAAU,IAAA,CAAK,OAAA,IAAW,OAAO,IAAI,KAAK,OAAO,CAAA;AAAA,IACvD;AACA,IAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,EACxD;AAAA;AAAA,EAGA,SAAA,CAAU,IAAA,mBAAa,IAAI,IAAA,EAAK,EAAS;AACvC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC7B,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,EAAK,CAAA,EAAA,EAAK;AAC5B,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAA,GAAI,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA,GAAI,CAAA,GAAI,KAAA;AAC5D,MAAA,MAAM,EAAA,GAAK,IAAI,IAAA,CAAK,KAAK,CAAA;AACzB,MAAA,MAAM,CAAA,GAAI,EAAE,CAAA,EAAG,EAAA,CAAG,gBAAe,EAAG,CAAA,EAAG,EAAA,CAAG,WAAA,EAAY,GAAI,CAAA,EAAG,CAAA,EAAG,EAAA,CAAG,YAAW,EAAE;AAChF,MAAA,IAAI,KAAK,IAAA,CAAK,OAAA,CAAQ,SAAS,EAAA,CAAG,SAAA,EAAW,CAAA,EAAG;AAChD,MAAA,IAAI,IAAA,CAAK,KAAK,QAAA,CAAS,QAAA,CAAS,KAAK,OAAA,CAAQ,CAAC,CAAC,CAAA,EAAG;AAClD,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,MAAA,CAAO,CAAA,CAAE,CAAA,EAAG,EAAE,CAAA,EAAG,CAAA,CAAE,CAAA,EAAG,IAAA,CAAK,QAAQ,CAAA;AACzD,MAAA,IAAI,WAAW,IAAA,CAAK,OAAA,IAAW,OAAO,IAAI,KAAK,QAAQ,CAAA;AAAA,IACzD;AACA,IAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,EACxD;AAAA;AAAA,EAGA,SAAA,CAAU,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAW;AACvC,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,CAAO,EAAE,GAAG,OAAO,CAAA;AAC7B,IAAA,OAAO,KAAK,SAAA,CAAU,EAAE,EAAE,OAAA,EAAQ,GAAI,GAAG,OAAA,EAAQ;AAAA,EACnD;AAAA;AAAA,EAGA,QAAA,CAAS,EAAA,mBAAW,IAAI,IAAA,EAAK,EAAW;AACtC,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,EAAE,CAAA,EAAG,OAAO,CAAA;AAC5B,IAAA,OAAO,KAAK,QAAA,CAAS,EAAE,EAAE,OAAA,EAAQ,GAAI,GAAG,OAAA,EAAQ;AAAA,EAClD;AACF;AAEO,SAAS,YAAY,IAAA,EAAiC;AAC3D,EAAA,OAAO,IAAI,YAAY,IAAI,CAAA;AAC7B;AAOA,IAAM,YAAA,GAAyB;AAAA;AAAA,EAE7B,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EACxD,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EACxD,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA;AAAA,EAE1C,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EAAc,YAAA;AAAA,EACxD,YAAA;AAAA,EAAc;AAChB,CAAA;AAGO,IAAM,GAAA,GAAoB;AAAA,EAC/B,IAAA,EAAM,KAAA;AAAA,EACN,aAAA,EAAe,GAAA;AAAA,EACf,OAAA,EAAS,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,OAAA,EAAQ;AAAA,EACzC,OAAA,EAAS,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,OAAA,EAAQ;AAAA,EACzC,OAAA,EAAS,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EACd,QAAA,EAAU;AACZ;AAGO,IAAM,GAAA,GAAoB;AAAA,EAC/B,GAAG,GAAA;AAAA,EACH,IAAA,EAAM;AACR","file":"index.cjs","sourcesContent":["/**\n * @lacspace/market-clock\n * Is the market open right now? When does it next open / close?\n *\n * A holiday-aware, timezone-correct trading clock for stock exchanges. Ships\n * with NSE / BSE (India) presets — India has no DST, so a fixed IST offset is\n * exact. Bring your own exchange spec or override the holiday list any time.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface Session {\n /** \"HH:MM\" 24-hour, in exchange-local time. */\n open: string;\n close: string;\n}\n\nexport interface ExchangeSpec {\n name: string;\n /** Minutes ahead of UTC (IST = +330). Exchanges without DST only. */\n offsetMinutes: number;\n regular: Session;\n preOpen?: Session;\n /** Weekday numbers that are always closed (0 = Sunday … 6 = Saturday). */\n weekend: number[];\n /** Full-day holidays as \"YYYY-MM-DD\" in exchange-local dates. */\n holidays: string[];\n}\n\nexport type MarketStatus = \"pre-open\" | \"open\" | \"closed\";\n\nfunction toMinutes(hhmm: string): number {\n const [h, m] = hhmm.split(\":\").map(Number);\n return (h || 0) * 60 + (m || 0);\n}\n\nfunction pad(n: number): string {\n return n < 10 ? \"0\" + n : String(n);\n}\n\ninterface LocalParts {\n y: number;\n m: number;\n d: number;\n day: number;\n minute: number;\n}\n\nexport class MarketClock {\n private readonly regOpen: number;\n private readonly regClose: number;\n private readonly preOpen: number | null;\n private readonly preClose: number | null;\n\n constructor(public readonly spec: ExchangeSpec) {\n this.regOpen = toMinutes(spec.regular.open);\n this.regClose = toMinutes(spec.regular.close);\n this.preOpen = spec.preOpen ? toMinutes(spec.preOpen.open) : null;\n this.preClose = spec.preOpen ? toMinutes(spec.preOpen.close) : null;\n }\n\n /** Exchange-local calendar parts for an instant. */\n private parts(date: Date): LocalParts {\n const local = new Date(date.getTime() + this.spec.offsetMinutes * 60000);\n return {\n y: local.getUTCFullYear(),\n m: local.getUTCMonth() + 1,\n d: local.getUTCDate(),\n day: local.getUTCDay(),\n minute: local.getUTCHours() * 60 + local.getUTCMinutes(),\n };\n }\n\n /** UTC epoch millis for an exchange-local calendar day + minute-of-day. */\n private utcFor(y: number, m: number, d: number, minute: number): number {\n return Date.UTC(y, m - 1, d) + minute * 60000 - this.spec.offsetMinutes * 60000;\n }\n\n private isoDate(p: { y: number; m: number; d: number }): string {\n return `${p.y}-${pad(p.m)}-${pad(p.d)}`;\n }\n\n isWeekend(at: Date = new Date()): boolean {\n return this.spec.weekend.includes(this.parts(at).day);\n }\n\n isHoliday(at: Date = new Date()): boolean {\n return this.spec.holidays.includes(this.isoDate(this.parts(at)));\n }\n\n /** A day the exchange trades (not weekend, not a holiday). */\n isTradingDay(at: Date = new Date()): boolean {\n return !this.isWeekend(at) && !this.isHoliday(at);\n }\n\n isOpen(at: Date = new Date()): boolean {\n if (!this.isTradingDay(at)) return false;\n const { minute } = this.parts(at);\n return minute >= this.regOpen && minute < this.regClose;\n }\n\n isPreOpen(at: Date = new Date()): boolean {\n if (this.preOpen === null || this.preClose === null) return false;\n if (!this.isTradingDay(at)) return false;\n const { minute } = this.parts(at);\n return minute >= this.preOpen && minute < this.preClose;\n }\n\n status(at: Date = new Date()): MarketStatus {\n if (this.isOpen(at)) return \"open\";\n if (this.isPreOpen(at)) return \"pre-open\";\n return \"closed\";\n }\n\n /** The next moment the regular session opens, at or after `from`. */\n nextOpen(from: Date = new Date()): Date {\n const start = this.parts(from);\n for (let i = 0; i < 500; i++) {\n const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 86400000;\n const dp = new Date(dayMs);\n const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };\n if (this.spec.weekend.includes(dp.getUTCDay())) continue;\n if (this.spec.holidays.includes(this.isoDate(p))) continue;\n const openUtc = this.utcFor(p.y, p.m, p.d, this.regOpen);\n if (openUtc > from.getTime()) return new Date(openUtc);\n }\n throw new Error(\"no trading day found within 500 days\");\n }\n\n /** The next moment the regular session closes, at or after `from`. */\n nextClose(from: Date = new Date()): Date {\n const start = this.parts(from);\n for (let i = 0; i < 500; i++) {\n const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 86400000;\n const dp = new Date(dayMs);\n const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };\n if (this.spec.weekend.includes(dp.getUTCDay())) continue;\n if (this.spec.holidays.includes(this.isoDate(p))) continue;\n const closeUtc = this.utcFor(p.y, p.m, p.d, this.regClose);\n if (closeUtc > from.getTime()) return new Date(closeUtc);\n }\n throw new Error(\"no trading day found within 500 days\");\n }\n\n /** Milliseconds until the session closes, or 0 if not currently open. */\n msToClose(at: Date = new Date()): number {\n if (!this.isOpen(at)) return 0;\n return this.nextClose(at).getTime() - at.getTime();\n }\n\n /** Milliseconds until the next open, or 0 if already open. */\n msToOpen(at: Date = new Date()): number {\n if (this.isOpen(at)) return 0;\n return this.nextOpen(at).getTime() - at.getTime();\n }\n}\n\nexport function createClock(spec: ExchangeSpec): MarketClock {\n return new MarketClock(spec);\n}\n\n/**\n * NSE trading holidays. Nationally-fixed days are reliable; the rest follow the\n * annual NSE circular and may shift year to year — verify and extend as needed\n * (spread via `new MarketClock({ ...NSE, holidays: [...NSE.holidays, ...] })`).\n */\nconst NSE_HOLIDAYS: string[] = [\n // 2025\n \"2025-02-26\", \"2025-03-14\", \"2025-03-31\", \"2025-04-10\", \"2025-04-14\",\n \"2025-04-18\", \"2025-05-01\", \"2025-08-15\", \"2025-08-27\", \"2025-10-02\",\n \"2025-10-21\", \"2025-10-22\", \"2025-11-05\", \"2025-12-25\",\n // 2026 (nationally-fixed observances; confirm exchange circular for the rest)\n \"2026-01-26\", \"2026-03-04\", \"2026-04-01\", \"2026-05-01\", \"2026-10-02\",\n \"2026-11-09\", \"2026-12-25\",\n];\n\n/** National Stock Exchange of India. Pre-open 09:00–09:15, regular 09:15–15:30 IST. */\nexport const NSE: ExchangeSpec = {\n name: \"NSE\",\n offsetMinutes: 330,\n preOpen: { open: \"09:00\", close: \"09:15\" },\n regular: { open: \"09:15\", close: \"15:30\" },\n weekend: [0, 6],\n holidays: NSE_HOLIDAYS,\n};\n\n/** Bombay Stock Exchange — same session timings and holiday calendar as NSE. */\nexport const BSE: ExchangeSpec = {\n ...NSE,\n name: \"BSE\",\n};\n"]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @lacspace/market-clock
3
+ * Is the market open right now? When does it next open / close?
4
+ *
5
+ * A holiday-aware, timezone-correct trading clock for stock exchanges. Ships
6
+ * with NSE / BSE (India) presets — India has no DST, so a fixed IST offset is
7
+ * exact. Bring your own exchange spec or override the holiday list any time.
8
+ *
9
+ * Zero dependencies · isomorphic · fully typed.
10
+ */
11
+ interface Session {
12
+ /** "HH:MM" 24-hour, in exchange-local time. */
13
+ open: string;
14
+ close: string;
15
+ }
16
+ interface ExchangeSpec {
17
+ name: string;
18
+ /** Minutes ahead of UTC (IST = +330). Exchanges without DST only. */
19
+ offsetMinutes: number;
20
+ regular: Session;
21
+ preOpen?: Session;
22
+ /** Weekday numbers that are always closed (0 = Sunday … 6 = Saturday). */
23
+ weekend: number[];
24
+ /** Full-day holidays as "YYYY-MM-DD" in exchange-local dates. */
25
+ holidays: string[];
26
+ }
27
+ type MarketStatus = "pre-open" | "open" | "closed";
28
+ declare class MarketClock {
29
+ readonly spec: ExchangeSpec;
30
+ private readonly regOpen;
31
+ private readonly regClose;
32
+ private readonly preOpen;
33
+ private readonly preClose;
34
+ constructor(spec: ExchangeSpec);
35
+ /** Exchange-local calendar parts for an instant. */
36
+ private parts;
37
+ /** UTC epoch millis for an exchange-local calendar day + minute-of-day. */
38
+ private utcFor;
39
+ private isoDate;
40
+ isWeekend(at?: Date): boolean;
41
+ isHoliday(at?: Date): boolean;
42
+ /** A day the exchange trades (not weekend, not a holiday). */
43
+ isTradingDay(at?: Date): boolean;
44
+ isOpen(at?: Date): boolean;
45
+ isPreOpen(at?: Date): boolean;
46
+ status(at?: Date): MarketStatus;
47
+ /** The next moment the regular session opens, at or after `from`. */
48
+ nextOpen(from?: Date): Date;
49
+ /** The next moment the regular session closes, at or after `from`. */
50
+ nextClose(from?: Date): Date;
51
+ /** Milliseconds until the session closes, or 0 if not currently open. */
52
+ msToClose(at?: Date): number;
53
+ /** Milliseconds until the next open, or 0 if already open. */
54
+ msToOpen(at?: Date): number;
55
+ }
56
+ declare function createClock(spec: ExchangeSpec): MarketClock;
57
+ /** National Stock Exchange of India. Pre-open 09:00–09:15, regular 09:15–15:30 IST. */
58
+ declare const NSE: ExchangeSpec;
59
+ /** Bombay Stock Exchange — same session timings and holiday calendar as NSE. */
60
+ declare const BSE: ExchangeSpec;
61
+
62
+ export { BSE, type ExchangeSpec, MarketClock, type MarketStatus, NSE, type Session, createClock };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @lacspace/market-clock
3
+ * Is the market open right now? When does it next open / close?
4
+ *
5
+ * A holiday-aware, timezone-correct trading clock for stock exchanges. Ships
6
+ * with NSE / BSE (India) presets — India has no DST, so a fixed IST offset is
7
+ * exact. Bring your own exchange spec or override the holiday list any time.
8
+ *
9
+ * Zero dependencies · isomorphic · fully typed.
10
+ */
11
+ interface Session {
12
+ /** "HH:MM" 24-hour, in exchange-local time. */
13
+ open: string;
14
+ close: string;
15
+ }
16
+ interface ExchangeSpec {
17
+ name: string;
18
+ /** Minutes ahead of UTC (IST = +330). Exchanges without DST only. */
19
+ offsetMinutes: number;
20
+ regular: Session;
21
+ preOpen?: Session;
22
+ /** Weekday numbers that are always closed (0 = Sunday … 6 = Saturday). */
23
+ weekend: number[];
24
+ /** Full-day holidays as "YYYY-MM-DD" in exchange-local dates. */
25
+ holidays: string[];
26
+ }
27
+ type MarketStatus = "pre-open" | "open" | "closed";
28
+ declare class MarketClock {
29
+ readonly spec: ExchangeSpec;
30
+ private readonly regOpen;
31
+ private readonly regClose;
32
+ private readonly preOpen;
33
+ private readonly preClose;
34
+ constructor(spec: ExchangeSpec);
35
+ /** Exchange-local calendar parts for an instant. */
36
+ private parts;
37
+ /** UTC epoch millis for an exchange-local calendar day + minute-of-day. */
38
+ private utcFor;
39
+ private isoDate;
40
+ isWeekend(at?: Date): boolean;
41
+ isHoliday(at?: Date): boolean;
42
+ /** A day the exchange trades (not weekend, not a holiday). */
43
+ isTradingDay(at?: Date): boolean;
44
+ isOpen(at?: Date): boolean;
45
+ isPreOpen(at?: Date): boolean;
46
+ status(at?: Date): MarketStatus;
47
+ /** The next moment the regular session opens, at or after `from`. */
48
+ nextOpen(from?: Date): Date;
49
+ /** The next moment the regular session closes, at or after `from`. */
50
+ nextClose(from?: Date): Date;
51
+ /** Milliseconds until the session closes, or 0 if not currently open. */
52
+ msToClose(at?: Date): number;
53
+ /** Milliseconds until the next open, or 0 if already open. */
54
+ msToOpen(at?: Date): number;
55
+ }
56
+ declare function createClock(spec: ExchangeSpec): MarketClock;
57
+ /** National Stock Exchange of India. Pre-open 09:00–09:15, regular 09:15–15:30 IST. */
58
+ declare const NSE: ExchangeSpec;
59
+ /** Bombay Stock Exchange — same session timings and holiday calendar as NSE. */
60
+ declare const BSE: ExchangeSpec;
61
+
62
+ export { BSE, type ExchangeSpec, MarketClock, type MarketStatus, NSE, type Session, createClock };
package/dist/index.js ADDED
@@ -0,0 +1,143 @@
1
+ // src/index.ts
2
+ function toMinutes(hhmm) {
3
+ const [h, m] = hhmm.split(":").map(Number);
4
+ return (h || 0) * 60 + (m || 0);
5
+ }
6
+ function pad(n) {
7
+ return n < 10 ? "0" + n : String(n);
8
+ }
9
+ var MarketClock = class {
10
+ constructor(spec) {
11
+ this.spec = spec;
12
+ this.regOpen = toMinutes(spec.regular.open);
13
+ this.regClose = toMinutes(spec.regular.close);
14
+ this.preOpen = spec.preOpen ? toMinutes(spec.preOpen.open) : null;
15
+ this.preClose = spec.preOpen ? toMinutes(spec.preOpen.close) : null;
16
+ }
17
+ /** Exchange-local calendar parts for an instant. */
18
+ parts(date) {
19
+ const local = new Date(date.getTime() + this.spec.offsetMinutes * 6e4);
20
+ return {
21
+ y: local.getUTCFullYear(),
22
+ m: local.getUTCMonth() + 1,
23
+ d: local.getUTCDate(),
24
+ day: local.getUTCDay(),
25
+ minute: local.getUTCHours() * 60 + local.getUTCMinutes()
26
+ };
27
+ }
28
+ /** UTC epoch millis for an exchange-local calendar day + minute-of-day. */
29
+ utcFor(y, m, d, minute) {
30
+ return Date.UTC(y, m - 1, d) + minute * 6e4 - this.spec.offsetMinutes * 6e4;
31
+ }
32
+ isoDate(p) {
33
+ return `${p.y}-${pad(p.m)}-${pad(p.d)}`;
34
+ }
35
+ isWeekend(at = /* @__PURE__ */ new Date()) {
36
+ return this.spec.weekend.includes(this.parts(at).day);
37
+ }
38
+ isHoliday(at = /* @__PURE__ */ new Date()) {
39
+ return this.spec.holidays.includes(this.isoDate(this.parts(at)));
40
+ }
41
+ /** A day the exchange trades (not weekend, not a holiday). */
42
+ isTradingDay(at = /* @__PURE__ */ new Date()) {
43
+ return !this.isWeekend(at) && !this.isHoliday(at);
44
+ }
45
+ isOpen(at = /* @__PURE__ */ new Date()) {
46
+ if (!this.isTradingDay(at)) return false;
47
+ const { minute } = this.parts(at);
48
+ return minute >= this.regOpen && minute < this.regClose;
49
+ }
50
+ isPreOpen(at = /* @__PURE__ */ new Date()) {
51
+ if (this.preOpen === null || this.preClose === null) return false;
52
+ if (!this.isTradingDay(at)) return false;
53
+ const { minute } = this.parts(at);
54
+ return minute >= this.preOpen && minute < this.preClose;
55
+ }
56
+ status(at = /* @__PURE__ */ new Date()) {
57
+ if (this.isOpen(at)) return "open";
58
+ if (this.isPreOpen(at)) return "pre-open";
59
+ return "closed";
60
+ }
61
+ /** The next moment the regular session opens, at or after `from`. */
62
+ nextOpen(from = /* @__PURE__ */ new Date()) {
63
+ const start = this.parts(from);
64
+ for (let i = 0; i < 500; i++) {
65
+ const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 864e5;
66
+ const dp = new Date(dayMs);
67
+ const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };
68
+ if (this.spec.weekend.includes(dp.getUTCDay())) continue;
69
+ if (this.spec.holidays.includes(this.isoDate(p))) continue;
70
+ const openUtc = this.utcFor(p.y, p.m, p.d, this.regOpen);
71
+ if (openUtc > from.getTime()) return new Date(openUtc);
72
+ }
73
+ throw new Error("no trading day found within 500 days");
74
+ }
75
+ /** The next moment the regular session closes, at or after `from`. */
76
+ nextClose(from = /* @__PURE__ */ new Date()) {
77
+ const start = this.parts(from);
78
+ for (let i = 0; i < 500; i++) {
79
+ const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 864e5;
80
+ const dp = new Date(dayMs);
81
+ const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };
82
+ if (this.spec.weekend.includes(dp.getUTCDay())) continue;
83
+ if (this.spec.holidays.includes(this.isoDate(p))) continue;
84
+ const closeUtc = this.utcFor(p.y, p.m, p.d, this.regClose);
85
+ if (closeUtc > from.getTime()) return new Date(closeUtc);
86
+ }
87
+ throw new Error("no trading day found within 500 days");
88
+ }
89
+ /** Milliseconds until the session closes, or 0 if not currently open. */
90
+ msToClose(at = /* @__PURE__ */ new Date()) {
91
+ if (!this.isOpen(at)) return 0;
92
+ return this.nextClose(at).getTime() - at.getTime();
93
+ }
94
+ /** Milliseconds until the next open, or 0 if already open. */
95
+ msToOpen(at = /* @__PURE__ */ new Date()) {
96
+ if (this.isOpen(at)) return 0;
97
+ return this.nextOpen(at).getTime() - at.getTime();
98
+ }
99
+ };
100
+ function createClock(spec) {
101
+ return new MarketClock(spec);
102
+ }
103
+ var NSE_HOLIDAYS = [
104
+ // 2025
105
+ "2025-02-26",
106
+ "2025-03-14",
107
+ "2025-03-31",
108
+ "2025-04-10",
109
+ "2025-04-14",
110
+ "2025-04-18",
111
+ "2025-05-01",
112
+ "2025-08-15",
113
+ "2025-08-27",
114
+ "2025-10-02",
115
+ "2025-10-21",
116
+ "2025-10-22",
117
+ "2025-11-05",
118
+ "2025-12-25",
119
+ // 2026 (nationally-fixed observances; confirm exchange circular for the rest)
120
+ "2026-01-26",
121
+ "2026-03-04",
122
+ "2026-04-01",
123
+ "2026-05-01",
124
+ "2026-10-02",
125
+ "2026-11-09",
126
+ "2026-12-25"
127
+ ];
128
+ var NSE = {
129
+ name: "NSE",
130
+ offsetMinutes: 330,
131
+ preOpen: { open: "09:00", close: "09:15" },
132
+ regular: { open: "09:15", close: "15:30" },
133
+ weekend: [0, 6],
134
+ holidays: NSE_HOLIDAYS
135
+ };
136
+ var BSE = {
137
+ ...NSE,
138
+ name: "BSE"
139
+ };
140
+
141
+ export { BSE, MarketClock, NSE, createClock };
142
+ //# sourceMappingURL=index.js.map
143
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/market-clock\n * Is the market open right now? When does it next open / close?\n *\n * A holiday-aware, timezone-correct trading clock for stock exchanges. Ships\n * with NSE / BSE (India) presets — India has no DST, so a fixed IST offset is\n * exact. Bring your own exchange spec or override the holiday list any time.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface Session {\n /** \"HH:MM\" 24-hour, in exchange-local time. */\n open: string;\n close: string;\n}\n\nexport interface ExchangeSpec {\n name: string;\n /** Minutes ahead of UTC (IST = +330). Exchanges without DST only. */\n offsetMinutes: number;\n regular: Session;\n preOpen?: Session;\n /** Weekday numbers that are always closed (0 = Sunday … 6 = Saturday). */\n weekend: number[];\n /** Full-day holidays as \"YYYY-MM-DD\" in exchange-local dates. */\n holidays: string[];\n}\n\nexport type MarketStatus = \"pre-open\" | \"open\" | \"closed\";\n\nfunction toMinutes(hhmm: string): number {\n const [h, m] = hhmm.split(\":\").map(Number);\n return (h || 0) * 60 + (m || 0);\n}\n\nfunction pad(n: number): string {\n return n < 10 ? \"0\" + n : String(n);\n}\n\ninterface LocalParts {\n y: number;\n m: number;\n d: number;\n day: number;\n minute: number;\n}\n\nexport class MarketClock {\n private readonly regOpen: number;\n private readonly regClose: number;\n private readonly preOpen: number | null;\n private readonly preClose: number | null;\n\n constructor(public readonly spec: ExchangeSpec) {\n this.regOpen = toMinutes(spec.regular.open);\n this.regClose = toMinutes(spec.regular.close);\n this.preOpen = spec.preOpen ? toMinutes(spec.preOpen.open) : null;\n this.preClose = spec.preOpen ? toMinutes(spec.preOpen.close) : null;\n }\n\n /** Exchange-local calendar parts for an instant. */\n private parts(date: Date): LocalParts {\n const local = new Date(date.getTime() + this.spec.offsetMinutes * 60000);\n return {\n y: local.getUTCFullYear(),\n m: local.getUTCMonth() + 1,\n d: local.getUTCDate(),\n day: local.getUTCDay(),\n minute: local.getUTCHours() * 60 + local.getUTCMinutes(),\n };\n }\n\n /** UTC epoch millis for an exchange-local calendar day + minute-of-day. */\n private utcFor(y: number, m: number, d: number, minute: number): number {\n return Date.UTC(y, m - 1, d) + minute * 60000 - this.spec.offsetMinutes * 60000;\n }\n\n private isoDate(p: { y: number; m: number; d: number }): string {\n return `${p.y}-${pad(p.m)}-${pad(p.d)}`;\n }\n\n isWeekend(at: Date = new Date()): boolean {\n return this.spec.weekend.includes(this.parts(at).day);\n }\n\n isHoliday(at: Date = new Date()): boolean {\n return this.spec.holidays.includes(this.isoDate(this.parts(at)));\n }\n\n /** A day the exchange trades (not weekend, not a holiday). */\n isTradingDay(at: Date = new Date()): boolean {\n return !this.isWeekend(at) && !this.isHoliday(at);\n }\n\n isOpen(at: Date = new Date()): boolean {\n if (!this.isTradingDay(at)) return false;\n const { minute } = this.parts(at);\n return minute >= this.regOpen && minute < this.regClose;\n }\n\n isPreOpen(at: Date = new Date()): boolean {\n if (this.preOpen === null || this.preClose === null) return false;\n if (!this.isTradingDay(at)) return false;\n const { minute } = this.parts(at);\n return minute >= this.preOpen && minute < this.preClose;\n }\n\n status(at: Date = new Date()): MarketStatus {\n if (this.isOpen(at)) return \"open\";\n if (this.isPreOpen(at)) return \"pre-open\";\n return \"closed\";\n }\n\n /** The next moment the regular session opens, at or after `from`. */\n nextOpen(from: Date = new Date()): Date {\n const start = this.parts(from);\n for (let i = 0; i < 500; i++) {\n const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 86400000;\n const dp = new Date(dayMs);\n const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };\n if (this.spec.weekend.includes(dp.getUTCDay())) continue;\n if (this.spec.holidays.includes(this.isoDate(p))) continue;\n const openUtc = this.utcFor(p.y, p.m, p.d, this.regOpen);\n if (openUtc > from.getTime()) return new Date(openUtc);\n }\n throw new Error(\"no trading day found within 500 days\");\n }\n\n /** The next moment the regular session closes, at or after `from`. */\n nextClose(from: Date = new Date()): Date {\n const start = this.parts(from);\n for (let i = 0; i < 500; i++) {\n const dayMs = Date.UTC(start.y, start.m - 1, start.d) + i * 86400000;\n const dp = new Date(dayMs);\n const p = { y: dp.getUTCFullYear(), m: dp.getUTCMonth() + 1, d: dp.getUTCDate() };\n if (this.spec.weekend.includes(dp.getUTCDay())) continue;\n if (this.spec.holidays.includes(this.isoDate(p))) continue;\n const closeUtc = this.utcFor(p.y, p.m, p.d, this.regClose);\n if (closeUtc > from.getTime()) return new Date(closeUtc);\n }\n throw new Error(\"no trading day found within 500 days\");\n }\n\n /** Milliseconds until the session closes, or 0 if not currently open. */\n msToClose(at: Date = new Date()): number {\n if (!this.isOpen(at)) return 0;\n return this.nextClose(at).getTime() - at.getTime();\n }\n\n /** Milliseconds until the next open, or 0 if already open. */\n msToOpen(at: Date = new Date()): number {\n if (this.isOpen(at)) return 0;\n return this.nextOpen(at).getTime() - at.getTime();\n }\n}\n\nexport function createClock(spec: ExchangeSpec): MarketClock {\n return new MarketClock(spec);\n}\n\n/**\n * NSE trading holidays. Nationally-fixed days are reliable; the rest follow the\n * annual NSE circular and may shift year to year — verify and extend as needed\n * (spread via `new MarketClock({ ...NSE, holidays: [...NSE.holidays, ...] })`).\n */\nconst NSE_HOLIDAYS: string[] = [\n // 2025\n \"2025-02-26\", \"2025-03-14\", \"2025-03-31\", \"2025-04-10\", \"2025-04-14\",\n \"2025-04-18\", \"2025-05-01\", \"2025-08-15\", \"2025-08-27\", \"2025-10-02\",\n \"2025-10-21\", \"2025-10-22\", \"2025-11-05\", \"2025-12-25\",\n // 2026 (nationally-fixed observances; confirm exchange circular for the rest)\n \"2026-01-26\", \"2026-03-04\", \"2026-04-01\", \"2026-05-01\", \"2026-10-02\",\n \"2026-11-09\", \"2026-12-25\",\n];\n\n/** National Stock Exchange of India. Pre-open 09:00–09:15, regular 09:15–15:30 IST. */\nexport const NSE: ExchangeSpec = {\n name: \"NSE\",\n offsetMinutes: 330,\n preOpen: { open: \"09:00\", close: \"09:15\" },\n regular: { open: \"09:15\", close: \"15:30\" },\n weekend: [0, 6],\n holidays: NSE_HOLIDAYS,\n};\n\n/** Bombay Stock Exchange — same session timings and holiday calendar as NSE. */\nexport const BSE: ExchangeSpec = {\n ...NSE,\n name: \"BSE\",\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@lacspace/market-clock",
3
+ "version": "1.0.0",
4
+ "description": "Holiday-aware, timezone-correct trading clock — is NSE/BSE open now, next open/close, pre-open, holidays. Bring your own exchange spec. Zero-dependency.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "market-hours",
31
+ "trading-hours",
32
+ "stock-market",
33
+ "nse",
34
+ "bse",
35
+ "market-open",
36
+ "holidays",
37
+ "exchange-calendar",
38
+ "timezone",
39
+ "typescript"
40
+ ],
41
+ "author": "Lacspace <contact@lacspace.com>",
42
+ "license": "MIT",
43
+ "homepage": "https://lacspace.com/packages",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/lacspace/npm-packages.git",
47
+ "directory": "market-clock"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/lacspace/npm-packages/issues"
51
+ },
52
+ "engines": {
53
+ "node": ">=18"
54
+ }
55
+ }