@ckirg/corelib-markets 0.1.22

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/dist/index.js ADDED
@@ -0,0 +1,2124 @@
1
+ // src/nasdaq/ApiNasdaqQuotes.ts
2
+ import {
3
+ ConfigManager as ConfigManager3,
4
+ nextCid as nextCid3,
5
+ RequestProxied
6
+ } from "@ckirg/corelib";
7
+
8
+ // src/nasdaq/ApiNasdaqUnlimited.ts
9
+ import {
10
+ ConfigManager,
11
+ endPoint,
12
+ logger,
13
+ nextCid
14
+ } from "@ckirg/corelib";
15
+ import { serializeError } from "serialize-error";
16
+ var nasdaqUnlimitedLogger = logger.child({ section: "ApiNasdaqUnlimited" });
17
+ function apiErrorToString(status) {
18
+ if (!status.bCodeMessage || status.bCodeMessage.length === 0) {
19
+ return status.developerMessage || "Unknown Nasdaq API Error";
20
+ }
21
+ return status.bCodeMessage.map((err) => `code: ${err.code} = ${err.errorMessage}`).join("::");
22
+ }
23
+ function log(level, msg, data) {
24
+ const payload = data instanceof Error ? { error: serializeError(data) } : data;
25
+ nasdaqUnlimitedLogger[level](msg, payload);
26
+ }
27
+ function getNasdaqHeaders(url) {
28
+ const chromeVersion = ConfigManager.get("markets.chromeVersion") ?? "145";
29
+ const isCharting = url.includes("charting");
30
+ const headers = isCharting ? {
31
+ accept: "*/*",
32
+ "accept-language": "en-US,en;q=0.9",
33
+ "cache-control": "no-cache",
34
+ pragma: "no-cache",
35
+ priority: "u=1, i",
36
+ "sec-ch-ua": `"Google Chrome";v="${chromeVersion}", "Not-A.Brand";v="8", "Chromium";v="${chromeVersion}"`,
37
+ "sec-ch-ua-mobile": "?0",
38
+ "sec-ch-ua-platform": '"Windows"',
39
+ "sec-fetch-dest": "empty",
40
+ "sec-fetch-mode": "cors",
41
+ "sec-fetch-site": "same-origin",
42
+ referer: "https://charting.nasdaq.com/dynamic/chart.html",
43
+ "user-agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}.0.0.0 Safari/537.36`
44
+ } : {
45
+ accept: "application/json, text/plain, */*",
46
+ "accept-language": "en-US,en;q=0.9",
47
+ origin: "https://www.nasdaq.com",
48
+ referer: "https://www.nasdaq.com/",
49
+ "sec-ch-ua": `"Google Chrome";v="${chromeVersion}", "Not-A.Brand";v="8", "Chromium";v="${chromeVersion}"`,
50
+ "sec-ch-ua-mobile": "?0",
51
+ "sec-ch-ua-platform": '"Windows"',
52
+ "user-agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}.0.0.0 Safari/537.36`
53
+ };
54
+ const configHeaders = ConfigManager.get("markets.nasdaq.headers");
55
+ return configHeaders ? { ...headers, ...configHeaders } : headers;
56
+ }
57
+ async function nasdaqEndPoint(url, options = {}) {
58
+ const cid = nextCid();
59
+ const startedAt = performance.now();
60
+ const urlStr = typeof url === "string" ? url : url.toString();
61
+ const urlObj = new URL(urlStr);
62
+ nasdaqUnlimitedLogger.trace("nasdaqEndPoint: request", {
63
+ cid,
64
+ url: urlObj.origin + urlObj.pathname
65
+ });
66
+ const headers = { ...getNasdaqHeaders(urlStr), ...options.headers ?? {} };
67
+ const result = await endPoint(url, { ...options, headers });
68
+ if (result.status === "error") {
69
+ nasdaqUnlimitedLogger.trace("nasdaqEndPoint: error", {
70
+ cid,
71
+ durationMs: performance.now() - startedAt,
72
+ errorMsg: "Transport Error"
73
+ });
74
+ log("error", "Transport Error", { url: urlStr, reason: result.reason });
75
+ return {
76
+ status: "error",
77
+ reason: { message: "Transport Error", original: result.reason }
78
+ };
79
+ }
80
+ const val = result.value;
81
+ const nasdaqBody = val.body;
82
+ if (nasdaqBody && typeof nasdaqBody === "object" && "status" in nasdaqBody) {
83
+ const statusObj = nasdaqBody.status;
84
+ if (statusObj?.rCode !== 200) {
85
+ nasdaqUnlimitedLogger.trace("nasdaqEndPoint: error", {
86
+ cid,
87
+ durationMs: performance.now() - startedAt,
88
+ errorMsg: "logic check failed"
89
+ });
90
+ log("warn", "Request failed logic check", {
91
+ url: urlStr,
92
+ status: nasdaqBody.status
93
+ });
94
+ const errorMessage = statusObj ? apiErrorToString(statusObj) : "Malformed Nasdaq Response";
95
+ return {
96
+ status: "error",
97
+ reason: { message: errorMessage }
98
+ };
99
+ }
100
+ }
101
+ const { body, ...details } = val;
102
+ nasdaqUnlimitedLogger.trace("nasdaqEndPoint: ok", {
103
+ cid,
104
+ durationMs: performance.now() - startedAt,
105
+ status: val.status
106
+ });
107
+ return {
108
+ status: "success",
109
+ value: body?.data,
110
+ details
111
+ };
112
+ }
113
+ async function nasdaqEndPoints(urls, options = {}) {
114
+ const promises = urls.map((url) => nasdaqEndPoint(url, options));
115
+ return Promise.all(promises);
116
+ }
117
+ var ApiNasdaqUnlimited = {
118
+ /**
119
+ * Executes a single Nasdaq API request.
120
+ */
121
+ endPoint: nasdaqEndPoint,
122
+ /**
123
+ * Executes multiple Nasdaq API requests in parallel.
124
+ */
125
+ endPoints: nasdaqEndPoints
126
+ };
127
+
128
+ // src/nasdaq/MarketSymbols.ts
129
+ import {
130
+ logger as baseLogger,
131
+ ConfigManager as ConfigManager2,
132
+ createDatabase,
133
+ detectRuntime,
134
+ endPoint as endPoint2,
135
+ endPoints,
136
+ getTempDir,
137
+ nextCid as nextCid2,
138
+ sleep
139
+ } from "@ckirg/corelib";
140
+ import { DateTime } from "luxon";
141
+ import { serializeError as serializeError2 } from "serialize-error";
142
+
143
+ // src/nasdaq/AssetClass.ts
144
+ var Realtime = /* @__PURE__ */ ((Realtime2) => {
145
+ Realtime2["Stocks"] = "stocks";
146
+ Realtime2["Etf"] = "etf";
147
+ Realtime2["Currencies"] = "currencies";
148
+ Realtime2["Crypto"] = "crypto";
149
+ return Realtime2;
150
+ })(Realtime || {});
151
+
152
+ // src/nasdaq/MarketSymbols.ts
153
+ var marketSymbolsLogger = baseLogger.child({ section: "MarketSymbols" });
154
+ var DEFAULT_NASDAQ_LISTED_URL = "https://www.nasdaqtrader.com/dynamic/symdir/nasdaqlisted.txt";
155
+ var DEFAULT_OTHER_LISTED_URL = "https://www.nasdaqtrader.com/dynamic/symdir/otherlisted.txt";
156
+ var DEFAULT_INITIAL_BACKOFF_MS = 1e3;
157
+ var DEFAULT_MAX_RETRY_BACKOFF_MS = 36e5;
158
+ var DEFAULT_MAX_FETCH_RETRIES = 10;
159
+ var MarketSymbols = class {
160
+ /**
161
+ * @param db - Optional database configuration or existing instance:
162
+ * - `undefined` → uses `${getTempDir()}/NasdaqSymbols.sqlite`
163
+ * - `string` → local SQLite file path
164
+ * - `{ dbUrl: string; dbToken: string }` → Turso/LibSQL remote
165
+ * - `Database` → An existing instance of a Database driver
166
+ * @param ingestors - Array of ingestor URLs (e.g., Google App Script endpoints) to query for missing symbols.
167
+ */
168
+ constructor(db, ingestors = []) {
169
+ this.ingestors = ingestors;
170
+ if (db && typeof db.query === "function") {
171
+ this.db = db;
172
+ this.isDbOwner = false;
173
+ return;
174
+ }
175
+ this.isDbOwner = true;
176
+ if (!db) {
177
+ const path = `${getTempDir()}/NasdaqSymbols.sqlite`;
178
+ this.config = {
179
+ dialect: "sqlite",
180
+ url: `file:${path}`,
181
+ mode: "stateful"
182
+ };
183
+ } else if (typeof db === "string") {
184
+ this.config = {
185
+ dialect: "sqlite",
186
+ url: db.startsWith("libsql://") || db.startsWith("file:") ? db : `file:${db}`,
187
+ mode: "stateful"
188
+ };
189
+ } else {
190
+ const turso = db;
191
+ this.config = {
192
+ dialect: "sqlite",
193
+ url: turso.dbUrl,
194
+ authToken: turso.dbToken,
195
+ mode: "stateful"
196
+ };
197
+ }
198
+ }
199
+ ingestors;
200
+ db = null;
201
+ initialized = false;
202
+ isDbOwner = true;
203
+ config;
204
+ /**
205
+ * Registry mapping URL patterns to specific ingestor methods.
206
+ * Designed to be "open" for additional ingestors in future releases.
207
+ */
208
+ ingestorRegistry = [
209
+ {
210
+ pattern: /script\.google\.com/i,
211
+ processor: this.ingestorGAS.bind(this)
212
+ }
213
+ ];
214
+ /**
215
+ * Force a full refresh of the symbol database.
216
+ * Called automatically on first use if needed.
217
+ */
218
+ async refresh() {
219
+ await this.ensureInitialized();
220
+ await this.performRefresh();
221
+ }
222
+ /**
223
+ * Get symbol data.
224
+ * Searches Nasdaq API, external ingestors, and the DB. The sequence order is
225
+ * optimized dynamically based on whether it is running in an Edge environment.
226
+ * @returns `null` if the symbol is not found or is inactive.
227
+ */
228
+ async get(symbol) {
229
+ const cid = nextCid2();
230
+ const startedAt = performance.now();
231
+ const runtime = detectRuntime();
232
+ const isEdge = ["cloudflare", "aws-lambda", "gcp-cloudrun"].includes(
233
+ runtime
234
+ );
235
+ marketSymbolsLogger.trace("resolve: start", { cid, symbol, isEdge });
236
+ let result;
237
+ if (isEdge) {
238
+ let res = await this.searchNasdaqApi(symbol);
239
+ if (res) {
240
+ result = res;
241
+ } else {
242
+ res = await this.searchIngestors(symbol);
243
+ result = res ?? await this.searchDb(symbol);
244
+ }
245
+ } else {
246
+ let res = await this.searchDb(symbol);
247
+ if (res) {
248
+ result = res;
249
+ } else {
250
+ res = await this.searchNasdaqApi(symbol);
251
+ result = res ?? await this.searchIngestors(symbol);
252
+ }
253
+ }
254
+ marketSymbolsLogger.trace("resolve: done", {
255
+ cid,
256
+ durationMs: performance.now() - startedAt,
257
+ symbol,
258
+ found: result !== null,
259
+ assetClass: result?.class ?? null
260
+ });
261
+ return result;
262
+ }
263
+ /**
264
+ * Graceful shutdown – disconnects the database driver if it was created internally.
265
+ */
266
+ async close() {
267
+ if (this.db) {
268
+ if (this.isDbOwner) {
269
+ await this.db.disconnect();
270
+ }
271
+ this.db = null;
272
+ this.initialized = false;
273
+ }
274
+ }
275
+ // -----------------------------------------------------------------------
276
+ // Search Sequences
277
+ // -----------------------------------------------------------------------
278
+ /**
279
+ * Queries the official Nasdaq autocomplete API for a symbol.
280
+ * Filters for an exact match.
281
+ */
282
+ async searchNasdaqApi(symbol) {
283
+ try {
284
+ const url = `https://api.nasdaq.com/api/autocomplete/slookup/10?search=${encodeURIComponent(symbol)}`;
285
+ const result = await ApiNasdaqUnlimited.endPoint(url);
286
+ if (result.status === "success" && Array.isArray(result.value)) {
287
+ const match = result.value.find(
288
+ (item) => String(item.symbol).toUpperCase() === symbol.toUpperCase()
289
+ );
290
+ if (match) {
291
+ const assetLower = match.asset ? String(match.asset).toLowerCase() : "";
292
+ let type = "eod";
293
+ if (Object.values(Realtime).includes(assetLower)) {
294
+ type = "rt";
295
+ }
296
+ return {
297
+ symbol: String(match.symbol),
298
+ name: String(match.name ?? "").trim(),
299
+ type,
300
+ class: assetLower,
301
+ ts: Date.now(),
302
+ active: true
303
+ };
304
+ }
305
+ }
306
+ } catch (e) {
307
+ marketSymbolsLogger.warn("SearchNasdaqApi failed", {
308
+ symbol,
309
+ error: serializeError2(e)
310
+ });
311
+ }
312
+ return null;
313
+ }
314
+ /**
315
+ * Queries external ingestors defined in the constructor based on the internal registry pattern.
316
+ */
317
+ async searchIngestors(symbol) {
318
+ if (!this.ingestors || this.ingestors.length === 0) return null;
319
+ for (const url of this.ingestors) {
320
+ for (const entry of this.ingestorRegistry) {
321
+ if (entry.pattern.test(url)) {
322
+ try {
323
+ const result = await entry.processor(url, symbol);
324
+ if (result) return result;
325
+ } catch (e) {
326
+ marketSymbolsLogger.warn("Ingestor failed", {
327
+ url,
328
+ error: serializeError2(e)
329
+ });
330
+ }
331
+ }
332
+ }
333
+ }
334
+ return null;
335
+ }
336
+ /**
337
+ * Specifically processes Google Apps Script (GAS) ingestor URLs.
338
+ */
339
+ async ingestorGAS(baseUrl, symbol) {
340
+ const url = new URL(baseUrl);
341
+ url.searchParams.set("symbol", symbol);
342
+ const result = await endPoint2(url.toString());
343
+ if (result.status === "success" && result.value?.body) {
344
+ const body = result.value.body;
345
+ if (body.status === "success" && body.value) {
346
+ const data = body.value;
347
+ if (String(data.symbol).toUpperCase() === symbol.toUpperCase()) {
348
+ return {
349
+ symbol: String(data.symbol),
350
+ name: String(data.name || ""),
351
+ type: data.type || "eod",
352
+ class: String(data.class || ""),
353
+ ts: Number(data.ts) || Date.now(),
354
+ active: typeof data.active === "boolean" ? data.active : true
355
+ };
356
+ }
357
+ }
358
+ }
359
+ return null;
360
+ }
361
+ /**
362
+ * Searches the local or remote SQLite database.
363
+ */
364
+ async searchDb(symbol) {
365
+ try {
366
+ const db = await this.ensureInitialized();
367
+ const result = await db.query(
368
+ "SELECT symbol, type, class, name, ts, active FROM nasdaq_symbols WHERE symbol = ? AND active = true LIMIT 1",
369
+ [symbol.toUpperCase()]
370
+ );
371
+ if (result.status === "success" && result.value.rows.length > 0) {
372
+ return result.value.rows[0];
373
+ }
374
+ } catch (e) {
375
+ marketSymbolsLogger.warn("SearchDb query failed", {
376
+ symbol,
377
+ error: serializeError2(e)
378
+ });
379
+ }
380
+ return null;
381
+ }
382
+ // -----------------------------------------------------------------------
383
+ // Private Database Management
384
+ // -----------------------------------------------------------------------
385
+ /**
386
+ * Initializes the database driver if not already done.
387
+ * Creates the `nasdaq_symbols` table if it doesn't exist.
388
+ * Creates an index on the `active` column if it doesn't exist.
389
+ * Called automatically on first use, and before any other operations.
390
+ */
391
+ async ensureInitialized() {
392
+ if (this.db && this.initialized) return this.db;
393
+ if (!this.db) {
394
+ if (!this.config) {
395
+ throw new Error(
396
+ "MarketSymbols: Database instance not provided and no configuration available."
397
+ );
398
+ }
399
+ this.db = await createDatabase(this.config);
400
+ }
401
+ await this.db.query(`
402
+ CREATE TABLE IF NOT EXISTS nasdaq_symbols (
403
+ symbol TEXT PRIMARY KEY,
404
+ type TEXT NOT NULL,
405
+ class TEXT NOT NULL,
406
+ name TEXT NOT NULL,
407
+ ts INTEGER NOT NULL,
408
+ active BOOLEAN NOT NULL DEFAULT true
409
+ )
410
+ `);
411
+ await this.db.query(
412
+ "CREATE INDEX IF NOT EXISTS idx_nasdaq_symbols_active ON nasdaq_symbols(active)"
413
+ );
414
+ this.initialized = true;
415
+ await this.performRefresh();
416
+ return this.db;
417
+ }
418
+ /**
419
+ * Checks if the database needs to be refreshed.
420
+ * Returns true if the database has never been populated, or if the last refresh was not today.
421
+ * @returns {Promise<boolean>} true if the database needs to be refreshed
422
+ */
423
+ async needsRefresh() {
424
+ if (!this.db) return true;
425
+ const result = await this.db.query(
426
+ "SELECT MAX(ts) AS max_ts FROM nasdaq_symbols LIMIT 1"
427
+ );
428
+ if (result.status === "error") return true;
429
+ const maxTs = result.value.rows[0]?.max_ts;
430
+ if (!maxTs) return true;
431
+ const lastDate = DateTime.fromMillis(maxTs).setZone("America/New_York").startOf("day");
432
+ const today = DateTime.now().setZone("America/New_York").startOf("day");
433
+ return !lastDate.equals(today);
434
+ }
435
+ /**
436
+ * Refreshes the symbol database.
437
+ * Only runs if the database has never been populated, or if the last refresh was not today.
438
+ * Downloads the official Nasdaq symbol directories, parses them, and updates the database.
439
+ * @returns {Promise<void>} resolves after the database has been refreshed
440
+ */
441
+ async performRefresh() {
442
+ if (!await this.needsRefresh()) return;
443
+ if (!this.db) return;
444
+ marketSymbolsLogger.info("Starting full symbol directory refresh");
445
+ let texts;
446
+ try {
447
+ texts = await this.fetchSymbolFilesWithRetry();
448
+ } catch (err) {
449
+ if (await this.hasExistingData()) {
450
+ marketSymbolsLogger.warn(
451
+ "Symbol refresh abandoned, using existing data",
452
+ {
453
+ error: serializeError2(err)
454
+ }
455
+ );
456
+ return;
457
+ }
458
+ throw err;
459
+ }
460
+ const nasdaqRows = this.parseNasdaqListed(texts.nasdaqText);
461
+ const otherRows = this.parseOtherListed(texts.otherText);
462
+ const allRows = /* @__PURE__ */ new Map();
463
+ for (const r of nasdaqRows) allRows.set(r.symbol, r);
464
+ for (const r of otherRows) {
465
+ if (!allRows.has(r.symbol)) allRows.set(r.symbol, r);
466
+ }
467
+ const now = Date.now();
468
+ const rowsArray = Array.from(allRows.values()).map((r) => ({
469
+ ...r,
470
+ ts: now
471
+ }));
472
+ await this.db.transaction(async () => {
473
+ if (!this.db) throw new Error("Database lost during transaction");
474
+ await this.db.query("UPDATE nasdaq_symbols SET active = false");
475
+ const BATCH_SIZE = 150;
476
+ for (let i = 0; i < rowsArray.length; i += BATCH_SIZE) {
477
+ const batch = rowsArray.slice(i, i + BATCH_SIZE);
478
+ const placeholders = batch.map(() => "(?, ?, ?, ?, ?, true)").join(", ");
479
+ const params = batch.flatMap((r) => [
480
+ r.symbol,
481
+ r.type,
482
+ r.class,
483
+ r.name,
484
+ r.ts
485
+ ]);
486
+ await this.db.query(
487
+ `INSERT INTO nasdaq_symbols (symbol, type, class, name, ts, active)
488
+ VALUES ${placeholders}
489
+ ON CONFLICT(symbol) DO UPDATE SET
490
+ type = excluded.type,
491
+ class = excluded.class,
492
+ name = excluded.name,
493
+ ts = excluded.ts,
494
+ active = true`,
495
+ params
496
+ );
497
+ }
498
+ return {
499
+ status: "success",
500
+ value: null
501
+ };
502
+ });
503
+ }
504
+ /**
505
+ * Downloads the official Nasdaq symbol directories with retry and circuit breaker.
506
+ * Retries with exponential backoff up to `markets.nasdaq.symbols.maxRetryBackoffMs` per interval.
507
+ * Stops after `markets.nasdaq.symbols.maxFetchRetries` consecutive failures even when existing data is present.
508
+ * Throws immediately on first failure when no existing data exists.
509
+ */
510
+ async fetchSymbolFilesWithRetry() {
511
+ const nasdaqListedUrl = ConfigManager2.get("markets.nasdaq.symbols.nasdaqListedUrl") ?? DEFAULT_NASDAQ_LISTED_URL;
512
+ const otherListedUrl = ConfigManager2.get("markets.nasdaq.symbols.otherListedUrl") ?? DEFAULT_OTHER_LISTED_URL;
513
+ const maxRetryBackoffMs = ConfigManager2.get("markets.nasdaq.symbols.maxRetryBackoffMs") ?? DEFAULT_MAX_RETRY_BACKOFF_MS;
514
+ const maxFetchRetries = ConfigManager2.get("markets.nasdaq.symbols.maxFetchRetries") ?? DEFAULT_MAX_FETCH_RETRIES;
515
+ let backoffMs = ConfigManager2.get("markets.nasdaq.symbols.initialBackoffMs") ?? DEFAULT_INITIAL_BACKOFF_MS;
516
+ let retryCount = 0;
517
+ while (retryCount <= maxFetchRetries) {
518
+ try {
519
+ const results = await endPoints(
520
+ [nasdaqListedUrl, otherListedUrl],
521
+ {
522
+ headers: {
523
+ accept: "text/plain, */*",
524
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
525
+ }
526
+ }
527
+ );
528
+ if (results[0].status === "success" && results[1].status === "success") {
529
+ return {
530
+ nasdaqText: results[0].value.body,
531
+ otherText: results[1].value.body
532
+ };
533
+ }
534
+ const errorResult = results[0].status === "error" ? results[0] : results[1];
535
+ const reason = errorResult.status === "error" ? errorResult.reason : {};
536
+ marketSymbolsLogger.warn("Symbol directory fetch failed \u2013 retrying", {
537
+ retryCount,
538
+ reason: serializeError2(reason)
539
+ });
540
+ const hasExistingData = await this.hasExistingData();
541
+ if (!hasExistingData) {
542
+ throw new Error(
543
+ `Failed to construct symbols db - ${reason.message ?? JSON.stringify(serializeError2(reason))}`
544
+ );
545
+ }
546
+ if (retryCount >= maxFetchRetries) {
547
+ throw new Error(
548
+ `Symbol fetch circuit breaker: gave up after ${maxFetchRetries} retries`
549
+ );
550
+ }
551
+ await sleep(backoffMs);
552
+ backoffMs = Math.min(backoffMs * 2, maxRetryBackoffMs);
553
+ retryCount++;
554
+ } catch (err) {
555
+ const hasExistingData = await this.hasExistingData();
556
+ if (hasExistingData && retryCount < maxFetchRetries) {
557
+ marketSymbolsLogger.warn("Symbol directory fetch thrown \u2013 retrying", {
558
+ retryCount,
559
+ error: serializeError2(err)
560
+ });
561
+ await sleep(backoffMs);
562
+ backoffMs = Math.min(backoffMs * 2, maxRetryBackoffMs);
563
+ retryCount++;
564
+ continue;
565
+ }
566
+ throw err;
567
+ }
568
+ }
569
+ throw new Error(
570
+ `Symbol fetch circuit breaker: gave up after ${maxFetchRetries} retries`
571
+ );
572
+ }
573
+ /**
574
+ * Checks if there is existing data in the database.
575
+ * Returns true if there is any existing data, false otherwise.
576
+ * @returns {Promise<boolean>} true if there is any existing data, false otherwise
577
+ */
578
+ async hasExistingData() {
579
+ if (!this.db) return false;
580
+ const res = await this.db.query(
581
+ "SELECT COUNT(*) AS count FROM nasdaq_symbols LIMIT 1"
582
+ );
583
+ if (res.status === "success") {
584
+ return (res.value.rows[0]?.count ?? 0) > 0;
585
+ }
586
+ return false;
587
+ }
588
+ /**
589
+ * Parses the official Nasdaq symbol directory file (nasdaqlisted.txt).
590
+ * Skips the first two lines (header) and empty lines.
591
+ * Skips lines with less than 8 fields (invalid).
592
+ * Extracts the symbol, name, and ETF status from the line.
593
+ * Creates a MarketSymbolRow with the extracted data and adds it to the result array.
594
+ * @param {string} text - The content of the nasdaqlisted.txt file as a string
595
+ * @returns {MarketSymbolRow[]} - An array of MarketSymbolRow objects parsed from the file
596
+ */
597
+ parseNasdaqListed(text) {
598
+ const rows = [];
599
+ const lines = text.trim().split(/\r?\n/);
600
+ for (const line of lines) {
601
+ if (line.startsWith("Symbol|") || line.startsWith("File Creation Time|") || !line.trim())
602
+ continue;
603
+ const fields = line.split("|");
604
+ if (fields.length < 8) continue;
605
+ const symbol = fields[0].trim();
606
+ const name = fields[1].trim();
607
+ const isEtf = fields[6] === "Y";
608
+ rows.push({
609
+ symbol,
610
+ name,
611
+ type: "rt",
612
+ class: isEtf ? "etf" /* Etf */ : "stocks" /* Stocks */,
613
+ ts: 0,
614
+ active: true
615
+ });
616
+ }
617
+ return rows;
618
+ }
619
+ /**
620
+ * Parses the official Nasdaq symbol directory file (otherlisted.txt).
621
+ * Skips the first two lines (header) and empty lines.
622
+ * Skips lines with less than 5 fields (invalid).
623
+ * Extracts the symbol, name, and ETF status from the line.
624
+ * Creates a MarketSymbolRow with the extracted data and adds it to the result array.
625
+ * @param {string} text - The content of the otherlisted.txt file as a string
626
+ * @returns {MarketSymbolRow[]} - An array of MarketSymbolRow objects parsed from the file
627
+ */
628
+ parseOtherListed(text) {
629
+ const rows = [];
630
+ const lines = text.trim().split(/\r?\n/);
631
+ for (const line of lines) {
632
+ if (line.startsWith("Symbol|") || line.startsWith("File Creation Time|") || !line.trim())
633
+ continue;
634
+ const fields = line.split("|");
635
+ if (fields.length < 5) continue;
636
+ const symbol = fields[0].trim();
637
+ const name = fields[1].trim();
638
+ const isEtf = fields[4] === "Y";
639
+ rows.push({
640
+ symbol,
641
+ name,
642
+ type: "rt",
643
+ class: isEtf ? "etf" /* Etf */ : "stocks" /* Stocks */,
644
+ ts: 0,
645
+ active: true
646
+ });
647
+ }
648
+ return rows;
649
+ }
650
+ };
651
+
652
+ // src/nasdaq/ApiNasdaqQuotes.ts
653
+ var DEFAULT_CONCURRENCY_LIMIT = 5;
654
+ var ApiNasdaqQuotes = class {
655
+ logger;
656
+ marketSymbols;
657
+ requestProxied;
658
+ isInternalMarketSymbols;
659
+ concurrencyLimit;
660
+ /**
661
+ * Creates an instance of ApiNasdaqQuotes.
662
+ * @param options Configuration options for the module.
663
+ */
664
+ constructor(options = {}) {
665
+ const baseLogger2 = options.logger || globalThis.logger;
666
+ this.logger = baseLogger2?.child({ section: "ApiNasdaqQuotes" });
667
+ this.concurrencyLimit = options.concurrencyLimit ?? ConfigManager3.get("markets.nasdaq.quotes.concurrencyLimit") ?? DEFAULT_CONCURRENCY_LIMIT;
668
+ if (options.marketSymbols) {
669
+ this.marketSymbols = options.marketSymbols;
670
+ this.isInternalMarketSymbols = false;
671
+ } else {
672
+ this.marketSymbols = new MarketSymbols();
673
+ this.isInternalMarketSymbols = true;
674
+ }
675
+ if (options.proxies && options.proxies.length > 0) {
676
+ this.requestProxied = new RequestProxied(options.proxies);
677
+ }
678
+ }
679
+ /**
680
+ * Retrieves real-time quotes for a batch of symbols.
681
+ * Results are returned in an array mirroring the order of the input symbols.
682
+ * @param symbols An array of ticker symbols (e.g. ['AAPL', 'MSFT']).
683
+ * @returns A promise resolving to an array of NasdaqResult objects.
684
+ */
685
+ async getNasdaqQuote(symbols) {
686
+ const cid = nextCid3();
687
+ const startedAt = performance.now();
688
+ const batches = this.requestProxied ? 1 : Math.ceil(symbols.length / this.concurrencyLimit);
689
+ this.logger?.trace("getNasdaqQuote: start", {
690
+ cid,
691
+ requested: symbols.length,
692
+ batches,
693
+ concurrency: this.concurrencyLimit
694
+ });
695
+ const results = new Array(symbols.length);
696
+ const fetchQueue = [];
697
+ for (let i = 0; i < symbols.length; i++) {
698
+ const symbol = symbols[i].toUpperCase();
699
+ try {
700
+ const symbolData = await this.marketSymbols.get(symbol);
701
+ if (!symbolData) {
702
+ results[i] = {
703
+ status: "error",
704
+ reason: { message: `Symbol ${symbol} not found in MarketSymbols` }
705
+ };
706
+ continue;
707
+ }
708
+ const assetClass = symbolData.class || "stocks";
709
+ const url = `https://api.nasdaq.com/api/quote/${symbol}/info?assetclass=${assetClass.toLowerCase()}`;
710
+ fetchQueue.push({ symbol, url, index: i });
711
+ } catch (error) {
712
+ const message = error instanceof Error ? error.message : String(error);
713
+ this.logger?.warn("Error resolving asset class", {
714
+ symbol,
715
+ error: message
716
+ });
717
+ results[i] = {
718
+ status: "error",
719
+ reason: {
720
+ message: `Internal error during symbol resolution: ${message}`
721
+ }
722
+ };
723
+ }
724
+ }
725
+ if (fetchQueue.length === 0) return results;
726
+ if (this.requestProxied) {
727
+ const urls = fetchQueue.map((q) => q.url);
728
+ const proxyResults = await this.requestProxied.endPoints(urls);
729
+ for (let i = 0; i < proxyResults.length; i++) {
730
+ const q = fetchQueue[i];
731
+ const pRes = proxyResults[i];
732
+ if (pRes.status === "success") {
733
+ const body = pRes.value.body;
734
+ const statusObj = body?.status;
735
+ if (statusObj?.rCode === 200) {
736
+ results[q.index] = {
737
+ status: "success",
738
+ value: body.data,
739
+ details: pRes.value
740
+ };
741
+ } else {
742
+ const devMsg = typeof statusObj?.developerMessage === "string" ? statusObj.developerMessage : "Nasdaq API Error via Proxy";
743
+ results[q.index] = {
744
+ status: "error",
745
+ reason: { message: devMsg }
746
+ };
747
+ }
748
+ } else {
749
+ const errMsg = pRes.reason?.message;
750
+ results[q.index] = {
751
+ status: "error",
752
+ reason: {
753
+ message: typeof errMsg === "string" ? errMsg : "Proxy request failed"
754
+ }
755
+ };
756
+ }
757
+ }
758
+ } else {
759
+ for (let i = 0; i < fetchQueue.length; i += this.concurrencyLimit) {
760
+ const batch = fetchQueue.slice(i, i + this.concurrencyLimit);
761
+ const batchTasks = batch.map(async (q) => {
762
+ try {
763
+ const res = await ApiNasdaqUnlimited.endPoint(q.url);
764
+ results[q.index] = res;
765
+ } catch (error) {
766
+ results[q.index] = {
767
+ status: "error",
768
+ reason: {
769
+ message: error instanceof Error ? error.message : "Unlimited fetch failed"
770
+ }
771
+ };
772
+ }
773
+ });
774
+ await Promise.all(batchTasks);
775
+ }
776
+ }
777
+ const ok = results.filter((r) => r?.status === "success").length;
778
+ const failed = results.filter((r) => r?.status === "error").length;
779
+ this.logger?.trace("getNasdaqQuote: done", {
780
+ cid,
781
+ durationMs: performance.now() - startedAt,
782
+ requested: symbols.length,
783
+ ok,
784
+ failed,
785
+ sampleSymbol: symbols[0]
786
+ });
787
+ return results;
788
+ }
789
+ /**
790
+ * Properly shuts down internal resources and database connections.
791
+ * Must be called if MarketSymbols was instantiated internally.
792
+ */
793
+ async close() {
794
+ if (this.isInternalMarketSymbols) {
795
+ await this.marketSymbols.close();
796
+ }
797
+ }
798
+ };
799
+
800
+ // src/nasdaq/CnnFearAndGreed.ts
801
+ import {
802
+ ConfigManager as ConfigManager4,
803
+ endPoint as endPoint3,
804
+ logger as logger2,
805
+ nextCid as nextCid4
806
+ } from "@ckirg/corelib";
807
+ import { DateTime as DateTime2 } from "luxon";
808
+ import { serializeError as serializeError3 } from "serialize-error";
809
+ var cnnLogger = logger2.child({ section: "CnnFearAndGreed" });
810
+ var CnnFearAndGreedFilter = /* @__PURE__ */ ((CnnFearAndGreedFilter2) => {
811
+ CnnFearAndGreedFilter2["FearAndGreed"] = "fear_and_greed";
812
+ CnnFearAndGreedFilter2["FearAndGreedHistorical"] = "fear_and_greed_historical";
813
+ CnnFearAndGreedFilter2["MarketMomentumSp500"] = "market_momentum_sp500";
814
+ CnnFearAndGreedFilter2["MarketMomentumSp125"] = "market_momentum_sp125";
815
+ CnnFearAndGreedFilter2["StockPriceStrength"] = "stock_price_strength";
816
+ CnnFearAndGreedFilter2["StockPriceBreadth"] = "stock_price_breadth";
817
+ CnnFearAndGreedFilter2["PutCallOptions"] = "put_call_options";
818
+ CnnFearAndGreedFilter2["MarketVolatilityVix"] = "market_volatility_vix";
819
+ CnnFearAndGreedFilter2["MarketVolatilityVix50"] = "market_volatility_vix_50";
820
+ CnnFearAndGreedFilter2["JunkBondDemand"] = "junk_bond_demand";
821
+ CnnFearAndGreedFilter2["SafeHavenDemand"] = "safe_haven_demand";
822
+ return CnnFearAndGreedFilter2;
823
+ })(CnnFearAndGreedFilter || {});
824
+ var ALL_KEYS = Object.values(
825
+ CnnFearAndGreedFilter
826
+ );
827
+ function getDefaultHeaders() {
828
+ const chromeVersion = ConfigManager4.get("markets.chromeVersion") ?? "146";
829
+ return {
830
+ accept: "*/*",
831
+ "accept-language": "en,el;q=0.9",
832
+ origin: "https://edition.cnn.com",
833
+ priority: "u=1, i",
834
+ referer: "https://edition.cnn.com/",
835
+ "sec-ch-ua": `"Chromium";v="${chromeVersion}", "Not-A.Brand";v="24", "Google Chrome";v="${chromeVersion}"`,
836
+ "sec-ch-ua-mobile": "?0",
837
+ "sec-ch-ua-platform": '"Windows"',
838
+ "sec-fetch-dest": "empty",
839
+ "sec-fetch-mode": "cors",
840
+ "sec-fetch-site": "cross-site",
841
+ "user-agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}.0.0.0 Safari/537.36`
842
+ };
843
+ }
844
+ function getHeaders() {
845
+ const configHeaders = ConfigManager4.get("markets.cnn.headers");
846
+ return { ...getDefaultHeaders(), ...configHeaders ?? {} };
847
+ }
848
+ function log2(level, msg, data) {
849
+ const payload = data instanceof Error ? { error: serializeError3(data) } : data;
850
+ cnnLogger[level](msg, payload);
851
+ }
852
+ function buildUrl(date) {
853
+ const url = "https://production.dataviz.cnn.io/index/fearandgreed/graphdata";
854
+ if (date === "Historical") return url;
855
+ const finalDate = date ?? DateTime2.now().toISODate();
856
+ return `${url}/${finalDate}`;
857
+ }
858
+ function getFilteredValue(body, filter) {
859
+ if (filter === "full") return body;
860
+ const keys = Array.isArray(filter) ? filter : [filter];
861
+ if (keys.length === 1 && !Array.isArray(filter)) {
862
+ return body[keys[0]];
863
+ }
864
+ const result = {};
865
+ keys.forEach((k) => {
866
+ if (k in body) result[k] = body[k];
867
+ });
868
+ return result;
869
+ }
870
+ function validateKeys(body, filter) {
871
+ const keysToCheck = filter === "full" ? ALL_KEYS : Array.isArray(filter) ? filter : [filter];
872
+ for (const key of keysToCheck) {
873
+ if (!(key in body) || body[key] == null) {
874
+ return `Missing or null key: ${key}`;
875
+ }
876
+ }
877
+ return null;
878
+ }
879
+ async function getFearAndGreed(date, filter = "fear_and_greed" /* FearAndGreed */, options = {}) {
880
+ const cid = nextCid4();
881
+ const startedAt = performance.now();
882
+ const url = buildUrl(date);
883
+ log2("trace", "getFearAndGreed: start", { cid, date, filter });
884
+ const headers = { ...getHeaders(), ...options.headers ?? {} };
885
+ const result = await endPoint3(url, { ...options, headers });
886
+ if (result.status === "error") {
887
+ log2("error", "Transport Error", { url, reason: result.reason });
888
+ return {
889
+ status: "error",
890
+ reason: { message: "Transport Error", original: result.reason }
891
+ };
892
+ }
893
+ const val = result.value;
894
+ const body = val.body;
895
+ if (!body || typeof body !== "object") {
896
+ const msg = "Malformed CNN Response";
897
+ log2("error", msg);
898
+ return { status: "error", reason: { message: msg } };
899
+ }
900
+ const validationError = validateKeys(body, filter);
901
+ if (validationError) {
902
+ log2("warn", "Schema validation failed", { validationError, body });
903
+ return {
904
+ status: "error",
905
+ reason: {
906
+ message: `STRICT SCHEMA VALIDATION FAILED: ${validationError}`
907
+ }
908
+ };
909
+ }
910
+ const value = getFilteredValue(body, filter);
911
+ const sampleScore = value != null && typeof value === "object" && "score" in value ? value.score : void 0;
912
+ log2("trace", "getFearAndGreed: done", {
913
+ cid,
914
+ durationMs: performance.now() - startedAt,
915
+ filter,
916
+ sampleScore
917
+ });
918
+ return {
919
+ status: "success",
920
+ value,
921
+ details: { ...val, body: void 0 }
922
+ };
923
+ }
924
+ var CnnFearAndGreed = {
925
+ /**
926
+ * Fetches Fear & Greed Index data.
927
+ */
928
+ getFearAndGreed
929
+ };
930
+
931
+ // src/index.ts
932
+ import * as Luxon from "luxon";
933
+
934
+ // src/nasdaq/datafeeds/polling/historical/Historical.ts
935
+ import { endPoint as endPoint4 } from "@ckirg/corelib";
936
+ import yahooFinance from "@gadicc/yahoo-finance2";
937
+
938
+ // src/nasdaq/datafeeds/polling/historical/providers/YahooHistoricalProvider.ts
939
+ import { logger as logger3, nextCid as nextCid5 } from "@ckirg/corelib";
940
+ import { DateTime as DateTime3 } from "luxon";
941
+ import { serializeError as serializeError4 } from "serialize-error";
942
+ var yahooHistoricalLogger = logger3.child({
943
+ section: "YahooHistoricalProvider"
944
+ });
945
+ var YahooHistoricalProvider = class {
946
+ constructor(yf2) {
947
+ this.yf = yf2;
948
+ }
949
+ yf;
950
+ async getHistoricalData(symbol, options) {
951
+ const cid = nextCid5();
952
+ const startedAt = performance.now();
953
+ yahooHistoricalLogger.trace("historical: start", {
954
+ cid,
955
+ symbol,
956
+ period1: options.period1,
957
+ period2: options.period2,
958
+ interval: options.interval
959
+ });
960
+ try {
961
+ const queryOptions = {
962
+ period1: options.period1,
963
+ period2: options.period2 || /* @__PURE__ */ new Date()
964
+ };
965
+ if (options.interval) queryOptions.interval = options.interval;
966
+ const data = await this.yf.historical(symbol, queryOptions);
967
+ const value = data.map((item) => ({
968
+ symbol,
969
+ date: DateTime3.fromJSDate(item.date).toUTC().toISO() || item.date.toISOString(),
970
+ open: item.open,
971
+ high: item.high,
972
+ low: item.low,
973
+ close: item.close,
974
+ volume: item.volume,
975
+ adjClose: item.adjClose ?? null
976
+ }));
977
+ yahooHistoricalLogger.trace("historical: done", {
978
+ cid,
979
+ durationMs: performance.now() - startedAt,
980
+ symbol,
981
+ count: value.length,
982
+ sampleDate: value[0]?.date ?? null
983
+ });
984
+ return { status: "success", value };
985
+ } catch (error) {
986
+ const serialized = serializeError4(error);
987
+ yahooHistoricalLogger.trace("historical: error", {
988
+ cid,
989
+ durationMs: performance.now() - startedAt,
990
+ errorMsg: error instanceof Error ? error.message : String(error)
991
+ });
992
+ yahooHistoricalLogger.error("Yahoo provider failed", {
993
+ symbol,
994
+ error: serialized
995
+ });
996
+ return {
997
+ status: "error",
998
+ reason: {
999
+ message: serialized.message || "Failed to fetch historical data",
1000
+ payload: serialized
1001
+ }
1002
+ };
1003
+ }
1004
+ }
1005
+ };
1006
+
1007
+ // src/nasdaq/datafeeds/polling/historical/Historical.ts
1008
+ if (typeof globalThis.Deno === "undefined") {
1009
+ globalThis.Deno = {
1010
+ stdout: {
1011
+ isTerminal: () => false
1012
+ }
1013
+ };
1014
+ }
1015
+ async function corelibFetchAdapter(input, init) {
1016
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1017
+ const kyOptions = {
1018
+ method: init?.method ?? "GET",
1019
+ headers: init?.headers ?? {},
1020
+ body: init?.body,
1021
+ throwHttpErrors: false
1022
+ };
1023
+ const result = await endPoint4(url, kyOptions);
1024
+ if (result.status === "error" && !("status" in result.reason)) {
1025
+ throw new Error(
1026
+ result.reason.message || "Network Error in corelibFetchAdapter"
1027
+ );
1028
+ }
1029
+ const serialized = result.status === "success" ? result.value : result.reason;
1030
+ return {
1031
+ ok: serialized.ok,
1032
+ status: serialized.status,
1033
+ statusText: serialized.statusText,
1034
+ url: serialized.url,
1035
+ headers: new Headers(serialized.headers),
1036
+ text: async () => typeof serialized.body === "string" ? serialized.body : JSON.stringify(serialized.body),
1037
+ json: async () => typeof serialized.body === "string" ? JSON.parse(serialized.body) : serialized.body,
1038
+ blob: async () => new Blob([JSON.stringify(serialized.body)]),
1039
+ arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(serialized.body)).buffer,
1040
+ clone: function() {
1041
+ return this;
1042
+ },
1043
+ body: null,
1044
+ bodyUsed: false,
1045
+ redirected: serialized.redirected,
1046
+ type: serialized.type || "basic"
1047
+ };
1048
+ }
1049
+ var yf = new yahooFinance({
1050
+ fetch: corelibFetchAdapter,
1051
+ suppressNotices: ["ripHistorical"],
1052
+ versionCheck: false
1053
+ });
1054
+ var defaultProvider = new YahooHistoricalProvider(yf);
1055
+ var Historical = {
1056
+ /**
1057
+ * Retrieves historical data for a given symbol.
1058
+ */
1059
+ getData: (symbol, options) => defaultProvider.getHistoricalData(symbol, options)
1060
+ };
1061
+
1062
+ // src/nasdaq/datafeeds/polling/nasdaq/NasdaqPolling.ts
1063
+ import { EventEmitter } from "events";
1064
+ import { logger as logger4, nextCid as nextCid6 } from "@ckirg/corelib";
1065
+ import { serializeError as serializeError5 } from "serialize-error";
1066
+ var nasdaqPollingLogger = logger4.child({ section: "NasdaqPolling" });
1067
+ var NasdaqPolling = class extends EventEmitter {
1068
+ intervalId = null;
1069
+ subscriptions = /* @__PURE__ */ new Set();
1070
+ apiInterval;
1071
+ proxies;
1072
+ nasdaqQuotes;
1073
+ /**
1074
+ * @param apiInterval - Polling interval in milliseconds (defaults to 10000ms/10s).
1075
+ * @param proxies - Array of proxy URLs to rotate or use for requests.
1076
+ */
1077
+ constructor(apiInterval = 1e4, proxies = []) {
1078
+ super();
1079
+ this.apiInterval = apiInterval;
1080
+ this.proxies = proxies.map(
1081
+ (f) => f.endsWith("/") ? `${f}api/v1/markets/nasdaq` : `${f}/api/v1/markets/nasdaq`
1082
+ );
1083
+ this.nasdaqQuotes = new ApiNasdaqQuotes({
1084
+ proxies: this.proxies,
1085
+ logger: nasdaqPollingLogger
1086
+ });
1087
+ }
1088
+ /**
1089
+ * Updates the polling interval at runtime.
1090
+ * If polling is active, it will restart with the new interval.
1091
+ * @param value - New interval in milliseconds.
1092
+ */
1093
+ setApiInterval(value) {
1094
+ nasdaqPollingLogger.info(
1095
+ `Setting API interval from ${this.apiInterval}ms to ${value}ms`
1096
+ );
1097
+ this.apiInterval = value;
1098
+ if (this.intervalId !== null) {
1099
+ this.stop();
1100
+ this.start();
1101
+ }
1102
+ }
1103
+ /**
1104
+ * Adds symbols to the internal subscription list.
1105
+ * @param symbols - Array of stock symbols (e.g., ["AAPL", "MSFT"]).
1106
+ */
1107
+ subscribe(symbols) {
1108
+ for (const symbol of symbols) {
1109
+ this.subscriptions.add(symbol.toUpperCase());
1110
+ }
1111
+ nasdaqPollingLogger.info(
1112
+ `Subscribed to ${this.subscriptions.size} symbols`
1113
+ );
1114
+ }
1115
+ /**
1116
+ * Removes symbols from the internal subscription list.
1117
+ * @param symbols - Array of stock symbols to remove.
1118
+ */
1119
+ unsubscribe(symbols) {
1120
+ for (const symbol of symbols) {
1121
+ this.subscriptions.delete(symbol.toUpperCase());
1122
+ }
1123
+ nasdaqPollingLogger.info(
1124
+ `Unsubscribed from ${this.subscriptions.size} symbols`
1125
+ );
1126
+ }
1127
+ /**
1128
+ * Starts the polling process at the defined apiInterval.
1129
+ * If polling is already active, this method does nothing.
1130
+ */
1131
+ start() {
1132
+ if (this.intervalId !== null) {
1133
+ nasdaqPollingLogger.warn("Polling is already active.");
1134
+ return;
1135
+ }
1136
+ nasdaqPollingLogger.info(
1137
+ `Starting Nasdaq polling with interval ${this.apiInterval}ms and ${this.proxies.length} proxies.`
1138
+ );
1139
+ this.emit("status", "started");
1140
+ void this.poll();
1141
+ this.intervalId = setInterval(() => {
1142
+ void this.poll();
1143
+ }, this.apiInterval);
1144
+ }
1145
+ /**
1146
+ * Stops the polling process.
1147
+ * Existing subscriptions are preserved.
1148
+ */
1149
+ stop() {
1150
+ if (this.intervalId !== null) {
1151
+ clearInterval(this.intervalId);
1152
+ this.intervalId = null;
1153
+ nasdaqPollingLogger.info("Nasdaq polling stopped.");
1154
+ this.emit("status", "stopped");
1155
+ }
1156
+ }
1157
+ /**
1158
+ * Resets the internal subscription list and halts active polling.
1159
+ */
1160
+ clear() {
1161
+ this.subscriptions.clear();
1162
+ nasdaqPollingLogger.info("Subscriptions cleared.");
1163
+ this.stop();
1164
+ }
1165
+ /**
1166
+ * Internal logic to fetch data from ApiNasdaqQuotes and emit results.
1167
+ */
1168
+ async poll() {
1169
+ if (this.subscriptions.size === 0) {
1170
+ return;
1171
+ }
1172
+ const cid = nextCid6();
1173
+ const symbolList = Array.from(this.subscriptions);
1174
+ const startedAt = performance.now();
1175
+ nasdaqPollingLogger.trace("poll: start", {
1176
+ cid,
1177
+ requested: symbolList.length
1178
+ });
1179
+ try {
1180
+ const results = await this.nasdaqQuotes.getNasdaqQuote(symbolList);
1181
+ const validResults = [];
1182
+ for (let i = 0; i < results.length; i++) {
1183
+ const result = results[i];
1184
+ nasdaqPollingLogger.trace("poll: result", {
1185
+ cid,
1186
+ symbol: symbolList[i],
1187
+ status: result.status
1188
+ });
1189
+ if (result.status === "success" && result.value !== void 0) {
1190
+ this.emit("data", result.value);
1191
+ validResults.push(result.value);
1192
+ } else if (result.status === "error") {
1193
+ nasdaqPollingLogger.error("Error fetching quote", {
1194
+ cid,
1195
+ symbol: symbolList[i],
1196
+ error: serializeError5(result.reason)
1197
+ });
1198
+ this.emit("error", result.reason);
1199
+ }
1200
+ }
1201
+ const failed = results.length - validResults.length;
1202
+ const missing = symbolList.length - results.length;
1203
+ nasdaqPollingLogger.trace("poll: done", {
1204
+ cid,
1205
+ durationMs: performance.now() - startedAt,
1206
+ requested: symbolList.length,
1207
+ ok: validResults.length,
1208
+ failed,
1209
+ missing
1210
+ });
1211
+ if (validResults.length > 0) {
1212
+ this.emit("poll-complete", validResults);
1213
+ }
1214
+ } catch (error) {
1215
+ const serialized = serializeError5(error);
1216
+ nasdaqPollingLogger.trace("poll: error", {
1217
+ cid,
1218
+ durationMs: performance.now() - startedAt,
1219
+ errorMsg: error instanceof Error ? error.message : String(error)
1220
+ });
1221
+ nasdaqPollingLogger.error("Polling execution failed", {
1222
+ cid,
1223
+ error: serialized
1224
+ });
1225
+ this.emit("error", serialized);
1226
+ }
1227
+ }
1228
+ };
1229
+
1230
+ // src/nasdaq/datafeeds/streaming/alpaca/AlpacaStreaming.ts
1231
+ import { EventEmitter as EventEmitter2 } from "events";
1232
+ import { coreFFI, getMode, getTempDir as getTempDir2, logger as logger5, nextCid as nextCid7 } from "@ckirg/corelib";
1233
+
1234
+ // src/nasdaq/datafeeds/streaming/StreamHealthTracker.ts
1235
+ var DEFAULT_HEALTH_INTERVAL_MS = 15e3;
1236
+ var StreamHealthTracker = class {
1237
+ constructor(feed, log3, intervalMs = DEFAULT_HEALTH_INTERVAL_MS) {
1238
+ this.feed = feed;
1239
+ this.log = log3;
1240
+ this.intervalMs = intervalMs;
1241
+ }
1242
+ feed;
1243
+ log;
1244
+ intervalMs;
1245
+ windowTicks = 0;
1246
+ totalTicks = 0;
1247
+ lastTickAt = 0;
1248
+ subs = /* @__PURE__ */ new Map();
1249
+ windowStart = 0;
1250
+ timer = null;
1251
+ /** Call on every received tick. Cheap (counter + timestamp). */
1252
+ recordTick() {
1253
+ this.windowTicks++;
1254
+ this.totalTicks++;
1255
+ this.lastTickAt = performance.now();
1256
+ }
1257
+ /** Distinct subscribed-symbol count (refcounted across overlapping subscriptions). */
1258
+ get symbolCount() {
1259
+ return this.subs.size;
1260
+ }
1261
+ /** Record symbols subscribed. Refcounted so a symbol on multiple streams (quotes/trades/
1262
+ * bars) counts once and survives a partial unsubscribe — no undercount. */
1263
+ recordSubscribe(symbols) {
1264
+ for (const s of symbols) this.subs.set(s, (this.subs.get(s) ?? 0) + 1);
1265
+ }
1266
+ /** Record symbols unsubscribed. Drops a symbol only when its last subscription is gone. */
1267
+ recordUnsubscribe(symbols) {
1268
+ for (const s of symbols) {
1269
+ const n = (this.subs.get(s) ?? 0) - 1;
1270
+ if (n <= 0) this.subs.delete(s);
1271
+ else this.subs.set(s, n);
1272
+ }
1273
+ }
1274
+ /** Begin emitting periodic `stream: health`. Idempotent. */
1275
+ start() {
1276
+ if (this.timer) return;
1277
+ this.windowStart = performance.now();
1278
+ this.timer = setInterval(() => this.emitHealth(), this.intervalMs);
1279
+ this.timer.unref?.();
1280
+ }
1281
+ /** Stop the periodic health emission. */
1282
+ stop() {
1283
+ if (this.timer) {
1284
+ clearInterval(this.timer);
1285
+ this.timer = null;
1286
+ }
1287
+ }
1288
+ emitHealth() {
1289
+ const now = performance.now();
1290
+ const windowSec = (now - this.windowStart) / 1e3;
1291
+ const ticksPerSec = windowSec > 0 ? this.windowTicks / windowSec : 0;
1292
+ const lastTickAgoMs = this.lastTickAt > 0 ? Math.round(now - this.lastTickAt) : null;
1293
+ this.log.trace("stream: health", {
1294
+ feed: this.feed,
1295
+ symbols: this.subs.size,
1296
+ ticksPerSec: Math.round(ticksPerSec * 100) / 100,
1297
+ windowTicks: this.windowTicks,
1298
+ totalTicks: this.totalTicks,
1299
+ // Raw staleness — an AI flags a frozen feed by comparing this to the
1300
+ // expected cadence for the current market phase. No hardcoded threshold.
1301
+ lastTickAgoMs
1302
+ });
1303
+ this.windowTicks = 0;
1304
+ this.windowStart = now;
1305
+ }
1306
+ };
1307
+
1308
+ // src/nasdaq/datafeeds/streaming/alpaca/AlpacaStreaming.ts
1309
+ var RustAlpaca = coreFFI?.AlpacaStreaming;
1310
+ var alpacaStreamingLogger = logger5.child({ section: "AlpacaStreaming" });
1311
+ var AlpacaStreaming = class extends EventEmitter2 {
1312
+ rust;
1313
+ initialized = false;
1314
+ health = new StreamHealthTracker(
1315
+ "alpaca",
1316
+ alpacaStreamingLogger
1317
+ );
1318
+ constructor() {
1319
+ super();
1320
+ if (!RustAlpaca) {
1321
+ throw new Error(
1322
+ "AlpacaStreaming (Native) is not supported in this runtime (no FFI available)."
1323
+ );
1324
+ }
1325
+ try {
1326
+ this.rust = new RustAlpaca(
1327
+ (_err, record) => this.emit("log", record),
1328
+ (_err, data) => {
1329
+ this.health.recordTick();
1330
+ this.emit("pricing", data);
1331
+ },
1332
+ (_err, event) => {
1333
+ if (event) {
1334
+ this.emit(event.type, event.data ?? null);
1335
+ }
1336
+ },
1337
+ (_err, json) => {
1338
+ try {
1339
+ this.emit("market", JSON.parse(json));
1340
+ } catch {
1341
+ }
1342
+ }
1343
+ );
1344
+ } catch (e) {
1345
+ throw new Error(
1346
+ `AlpacaStreaming: native init failed (${e.message})`,
1347
+ { cause: e }
1348
+ );
1349
+ }
1350
+ if (getMode() === "development") {
1351
+ this.rust.clean();
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Initializes the configuration for the Alpaca streaming client.
1356
+ *
1357
+ * @param {object} [config] - Configuration options.
1358
+ * @param {string} [config.dbPath] - Path to the local persistence database. Defaults to system temp.
1359
+ * @param {number} [config.silenceSeconds] - Duration of silence (in seconds) before triggering a reconnect.
1360
+ * @param {string} [config.baseUrl] - Alpaca API base URL.
1361
+ * @param {string} [config.keyId] - Alpaca API Key ID.
1362
+ * @param {string} [config.secretKey] - Alpaca API Secret Key.
1363
+ * @returns {Promise<void>}
1364
+ */
1365
+ async init(config = {}) {
1366
+ const finalConfig = {
1367
+ dbPath: config.dbPath ?? `${getTempDir2()}/alpaca_streaming.redb`,
1368
+ silenceSeconds: config.silenceSeconds ?? 60,
1369
+ baseUrl: config.baseUrl ?? void 0,
1370
+ keyId: config.keyId ?? void 0,
1371
+ secretKey: config.secretKey ?? void 0
1372
+ };
1373
+ await this.rust.init(finalConfig);
1374
+ this.initialized = true;
1375
+ }
1376
+ /**
1377
+ * Starts the streaming client and begins connecting to Alpaca.
1378
+ * @returns {Promise<void>}
1379
+ */
1380
+ async start() {
1381
+ const cid = nextCid7();
1382
+ alpacaStreamingLogger.trace("stream: start", {
1383
+ cid,
1384
+ feed: "alpaca",
1385
+ symbols: this.health.symbolCount
1386
+ });
1387
+ if (!this.initialized) await this.init();
1388
+ await this.rust.start();
1389
+ this.health.start();
1390
+ }
1391
+ /**
1392
+ * Subscribes to real-time updates for the specified symbols.
1393
+ * @param {string[] | { trades?: string[]; quotes?: string[]; bars?: string[] }} input - Array of symbols (mapped to quotes) or subscription options object.
1394
+ */
1395
+ subscribe(input) {
1396
+ const opts = Array.isArray(input) ? { quotes: input } : input;
1397
+ this.health.recordSubscribe([
1398
+ ...opts.quotes ?? [],
1399
+ ...opts.trades ?? [],
1400
+ ...opts.bars ?? []
1401
+ ]);
1402
+ alpacaStreamingLogger.trace("stream: subscribe", {
1403
+ feed: "alpaca",
1404
+ symbols: this.health.symbolCount
1405
+ });
1406
+ this.rust.subscribe(opts);
1407
+ }
1408
+ /**
1409
+ * Unsubscribes from updates for the specified symbols.
1410
+ * @param {string[] | { trades?: string[]; quotes?: string[]; bars?: string[] }} input - Array of symbols (mapped to quotes) or subscription options object.
1411
+ */
1412
+ unsubscribe(input) {
1413
+ const opts = Array.isArray(input) ? { quotes: input } : input;
1414
+ this.health.recordUnsubscribe([
1415
+ ...opts.quotes ?? [],
1416
+ ...opts.trades ?? [],
1417
+ ...opts.bars ?? []
1418
+ ]);
1419
+ alpacaStreamingLogger.trace("stream: unsubscribe", {
1420
+ feed: "alpaca",
1421
+ symbols: this.health.symbolCount
1422
+ });
1423
+ this.rust.unsubscribe(opts);
1424
+ }
1425
+ /**
1426
+ * Cleans up the local state/database.
1427
+ */
1428
+ clean() {
1429
+ this.rust.clean();
1430
+ }
1431
+ /**
1432
+ * Stops the streaming client and disconnects.
1433
+ */
1434
+ stop() {
1435
+ alpacaStreamingLogger.trace("stream: stop", { feed: "alpaca" });
1436
+ this.health.stop();
1437
+ this.rust.stop();
1438
+ }
1439
+ };
1440
+
1441
+ // src/nasdaq/datafeeds/streaming/finnhub/FinnhubStreaming.ts
1442
+ import { EventEmitter as EventEmitter3 } from "events";
1443
+ import { coreFFI as coreFFI2, logger as logger6, nextCid as nextCid8 } from "@ckirg/corelib";
1444
+ var RustFinnhub = coreFFI2?.FinnhubStreaming;
1445
+ var finnhubStreamingLogger = logger6.child({ section: "FinnhubStreaming" });
1446
+ var FinnhubStreaming = class extends EventEmitter3 {
1447
+ rust;
1448
+ initialized = false;
1449
+ health = new StreamHealthTracker(
1450
+ "finnhub",
1451
+ finnhubStreamingLogger
1452
+ );
1453
+ constructor() {
1454
+ super();
1455
+ if (!RustFinnhub)
1456
+ throw new Error(
1457
+ "FinnhubStreaming (Native) is not supported in this runtime (no FFI available)."
1458
+ );
1459
+ try {
1460
+ this.rust = new RustFinnhub(
1461
+ // napi invokes TSFNs error-first: (err, data). Discard err, forward data.
1462
+ (_err, record) => this.emit("log", record),
1463
+ (_err, data) => {
1464
+ this.health.recordTick();
1465
+ this.emit("pricing", data);
1466
+ },
1467
+ (_err, event) => {
1468
+ if (event) this.emit(event.type, event.data ?? null);
1469
+ },
1470
+ (_err, json) => {
1471
+ try {
1472
+ this.emit("market", JSON.parse(json));
1473
+ } catch {
1474
+ }
1475
+ }
1476
+ );
1477
+ } catch (e) {
1478
+ throw new Error(
1479
+ `FinnhubStreaming: native init failed (${e.message})`,
1480
+ { cause: e }
1481
+ );
1482
+ }
1483
+ }
1484
+ async init(config = {}) {
1485
+ await this.rust.init({
1486
+ token: config.token ?? void 0,
1487
+ name: config.name ?? void 0,
1488
+ base_url: config.baseUrl ?? void 0
1489
+ });
1490
+ this.initialized = true;
1491
+ }
1492
+ async start() {
1493
+ const cid = nextCid8();
1494
+ finnhubStreamingLogger.trace("stream: start", {
1495
+ cid,
1496
+ feed: "finnhub",
1497
+ symbols: this.health.symbolCount
1498
+ });
1499
+ if (!this.initialized) await this.init();
1500
+ await this.rust.start();
1501
+ this.health.start();
1502
+ }
1503
+ async subscribe(symbols) {
1504
+ this.health.recordSubscribe(symbols);
1505
+ finnhubStreamingLogger.trace("stream: subscribe", {
1506
+ feed: "finnhub",
1507
+ symbols: this.health.symbolCount
1508
+ });
1509
+ await this.rust.subscribe(symbols);
1510
+ }
1511
+ async unsubscribe(symbols) {
1512
+ this.health.recordUnsubscribe(symbols);
1513
+ finnhubStreamingLogger.trace("stream: unsubscribe", {
1514
+ feed: "finnhub",
1515
+ symbols: this.health.symbolCount
1516
+ });
1517
+ await this.rust.unsubscribe(symbols);
1518
+ }
1519
+ async stop() {
1520
+ finnhubStreamingLogger.trace("stream: stop", { feed: "finnhub" });
1521
+ this.health.stop();
1522
+ await this.rust.stop();
1523
+ }
1524
+ async clean() {
1525
+ await this.rust.clean();
1526
+ }
1527
+ };
1528
+
1529
+ // src/nasdaq/datafeeds/streaming/yahoo/YahooStreaming.ts
1530
+ import { EventEmitter as EventEmitter4 } from "events";
1531
+ import { coreFFI as coreFFI3, getMode as getMode2, getTempDir as getTempDir3, logger as logger7, nextCid as nextCid9 } from "@ckirg/corelib";
1532
+ var RustYahoo = coreFFI3?.YahooStreaming;
1533
+ var yahooStreamingLogger = logger7.child({ section: "YahooStreaming" });
1534
+ var YahooStreaming = class extends EventEmitter4 {
1535
+ rust;
1536
+ initialized = false;
1537
+ health = new StreamHealthTracker(
1538
+ "yahoo",
1539
+ yahooStreamingLogger
1540
+ );
1541
+ constructor() {
1542
+ super();
1543
+ if (!RustYahoo) {
1544
+ throw new Error(
1545
+ "YahooStreaming (Native) is not supported in this runtime (no FFI available)."
1546
+ );
1547
+ }
1548
+ try {
1549
+ this.rust = new RustYahoo(
1550
+ (_err, record) => this.emit("log", record),
1551
+ (_err, data) => {
1552
+ this.health.recordTick();
1553
+ this.emit("pricing", data);
1554
+ },
1555
+ (_err, event) => {
1556
+ if (event) {
1557
+ this.emit(event.type, event.data ?? null);
1558
+ }
1559
+ },
1560
+ (_err, json) => {
1561
+ try {
1562
+ this.emit("market", JSON.parse(json));
1563
+ } catch {
1564
+ }
1565
+ }
1566
+ );
1567
+ } catch (e) {
1568
+ throw new Error(
1569
+ `YahooStreaming: native init failed (${e.message})`,
1570
+ { cause: e }
1571
+ );
1572
+ }
1573
+ if (getMode2() === "development") {
1574
+ this.rust.clean();
1575
+ }
1576
+ }
1577
+ /**
1578
+ * Initializes the configuration for the Yahoo streaming client.
1579
+ *
1580
+ * @param {object} [config] - Configuration options.
1581
+ * @param {string} [config.dbPath] - Path to the local persistence database.
1582
+ * @param {number} [config.silenceSeconds] - Duration of silence (in seconds) before triggering a reconnect.
1583
+ * @returns {Promise<void>}
1584
+ */
1585
+ async init(config = {}) {
1586
+ const finalConfig = {
1587
+ dbPath: config.dbPath ?? `${getTempDir3()}/yahoo_streaming.redb`,
1588
+ silenceSeconds: config.silenceSeconds ?? 60
1589
+ };
1590
+ await this.rust.init(finalConfig);
1591
+ this.initialized = true;
1592
+ }
1593
+ /**
1594
+ * Starts the streaming client and begins connecting.
1595
+ * @returns {Promise<void>}
1596
+ */
1597
+ async start() {
1598
+ const cid = nextCid9();
1599
+ yahooStreamingLogger.trace("stream: start", {
1600
+ cid,
1601
+ feed: "yahoo",
1602
+ symbols: this.health.symbolCount
1603
+ });
1604
+ if (!this.initialized) await this.init();
1605
+ await this.rust.start();
1606
+ this.health.start();
1607
+ }
1608
+ /**
1609
+ * Subscribes to real-time updates for the specified symbols.
1610
+ * @param {string[]} symbols - Array of trading symbols.
1611
+ */
1612
+ subscribe(symbols) {
1613
+ this.health.recordSubscribe(symbols);
1614
+ yahooStreamingLogger.trace("stream: subscribe", {
1615
+ feed: "yahoo",
1616
+ symbols: this.health.symbolCount
1617
+ });
1618
+ this.rust.subscribe(symbols);
1619
+ }
1620
+ /**
1621
+ * Unsubscribes from updates for the specified symbols.
1622
+ * @param {string[]} symbols - Array of trading symbols.
1623
+ */
1624
+ unsubscribe(symbols) {
1625
+ this.health.recordUnsubscribe(symbols);
1626
+ yahooStreamingLogger.trace("stream: unsubscribe", {
1627
+ feed: "yahoo",
1628
+ symbols: this.health.symbolCount
1629
+ });
1630
+ this.rust.unsubscribe(symbols);
1631
+ }
1632
+ /**
1633
+ * Cleans up the local state/database.
1634
+ */
1635
+ clean() {
1636
+ this.rust.clean();
1637
+ }
1638
+ /**
1639
+ * Stops the streaming client and disconnects.
1640
+ */
1641
+ stop() {
1642
+ yahooStreamingLogger.trace("stream: stop", { feed: "yahoo" });
1643
+ this.health.stop();
1644
+ this.rust.stop();
1645
+ }
1646
+ };
1647
+
1648
+ // src/nasdaq/groups/Top100.ts
1649
+ import { logger as logger8 } from "@ckirg/corelib";
1650
+ var top100Logger = logger8.child({ section: "Top100" });
1651
+ var cachedSymbols = null;
1652
+ var activeFetchPromise = null;
1653
+ async function getSymbolsTop100() {
1654
+ if (cachedSymbols !== null) {
1655
+ top100Logger.debug("top100: cache hit", { count: cachedSymbols.length });
1656
+ return cachedSymbols;
1657
+ }
1658
+ if (activeFetchPromise !== null) {
1659
+ top100Logger.debug("top100: join in-flight");
1660
+ return activeFetchPromise;
1661
+ }
1662
+ activeFetchPromise = (async () => {
1663
+ try {
1664
+ const url = "https://api.nasdaq.com/api/quote/list-type/nasdaq100";
1665
+ top100Logger.debug("top100: fetch", { url });
1666
+ const response = await ApiNasdaqUnlimited.endPoint(url);
1667
+ if (response.status === "error") {
1668
+ top100Logger.warn("Failed to fetch Nasdaq 100 symbols via API", {
1669
+ reason: response.reason
1670
+ });
1671
+ return [];
1672
+ }
1673
+ const rows = response.value?.data?.rows;
1674
+ if (!Array.isArray(rows) || rows.length === 0) {
1675
+ top100Logger.warn(
1676
+ "Nasdaq 100 API returned an empty or invalid dataset",
1677
+ {
1678
+ payload: response.value
1679
+ }
1680
+ );
1681
+ return [];
1682
+ }
1683
+ const symbols = rows.map((row) => row.symbol).sort((a, b) => a.localeCompare(b));
1684
+ cachedSymbols = symbols;
1685
+ top100Logger.debug("top100: populated", { count: symbols.length });
1686
+ return symbols;
1687
+ } catch (error) {
1688
+ top100Logger.warn("Unexpected error in Top100 module", {
1689
+ error: error instanceof Error ? error.message : String(error)
1690
+ });
1691
+ return [];
1692
+ } finally {
1693
+ activeFetchPromise = null;
1694
+ }
1695
+ })();
1696
+ return activeFetchPromise;
1697
+ }
1698
+
1699
+ // src/nasdaq/MarketMonitor.ts
1700
+ import { EventEmitter as EventEmitter5 } from "events";
1701
+ import { ConfigManager as ConfigManager6, endPoint as endPoint5, logger as logger10, nextCid as nextCid11 } from "@ckirg/corelib";
1702
+ import { DateTime as DateTime5 } from "luxon";
1703
+ import { serializeError as serializeError7 } from "serialize-error";
1704
+
1705
+ // src/nasdaq/MarketStatus.ts
1706
+ import { ConfigManager as ConfigManager5, logger as logger9, nextCid as nextCid10 } from "@ckirg/corelib";
1707
+ import { DateTime as DateTime4 } from "luxon";
1708
+ import { serializeError as serializeError6 } from "serialize-error";
1709
+ var DEFAULT_ENDPOINT = "https://api.nasdaq.com/api/market-info";
1710
+ var ZONE = "America/New_York";
1711
+ var marketStatusLogger = logger9.child({ section: "MarketStatus" });
1712
+ function getSleepDuration(data) {
1713
+ const now = DateTime4.now().setZone(ZONE);
1714
+ if (data.mrktStatus === "Open") {
1715
+ return 0;
1716
+ }
1717
+ const pmOpen = DateTime4.fromISO(data.pmOpenRaw, {
1718
+ zone: ZONE
1719
+ });
1720
+ const marketOpen = DateTime4.fromISO(data.openRaw, {
1721
+ zone: ZONE
1722
+ });
1723
+ let target = now < pmOpen ? pmOpen : marketOpen;
1724
+ if (target <= now) {
1725
+ const nextTrade = DateTime4.fromFormat(data.nextTradeDate, "MMM d, yyyy", {
1726
+ zone: ZONE
1727
+ });
1728
+ if (nextTrade.isValid) {
1729
+ target = nextTrade.set({
1730
+ hour: 4,
1731
+ minute: 0,
1732
+ second: 0,
1733
+ millisecond: 0
1734
+ });
1735
+ } else {
1736
+ marketStatusLogger.warn("Failed to parse nextTradeDate", {
1737
+ date: data.nextTradeDate
1738
+ });
1739
+ return 300 * 1e3;
1740
+ }
1741
+ }
1742
+ if (target > now) {
1743
+ const diff = target.diff(now);
1744
+ marketStatusLogger.debug(
1745
+ `Target NY Open: ${target.toFormat("yyyy-MM-dd HH:mm:ss")} (${diff.toFormat("hh:mm:ss")} remaining)`
1746
+ );
1747
+ const ms = diff.as("milliseconds");
1748
+ return ms > 0 ? ms : 60 * 1e3;
1749
+ }
1750
+ return 60 * 1e3;
1751
+ }
1752
+ async function getStatus() {
1753
+ const endpoint = ConfigManager5.get("markets.nasdaq.statusEndpoint") ?? DEFAULT_ENDPOINT;
1754
+ const cid = nextCid10();
1755
+ const startedAt = performance.now();
1756
+ marketStatusLogger.trace("status: fetch", { cid });
1757
+ try {
1758
+ const result = await ApiNasdaqUnlimited.endPoint(endpoint);
1759
+ if (result.status === "error") {
1760
+ const errorData = serializeError6(result.reason);
1761
+ const reasonSerialized = {
1762
+ ...errorData,
1763
+ message: errorData.message || "Nasdaq API returned an error status"
1764
+ };
1765
+ marketStatusLogger.trace("status: error", {
1766
+ cid,
1767
+ durationMs: performance.now() - startedAt,
1768
+ errorMsg: reasonSerialized.message
1769
+ });
1770
+ marketStatusLogger.error("Fetch Failed", {
1771
+ cid,
1772
+ reason: reasonSerialized
1773
+ });
1774
+ return { status: "error", reason: reasonSerialized };
1775
+ }
1776
+ const data = result.value;
1777
+ if (!data?.mrktStatus || !data.nextTradeDate || !data.pmOpenRaw || !data.openRaw) {
1778
+ const msg = "STRICT SCHEMA VALIDATION FAILED: Missing required fields";
1779
+ const payload = serializeError6(data);
1780
+ marketStatusLogger.trace("status: error", {
1781
+ cid,
1782
+ durationMs: performance.now() - startedAt,
1783
+ errorMsg: msg,
1784
+ schemaValid: false
1785
+ });
1786
+ marketStatusLogger.warn(msg, { cid, payload });
1787
+ return {
1788
+ status: "error",
1789
+ reason: { message: msg, payload }
1790
+ };
1791
+ }
1792
+ marketStatusLogger.trace("status: ok", {
1793
+ cid,
1794
+ durationMs: performance.now() - startedAt,
1795
+ marketStatus: data.mrktStatus,
1796
+ schemaValid: true
1797
+ });
1798
+ return {
1799
+ status: "success",
1800
+ value: data,
1801
+ details: result.details
1802
+ };
1803
+ } catch (e) {
1804
+ const errorData = serializeError6(e);
1805
+ const serializedReason = {
1806
+ ...errorData,
1807
+ message: errorData.message || "Unexpected MarketStatus Exception"
1808
+ };
1809
+ marketStatusLogger.trace("status: error", {
1810
+ cid,
1811
+ durationMs: performance.now() - startedAt,
1812
+ errorMsg: serializedReason.message
1813
+ });
1814
+ marketStatusLogger.error("Unexpected Error", {
1815
+ cid,
1816
+ error: serializedReason
1817
+ });
1818
+ return {
1819
+ status: "error",
1820
+ reason: serializedReason
1821
+ };
1822
+ }
1823
+ }
1824
+ var MarketStatus = {
1825
+ /**
1826
+ * Fetches the current market status.
1827
+ */
1828
+ getStatus,
1829
+ /**
1830
+ * Calculates the sleep duration until next market phase.
1831
+ */
1832
+ getSleepDuration
1833
+ };
1834
+
1835
+ // src/nasdaq/MarketMonitor.ts
1836
+ var marketMonitorLogger = logger10.child({ section: "MarketMonitor" });
1837
+ var DEFAULT_LIVE_INTERVAL_SEC = 10;
1838
+ var DEFAULT_CLOSED_INTERVAL_SEC = 3600;
1839
+ var DEFAULT_WARN_INTERVAL_SEC = 60;
1840
+ var MarketMonitor = class extends EventEmitter5 {
1841
+ liveIntervalSec;
1842
+ closedIntervalSec;
1843
+ warnIntervalSec;
1844
+ proxies;
1845
+ proxyIndex = 0;
1846
+ timeoutId = null;
1847
+ isRunning = false;
1848
+ lastData = null;
1849
+ lastPhase = "closed";
1850
+ lastWarnTime = 0;
1851
+ failureCount = 0;
1852
+ hasEmitted = false;
1853
+ /**
1854
+ * @param {object} [options] - Configuration options.
1855
+ * @param {number} [options.liveIntervalSec] - Polling interval in seconds when market is active.
1856
+ * @param {number} [options.closedIntervalSec] - Polling interval in seconds when market is closed.
1857
+ * @param {number} [options.warnIntervalSec] - Interval for logging fetch failure warnings.
1858
+ * @param {string[]} [options.proxies] - Optional array of proxy URLs for status fetching.
1859
+ */
1860
+ constructor(options = {}) {
1861
+ super();
1862
+ this.liveIntervalSec = options.liveIntervalSec ?? ConfigManager6.get("markets.nasdaq.monitor.liveIntervalSec") ?? DEFAULT_LIVE_INTERVAL_SEC;
1863
+ this.closedIntervalSec = options.closedIntervalSec ?? ConfigManager6.get("markets.nasdaq.monitor.closedIntervalSec") ?? DEFAULT_CLOSED_INTERVAL_SEC;
1864
+ this.warnIntervalSec = options.warnIntervalSec ?? ConfigManager6.get("markets.nasdaq.monitor.warnIntervalSec") ?? DEFAULT_WARN_INTERVAL_SEC;
1865
+ this.proxies = (options.proxies || []).map(
1866
+ (p) => p.endsWith("/") ? `${p}api/v1/markets/nasdaq/status` : `${p}/api/v1/markets/nasdaq/status`
1867
+ );
1868
+ }
1869
+ /** Start the monitor. First emission happens only after the first successful poll. */
1870
+ start() {
1871
+ if (this.isRunning) return;
1872
+ this.isRunning = true;
1873
+ this.failureCount = 0;
1874
+ marketMonitorLogger.info(
1875
+ `Starting market status monitor. Using ${this.proxies.length} proxies.`
1876
+ );
1877
+ this.poll();
1878
+ }
1879
+ /** Graceful shutdown. Clears timer and emits 'stopped'. */
1880
+ stop() {
1881
+ if (!this.isRunning) return;
1882
+ this.isRunning = false;
1883
+ if (this.timeoutId) {
1884
+ clearTimeout(this.timeoutId);
1885
+ this.timeoutId = null;
1886
+ }
1887
+ marketMonitorLogger.info("Monitor stopped");
1888
+ this.emit("stopped");
1889
+ }
1890
+ /** Current running state */
1891
+ get isRunningState() {
1892
+ return this.isRunning;
1893
+ }
1894
+ /** Last known phase (real or heuristic) */
1895
+ get currentPhase() {
1896
+ return this.lastPhase;
1897
+ }
1898
+ /** Last known full market data (null until first success) */
1899
+ get lastKnownData() {
1900
+ return this.lastData ? { ...this.lastData } : null;
1901
+ }
1902
+ /** Number of consecutive fetch failures (reset on success) */
1903
+ get failureCountValue() {
1904
+ return this.failureCount;
1905
+ }
1906
+ async poll() {
1907
+ if (!this.isRunning) return;
1908
+ const cid = nextCid11();
1909
+ const startedAt = performance.now();
1910
+ marketMonitorLogger.trace("phase: poll", { cid });
1911
+ let success = false;
1912
+ let source = "none";
1913
+ if (this.proxies.length > 0) {
1914
+ const startIdx = this.proxyIndex;
1915
+ for (let i = 0; i < this.proxies.length; i++) {
1916
+ const currentIdx = (startIdx + i) % this.proxies.length;
1917
+ const proxyUrl = this.proxies[currentIdx];
1918
+ try {
1919
+ const result = await endPoint5(proxyUrl);
1920
+ if (result.status === "success" && typeof result.value.body === "object" && result.value.body !== null) {
1921
+ const body = result.value.body;
1922
+ const data = body.value || body;
1923
+ if (data && typeof data === "object" && data.mrktStatus && data.openRaw) {
1924
+ this.handleSuccess(data, cid);
1925
+ this.proxyIndex = (currentIdx + 1) % this.proxies.length;
1926
+ success = true;
1927
+ source = "proxy";
1928
+ break;
1929
+ }
1930
+ }
1931
+ marketMonitorLogger.warn(
1932
+ "Proxy status fetch failed or returned unexpected format",
1933
+ { cid, proxyUrl }
1934
+ );
1935
+ } catch (err) {
1936
+ marketMonitorLogger.error("Error fetching via proxy", {
1937
+ cid,
1938
+ proxyUrl,
1939
+ error: serializeError7(err)
1940
+ });
1941
+ }
1942
+ }
1943
+ }
1944
+ if (!success) {
1945
+ try {
1946
+ const result = await MarketStatus.getStatus();
1947
+ if (result.status === "success") {
1948
+ this.handleSuccess(result.value, cid);
1949
+ success = true;
1950
+ source = "api";
1951
+ } else {
1952
+ this.handleFailure(cid);
1953
+ source = this.lastData ? "heuristic" : "none";
1954
+ }
1955
+ } catch (err) {
1956
+ marketMonitorLogger.error("Unexpected poll error", {
1957
+ cid,
1958
+ error: serializeError7(err)
1959
+ });
1960
+ this.handleFailure(cid);
1961
+ source = this.lastData ? "heuristic" : "none";
1962
+ }
1963
+ }
1964
+ marketMonitorLogger.trace("phase: poll-done", {
1965
+ cid,
1966
+ durationMs: performance.now() - startedAt,
1967
+ phase: this.lastPhase,
1968
+ heuristic: source === "heuristic",
1969
+ source
1970
+ });
1971
+ this.scheduleNextPoll();
1972
+ }
1973
+ handleSuccess(data, cid) {
1974
+ this.failureCount = 0;
1975
+ this.lastData = { ...data };
1976
+ const from = this.lastPhase;
1977
+ const phase = this.determinePhase(data);
1978
+ const phaseChanged = phase !== this.lastPhase;
1979
+ this.lastPhase = phase;
1980
+ if (!this.hasEmitted || phaseChanged) {
1981
+ marketMonitorLogger.trace("phase: change", {
1982
+ cid,
1983
+ from,
1984
+ to: phase,
1985
+ heuristic: false,
1986
+ failureCount: 0,
1987
+ rawStatus: data.mrktStatus
1988
+ });
1989
+ this.emit("status-change", phase, { ...data }, false);
1990
+ this.hasEmitted = true;
1991
+ }
1992
+ }
1993
+ handleFailure(cid) {
1994
+ this.failureCount++;
1995
+ if (!this.lastData) {
1996
+ this.maybeLogWarn();
1997
+ return;
1998
+ }
1999
+ const from = this.lastPhase;
2000
+ const computedEt = DateTime5.now().setZone("America/New_York").toISO();
2001
+ const phase = this.determinePhase({ ...this.lastData, mrktStatus: "" });
2002
+ const phaseChanged = phase !== this.lastPhase;
2003
+ this.lastPhase = phase;
2004
+ const heuristicData = {
2005
+ ...this.lastData,
2006
+ mrktStatus: "",
2007
+ // Official status is no longer valid during heuristic calculation
2008
+ heuristic: true
2009
+ };
2010
+ if (phaseChanged) {
2011
+ marketMonitorLogger.trace("phase: change", {
2012
+ cid,
2013
+ from,
2014
+ to: phase,
2015
+ heuristic: true,
2016
+ failureCount: this.failureCount,
2017
+ rawStatus: "",
2018
+ computedEt
2019
+ });
2020
+ this.emit("status-change", phase, heuristicData, true);
2021
+ }
2022
+ this.maybeLogWarn();
2023
+ }
2024
+ /**
2025
+ * Determine market phase.
2026
+ * 1. Try to normalize the official mrktStatus field first
2027
+ * 2. Fall back to precise time-based calculation using the four raw timestamps
2028
+ */
2029
+ determinePhase(data) {
2030
+ const rawStatus = (data.mrktStatus || "").toLowerCase().trim();
2031
+ if (rawStatus.includes("open") && !rawStatus.includes("after") && !rawStatus.includes("pre")) {
2032
+ return "open";
2033
+ }
2034
+ if (rawStatus.includes("pre") || rawStatus.includes("pre-market")) {
2035
+ return "pre-market";
2036
+ }
2037
+ if (rawStatus.includes("after") || rawStatus.includes("after-hours")) {
2038
+ return "after-hours";
2039
+ }
2040
+ if (rawStatus.includes("closed")) {
2041
+ return "closed";
2042
+ }
2043
+ const now = DateTime5.now().setZone("America/New_York");
2044
+ const pmOpen = DateTime5.fromISO(data.pmOpenRaw || "", {
2045
+ zone: "America/New_York"
2046
+ });
2047
+ const mOpen = DateTime5.fromISO(data.openRaw || "", {
2048
+ zone: "America/New_York"
2049
+ });
2050
+ const mClose = DateTime5.fromISO(data.closeRaw || "", {
2051
+ zone: "America/New_York"
2052
+ });
2053
+ const ahClose = DateTime5.fromISO(data.ahCloseRaw || "", {
2054
+ zone: "America/New_York"
2055
+ });
2056
+ if (!pmOpen.isValid || !mOpen.isValid) {
2057
+ return "closed";
2058
+ }
2059
+ if (now >= pmOpen && now < mOpen) return "pre-market";
2060
+ if (now >= mOpen && now < mClose) return "open";
2061
+ if (now >= mClose && now < ahClose) return "after-hours";
2062
+ return "closed";
2063
+ }
2064
+ scheduleNextPoll() {
2065
+ if (!this.isRunning) return;
2066
+ const intervalMs = this.getPollIntervalMs();
2067
+ this.timeoutId = setTimeout(() => this.poll(), intervalMs);
2068
+ }
2069
+ /**
2070
+ * Adaptive polling interval.
2071
+ * • No data yet → warnIntervalSec
2072
+ * • Has data → use liveIntervalSec or closedIntervalSec based on CURRENT (real or heuristic) phase
2073
+ */
2074
+ getPollIntervalMs() {
2075
+ if (!this.lastData) {
2076
+ return this.warnIntervalSec * 1e3;
2077
+ }
2078
+ const phase = this.determinePhase(this.lastData);
2079
+ return phase === "closed" ? this.closedIntervalSec * 1e3 : this.liveIntervalSec * 1e3;
2080
+ }
2081
+ maybeLogWarn() {
2082
+ const now = Date.now();
2083
+ if (now - this.lastWarnTime >= this.warnIntervalSec * 1e3) {
2084
+ marketMonitorLogger.warn(
2085
+ "MarketStatus fetch failed \u2013 using heuristic data",
2086
+ {
2087
+ failures: this.failureCount
2088
+ }
2089
+ );
2090
+ this.lastWarnTime = now;
2091
+ }
2092
+ }
2093
+ };
2094
+
2095
+ // src/index.ts
2096
+ var Markets = {
2097
+ /** Nasdaq-specific integrations. */
2098
+ nasdaq: {
2099
+ /** High-performance Nasdaq API wrapper. */
2100
+ ApiNasdaqUnlimited,
2101
+ /** Simple Nasdaq quote fetcher. */
2102
+ ApiNasdaqQuotes,
2103
+ /** Persistent Nasdaq symbol database. */
2104
+ MarketSymbols
2105
+ }
2106
+ };
2107
+ export {
2108
+ AlpacaStreaming,
2109
+ ApiNasdaqQuotes,
2110
+ ApiNasdaqUnlimited,
2111
+ CnnFearAndGreed,
2112
+ CnnFearAndGreedFilter,
2113
+ FinnhubStreaming,
2114
+ Historical,
2115
+ Luxon,
2116
+ MarketMonitor,
2117
+ MarketStatus,
2118
+ MarketSymbols,
2119
+ Markets,
2120
+ NasdaqPolling,
2121
+ YahooStreaming,
2122
+ getNasdaqHeaders,
2123
+ getSymbolsTop100
2124
+ };