@discomedia/utils 1.0.59 → 1.0.60
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/README.md +0 -2
- package/dist/index.cjs +0 -1378
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +0 -1378
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +1 -1
- package/dist/test.js +0 -225
- package/dist/test.js.map +1 -1
- package/dist/types/index.d.ts +0 -82
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/misc-utils.d.ts +0 -6
- package/dist/types/misc-utils.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +0 -2
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types-frontend/index.d.ts +0 -82
- package/dist/types-frontend/index.d.ts.map +1 -1
- package/dist/types-frontend/misc-utils.d.ts +0 -6
- package/dist/types-frontend/misc-utils.d.ts.map +1 -1
- package/dist/types-frontend/types/index.d.ts +0 -2
- package/dist/types-frontend/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/types/polygon-indices.d.ts +0 -85
- package/dist/types/polygon-indices.d.ts.map +0 -1
- package/dist/types/polygon.d.ts +0 -126
- package/dist/types/polygon.d.ts.map +0 -1
- package/dist/types/technical-analysis.d.ts +0 -90
- package/dist/types/technical-analysis.d.ts.map +0 -1
- package/dist/types/types/polygon-indices-types.d.ts +0 -190
- package/dist/types/types/polygon-indices-types.d.ts.map +0 -1
- package/dist/types/types/polygon-types.d.ts +0 -204
- package/dist/types/types/polygon-types.d.ts.map +0 -1
- package/dist/types-frontend/polygon-indices.d.ts +0 -85
- package/dist/types-frontend/polygon-indices.d.ts.map +0 -1
- package/dist/types-frontend/polygon.d.ts +0 -126
- package/dist/types-frontend/polygon.d.ts.map +0 -1
- package/dist/types-frontend/technical-analysis.d.ts +0 -90
- package/dist/types-frontend/technical-analysis.d.ts.map +0 -1
- package/dist/types-frontend/types/polygon-indices-types.d.ts +0 -190
- package/dist/types-frontend/types/polygon-indices-types.d.ts.map +0 -1
- package/dist/types-frontend/types/polygon-types.d.ts +0 -204
- package/dist/types-frontend/types/polygon-types.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1382,933 +1382,6 @@ async function fetchWithRetry(url, options = {}, retries = 3, initialBackoff = 1
|
|
|
1382
1382
|
}
|
|
1383
1383
|
throw new Error('Failed to fetch after multiple attempts');
|
|
1384
1384
|
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Validates a Polygon.io API key by making a test request.
|
|
1387
|
-
* @param apiKey - The API key to validate.
|
|
1388
|
-
* @returns Promise that resolves to true if valid, false otherwise.
|
|
1389
|
-
*/
|
|
1390
|
-
async function validatePolygonApiKey(apiKey) {
|
|
1391
|
-
try {
|
|
1392
|
-
const response = await fetch(`https://api.polygon.io/v1/meta/symbols?apikey=${apiKey}&limit=1`);
|
|
1393
|
-
if (response.status === 401) {
|
|
1394
|
-
throw new Error('Invalid or expired Polygon.io API key');
|
|
1395
|
-
}
|
|
1396
|
-
if (response.status === 403) {
|
|
1397
|
-
throw new Error('Polygon.io API key lacks required permissions');
|
|
1398
|
-
}
|
|
1399
|
-
return response.ok;
|
|
1400
|
-
}
|
|
1401
|
-
catch (error) {
|
|
1402
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1403
|
-
console.error('Polygon.io API key validation failed:', errorMessage);
|
|
1404
|
-
return false;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
/*
|
|
1409
|
-
How it works:
|
|
1410
|
-
`this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
|
|
1411
|
-
*/
|
|
1412
|
-
|
|
1413
|
-
class Node {
|
|
1414
|
-
value;
|
|
1415
|
-
next;
|
|
1416
|
-
|
|
1417
|
-
constructor(value) {
|
|
1418
|
-
this.value = value;
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
class Queue {
|
|
1423
|
-
#head;
|
|
1424
|
-
#tail;
|
|
1425
|
-
#size;
|
|
1426
|
-
|
|
1427
|
-
constructor() {
|
|
1428
|
-
this.clear();
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
enqueue(value) {
|
|
1432
|
-
const node = new Node(value);
|
|
1433
|
-
|
|
1434
|
-
if (this.#head) {
|
|
1435
|
-
this.#tail.next = node;
|
|
1436
|
-
this.#tail = node;
|
|
1437
|
-
} else {
|
|
1438
|
-
this.#head = node;
|
|
1439
|
-
this.#tail = node;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
this.#size++;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
dequeue() {
|
|
1446
|
-
const current = this.#head;
|
|
1447
|
-
if (!current) {
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
this.#head = this.#head.next;
|
|
1452
|
-
this.#size--;
|
|
1453
|
-
return current.value;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
peek() {
|
|
1457
|
-
if (!this.#head) {
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
return this.#head.value;
|
|
1462
|
-
|
|
1463
|
-
// TODO: Node.js 18.
|
|
1464
|
-
// return this.#head?.value;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
clear() {
|
|
1468
|
-
this.#head = undefined;
|
|
1469
|
-
this.#tail = undefined;
|
|
1470
|
-
this.#size = 0;
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
get size() {
|
|
1474
|
-
return this.#size;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
* [Symbol.iterator]() {
|
|
1478
|
-
let current = this.#head;
|
|
1479
|
-
|
|
1480
|
-
while (current) {
|
|
1481
|
-
yield current.value;
|
|
1482
|
-
current = current.next;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
* drain() {
|
|
1487
|
-
while (this.#head) {
|
|
1488
|
-
yield this.dequeue();
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
function pLimit(concurrency) {
|
|
1494
|
-
let rejectOnClear = false;
|
|
1495
|
-
|
|
1496
|
-
if (typeof concurrency === 'object') {
|
|
1497
|
-
({concurrency, rejectOnClear = false} = concurrency);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
validateConcurrency(concurrency);
|
|
1501
|
-
|
|
1502
|
-
if (typeof rejectOnClear !== 'boolean') {
|
|
1503
|
-
throw new TypeError('Expected `rejectOnClear` to be a boolean');
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const queue = new Queue();
|
|
1507
|
-
let activeCount = 0;
|
|
1508
|
-
|
|
1509
|
-
const resumeNext = () => {
|
|
1510
|
-
// Process the next queued function if we're under the concurrency limit
|
|
1511
|
-
if (activeCount < concurrency && queue.size > 0) {
|
|
1512
|
-
activeCount++;
|
|
1513
|
-
queue.dequeue().run();
|
|
1514
|
-
}
|
|
1515
|
-
};
|
|
1516
|
-
|
|
1517
|
-
const next = () => {
|
|
1518
|
-
activeCount--;
|
|
1519
|
-
resumeNext();
|
|
1520
|
-
};
|
|
1521
|
-
|
|
1522
|
-
const run = async (function_, resolve, arguments_) => {
|
|
1523
|
-
// Execute the function and capture the result promise
|
|
1524
|
-
const result = (async () => function_(...arguments_))();
|
|
1525
|
-
|
|
1526
|
-
// Resolve immediately with the promise (don't wait for completion)
|
|
1527
|
-
resolve(result);
|
|
1528
|
-
|
|
1529
|
-
// Wait for the function to complete (success or failure)
|
|
1530
|
-
// We catch errors here to prevent unhandled rejections,
|
|
1531
|
-
// but the original promise rejection is preserved for the caller
|
|
1532
|
-
try {
|
|
1533
|
-
await result;
|
|
1534
|
-
} catch {}
|
|
1535
|
-
|
|
1536
|
-
// Decrement active count and process next queued function
|
|
1537
|
-
next();
|
|
1538
|
-
};
|
|
1539
|
-
|
|
1540
|
-
const enqueue = (function_, resolve, reject, arguments_) => {
|
|
1541
|
-
const queueItem = {reject};
|
|
1542
|
-
|
|
1543
|
-
// Queue the internal resolve function instead of the run function
|
|
1544
|
-
// to preserve the asynchronous execution context.
|
|
1545
|
-
new Promise(internalResolve => { // eslint-disable-line promise/param-names
|
|
1546
|
-
queueItem.run = internalResolve;
|
|
1547
|
-
queue.enqueue(queueItem);
|
|
1548
|
-
}).then(run.bind(undefined, function_, resolve, arguments_)); // eslint-disable-line promise/prefer-await-to-then
|
|
1549
|
-
|
|
1550
|
-
// Start processing immediately if we haven't reached the concurrency limit
|
|
1551
|
-
if (activeCount < concurrency) {
|
|
1552
|
-
resumeNext();
|
|
1553
|
-
}
|
|
1554
|
-
};
|
|
1555
|
-
|
|
1556
|
-
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
|
|
1557
|
-
enqueue(function_, resolve, reject, arguments_);
|
|
1558
|
-
});
|
|
1559
|
-
|
|
1560
|
-
Object.defineProperties(generator, {
|
|
1561
|
-
activeCount: {
|
|
1562
|
-
get: () => activeCount,
|
|
1563
|
-
},
|
|
1564
|
-
pendingCount: {
|
|
1565
|
-
get: () => queue.size,
|
|
1566
|
-
},
|
|
1567
|
-
clearQueue: {
|
|
1568
|
-
value() {
|
|
1569
|
-
if (!rejectOnClear) {
|
|
1570
|
-
queue.clear();
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
const abortError = AbortSignal.abort().reason;
|
|
1575
|
-
|
|
1576
|
-
while (queue.size > 0) {
|
|
1577
|
-
queue.dequeue().reject(abortError);
|
|
1578
|
-
}
|
|
1579
|
-
},
|
|
1580
|
-
},
|
|
1581
|
-
concurrency: {
|
|
1582
|
-
get: () => concurrency,
|
|
1583
|
-
|
|
1584
|
-
set(newConcurrency) {
|
|
1585
|
-
validateConcurrency(newConcurrency);
|
|
1586
|
-
concurrency = newConcurrency;
|
|
1587
|
-
|
|
1588
|
-
queueMicrotask(() => {
|
|
1589
|
-
// eslint-disable-next-line no-unmodified-loop-condition
|
|
1590
|
-
while (activeCount < concurrency && queue.size > 0) {
|
|
1591
|
-
resumeNext();
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1594
|
-
},
|
|
1595
|
-
},
|
|
1596
|
-
map: {
|
|
1597
|
-
async value(iterable, function_) {
|
|
1598
|
-
const promises = Array.from(iterable, (value, index) => this(function_, value, index));
|
|
1599
|
-
return Promise.all(promises);
|
|
1600
|
-
},
|
|
1601
|
-
},
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
|
-
return generator;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
function validateConcurrency(concurrency) {
|
|
1608
|
-
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
|
|
1609
|
-
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
/**********************************************************************************
|
|
1614
|
-
* Polygon.io calls
|
|
1615
|
-
**********************************************************************************/
|
|
1616
|
-
// Constants from environment variables
|
|
1617
|
-
const POLYGON_API_KEY = process.env.POLYGON_API_KEY;
|
|
1618
|
-
// Define concurrency limits per API
|
|
1619
|
-
const POLYGON_CONCURRENCY_LIMIT = 100;
|
|
1620
|
-
const polygonLimit = pLimit(POLYGON_CONCURRENCY_LIMIT);
|
|
1621
|
-
// Use to update general information about stocks
|
|
1622
|
-
/**
|
|
1623
|
-
* Fetches general information about a stock ticker.
|
|
1624
|
-
* @param {string} symbol - The stock ticker symbol to fetch information for.
|
|
1625
|
-
* @param {Object} [options] - Optional parameters.
|
|
1626
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1627
|
-
* @returns {Promise<PolygonTickerInfo | null>} The ticker information or null if not found.
|
|
1628
|
-
*/
|
|
1629
|
-
const fetchTickerInfo = async (symbol, options) => {
|
|
1630
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1631
|
-
throw new Error('Polygon API key is missing');
|
|
1632
|
-
}
|
|
1633
|
-
const baseUrl = `https://api.polygon.io/v3/reference/tickers/${encodeURIComponent(symbol)}`;
|
|
1634
|
-
const params = new URLSearchParams({
|
|
1635
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1636
|
-
});
|
|
1637
|
-
return polygonLimit(async () => {
|
|
1638
|
-
try {
|
|
1639
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1640
|
-
const data = await response.json();
|
|
1641
|
-
// Check for "NOT_FOUND" status and return null
|
|
1642
|
-
if (data.status === 'NOT_FOUND') {
|
|
1643
|
-
console.warn(`Ticker not found: ${symbol}`);
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
// Map the results to the required structure
|
|
1647
|
-
const results = data.results;
|
|
1648
|
-
if (!results) {
|
|
1649
|
-
throw new Error('No results in Polygon API response');
|
|
1650
|
-
}
|
|
1651
|
-
// Validate required fields
|
|
1652
|
-
const requiredFields = [
|
|
1653
|
-
'active',
|
|
1654
|
-
'currency_name',
|
|
1655
|
-
'locale',
|
|
1656
|
-
'market',
|
|
1657
|
-
'name',
|
|
1658
|
-
'primary_exchange',
|
|
1659
|
-
'ticker',
|
|
1660
|
-
'type',
|
|
1661
|
-
];
|
|
1662
|
-
for (const field of requiredFields) {
|
|
1663
|
-
if (results[field] === undefined) {
|
|
1664
|
-
throw new Error(`Missing required field in Polygon API response: ${field}`);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
// Handle optional share_class_shares_outstanding field
|
|
1668
|
-
if (results.share_class_shares_outstanding === undefined) {
|
|
1669
|
-
results.share_class_shares_outstanding = null;
|
|
1670
|
-
}
|
|
1671
|
-
return {
|
|
1672
|
-
ticker: results.ticker,
|
|
1673
|
-
type: results.type,
|
|
1674
|
-
active: results.active,
|
|
1675
|
-
currency_name: results.currency_name,
|
|
1676
|
-
description: results.description ?? 'No description available',
|
|
1677
|
-
locale: results.locale,
|
|
1678
|
-
market: results.market,
|
|
1679
|
-
market_cap: results.market_cap ?? 0,
|
|
1680
|
-
name: results.name,
|
|
1681
|
-
primary_exchange: results.primary_exchange,
|
|
1682
|
-
share_class_shares_outstanding: results.share_class_shares_outstanding,
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
catch (error) {
|
|
1686
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1687
|
-
const contextualMessage = `Error fetching ticker info for ${symbol}`;
|
|
1688
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1689
|
-
symbol,
|
|
1690
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1691
|
-
? 'AUTH_ERROR'
|
|
1692
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1693
|
-
? 'RATE_LIMIT'
|
|
1694
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1695
|
-
? 'NETWORK_ERROR'
|
|
1696
|
-
: 'UNKNOWN',
|
|
1697
|
-
url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
|
|
1698
|
-
source: 'PolygonAPI.fetchTickerInfo',
|
|
1699
|
-
timestamp: new Date().toISOString(),
|
|
1700
|
-
});
|
|
1701
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1702
|
-
}
|
|
1703
|
-
});
|
|
1704
|
-
};
|
|
1705
|
-
// Fetch last trade using Polygon.io
|
|
1706
|
-
/**
|
|
1707
|
-
* Fetches the last trade for a given stock ticker.
|
|
1708
|
-
* @param {string} symbol - The stock ticker symbol to fetch the last trade for.
|
|
1709
|
-
* @param {Object} [options] - Optional parameters.
|
|
1710
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1711
|
-
* @returns {Promise<PolygonQuote>} The last trade information.
|
|
1712
|
-
*/
|
|
1713
|
-
const fetchLastTrade = async (symbol, options) => {
|
|
1714
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1715
|
-
throw new Error('Polygon API key is missing');
|
|
1716
|
-
}
|
|
1717
|
-
const baseUrl = `https://api.polygon.io/v2/last/trade/${encodeURIComponent(symbol)}`;
|
|
1718
|
-
const params = new URLSearchParams({
|
|
1719
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1720
|
-
});
|
|
1721
|
-
return polygonLimit(async () => {
|
|
1722
|
-
try {
|
|
1723
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1724
|
-
const data = await response.json();
|
|
1725
|
-
if (data.status !== 'OK' || !data.results) {
|
|
1726
|
-
throw new Error(`Polygon.io API error: ${data.status || 'No results'} ${data.error || ''}`);
|
|
1727
|
-
}
|
|
1728
|
-
const { p: price, s: vol, t: timestamp } = data.results;
|
|
1729
|
-
if (typeof price !== 'number' || typeof vol !== 'number' || typeof timestamp !== 'number') {
|
|
1730
|
-
throw new Error('Invalid trade data received from Polygon.io API');
|
|
1731
|
-
}
|
|
1732
|
-
return {
|
|
1733
|
-
price,
|
|
1734
|
-
vol,
|
|
1735
|
-
time: new Date(Math.floor(timestamp / 1000000)), // Convert nanoseconds to milliseconds
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
|
-
catch (error) {
|
|
1739
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1740
|
-
const contextualMessage = `Error fetching last trade for ${symbol}`;
|
|
1741
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1742
|
-
symbol,
|
|
1743
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1744
|
-
? 'AUTH_ERROR'
|
|
1745
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1746
|
-
? 'RATE_LIMIT'
|
|
1747
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1748
|
-
? 'NETWORK_ERROR'
|
|
1749
|
-
: 'UNKNOWN',
|
|
1750
|
-
url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
|
|
1751
|
-
source: 'PolygonAPI.fetchLastTrade',
|
|
1752
|
-
timestamp: new Date().toISOString(),
|
|
1753
|
-
});
|
|
1754
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1755
|
-
}
|
|
1756
|
-
});
|
|
1757
|
-
};
|
|
1758
|
-
// use Polygon for all price data fetching
|
|
1759
|
-
/**
|
|
1760
|
-
* Fetches price data for a given stock ticker.
|
|
1761
|
-
* @param {Object} params - The parameters for fetching price data.
|
|
1762
|
-
* @param {string} params.ticker - The stock ticker symbol.
|
|
1763
|
-
* @param {number} params.start - The start timestamp for fetching price data.
|
|
1764
|
-
* @param {number} [params.end] - The end timestamp for fetching price data.
|
|
1765
|
-
* @param {number} params.multiplier - The multiplier for the price data.
|
|
1766
|
-
* @param {string} params.timespan - The timespan for the price data.
|
|
1767
|
-
* @param {number} [params.limit] - The maximum number of price data points to fetch.
|
|
1768
|
-
* @param {Object} [options] - Optional parameters.
|
|
1769
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1770
|
-
* @returns {Promise<PolygonPriceData[]>} The fetched price data.
|
|
1771
|
-
*/
|
|
1772
|
-
const fetchPrices = async (params, options) => {
|
|
1773
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1774
|
-
throw new Error('Polygon API key is missing');
|
|
1775
|
-
}
|
|
1776
|
-
const { ticker, start, end = Date.now().valueOf(), multiplier, timespan, limit = 1000 } = params;
|
|
1777
|
-
const baseUrl = `https://api.polygon.io/v2/aggs/ticker/${encodeURIComponent(ticker)}/range/${multiplier}/${timespan}/${start}/${end}`;
|
|
1778
|
-
const urlParams = new URLSearchParams({
|
|
1779
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1780
|
-
adjusted: 'true',
|
|
1781
|
-
sort: 'asc',
|
|
1782
|
-
limit: limit.toString(),
|
|
1783
|
-
});
|
|
1784
|
-
return polygonLimit(async () => {
|
|
1785
|
-
try {
|
|
1786
|
-
let allResults = [];
|
|
1787
|
-
let nextUrl = `${baseUrl}?${urlParams.toString()}`;
|
|
1788
|
-
while (nextUrl) {
|
|
1789
|
-
//console.log(`Debug: Fetching ${nextUrl}`);
|
|
1790
|
-
const response = await fetchWithRetry(nextUrl, {}, 3, 1000);
|
|
1791
|
-
const data = await response.json();
|
|
1792
|
-
if (data.status !== 'OK') {
|
|
1793
|
-
throw new Error(`Polygon.io API responded with status: ${data.status}`);
|
|
1794
|
-
}
|
|
1795
|
-
if (data.results) {
|
|
1796
|
-
allResults = [...allResults, ...data.results];
|
|
1797
|
-
}
|
|
1798
|
-
// Check if there's a next page and append API key
|
|
1799
|
-
nextUrl = data.next_url ? `${data.next_url}&apiKey=${options?.apiKey || POLYGON_API_KEY}` : '';
|
|
1800
|
-
}
|
|
1801
|
-
return allResults.map((entry) => ({
|
|
1802
|
-
date: new Date(entry.t).toLocaleString('en-US', {
|
|
1803
|
-
year: 'numeric',
|
|
1804
|
-
month: 'short',
|
|
1805
|
-
day: '2-digit',
|
|
1806
|
-
hour: '2-digit',
|
|
1807
|
-
minute: '2-digit',
|
|
1808
|
-
second: '2-digit',
|
|
1809
|
-
timeZone: 'America/New_York',
|
|
1810
|
-
timeZoneName: 'short',
|
|
1811
|
-
hourCycle: 'h23',
|
|
1812
|
-
}),
|
|
1813
|
-
timeStamp: entry.t,
|
|
1814
|
-
open: entry.o,
|
|
1815
|
-
high: entry.h,
|
|
1816
|
-
low: entry.l,
|
|
1817
|
-
close: entry.c,
|
|
1818
|
-
vol: entry.v,
|
|
1819
|
-
vwap: entry.vw,
|
|
1820
|
-
trades: entry.n,
|
|
1821
|
-
}));
|
|
1822
|
-
}
|
|
1823
|
-
catch (error) {
|
|
1824
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1825
|
-
const contextualMessage = `Error fetching price data for ${ticker}`;
|
|
1826
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1827
|
-
ticker,
|
|
1828
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1829
|
-
? 'AUTH_ERROR'
|
|
1830
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1831
|
-
? 'RATE_LIMIT'
|
|
1832
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1833
|
-
? 'NETWORK_ERROR'
|
|
1834
|
-
: 'UNKNOWN',
|
|
1835
|
-
source: 'PolygonAPI.fetchPrices',
|
|
1836
|
-
timestamp: new Date().toISOString(),
|
|
1837
|
-
});
|
|
1838
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1839
|
-
}
|
|
1840
|
-
});
|
|
1841
|
-
};
|
|
1842
|
-
/**
|
|
1843
|
-
* Analyzes the price data for a given stock.
|
|
1844
|
-
* @param {PolygonPriceData[]} priceData - The price data to analyze.
|
|
1845
|
-
* @returns {string} The analysis report.
|
|
1846
|
-
*/
|
|
1847
|
-
function analysePolygonPriceData(priceData) {
|
|
1848
|
-
if (!priceData || priceData.length === 0) {
|
|
1849
|
-
return 'No price data available for analysis.';
|
|
1850
|
-
}
|
|
1851
|
-
// Parse the dates into Date objects
|
|
1852
|
-
const parsedData = priceData.map((entry) => ({
|
|
1853
|
-
...entry,
|
|
1854
|
-
date: new Date(entry.date),
|
|
1855
|
-
}));
|
|
1856
|
-
// Sort the data by date
|
|
1857
|
-
parsedData.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
1858
|
-
// Extract start and end times
|
|
1859
|
-
const startTime = parsedData[0].date;
|
|
1860
|
-
const endTime = parsedData[parsedData.length - 1].date;
|
|
1861
|
-
// Calculate the total time in hours
|
|
1862
|
-
(endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
|
|
1863
|
-
// Calculate the interval between data points
|
|
1864
|
-
const intervals = parsedData
|
|
1865
|
-
.slice(1)
|
|
1866
|
-
.map((_, i) => (parsedData[i + 1].date.getTime() - parsedData[i].date.getTime()) / 1000); // in seconds
|
|
1867
|
-
const avgInterval = intervals.length > 0 ? intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length : 0;
|
|
1868
|
-
// Format the report
|
|
1869
|
-
const report = `
|
|
1870
|
-
Report:
|
|
1871
|
-
* Start time of data (US Eastern): ${startTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}
|
|
1872
|
-
* End time of data (US Eastern): ${endTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}
|
|
1873
|
-
* Number of data points: ${priceData.length}
|
|
1874
|
-
* Average interval between data points (seconds): ${avgInterval.toFixed(2)}
|
|
1875
|
-
`;
|
|
1876
|
-
return report.trim();
|
|
1877
|
-
}
|
|
1878
|
-
/**
|
|
1879
|
-
* Fetches grouped daily price data for a specific date.
|
|
1880
|
-
* @param {string} date - The date to fetch grouped daily data for.
|
|
1881
|
-
* @param {Object} [options] - Optional parameters.
|
|
1882
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1883
|
-
* @param {boolean} [options.adjusted] - Whether to adjust the data.
|
|
1884
|
-
* @param {boolean} [options.includeOTC] - Whether to include OTC data.
|
|
1885
|
-
* @returns {Promise<PolygonGroupedDailyResponse>} The grouped daily response.
|
|
1886
|
-
*/
|
|
1887
|
-
const fetchGroupedDaily = async (date, options) => {
|
|
1888
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1889
|
-
throw new Error('Polygon API key is missing');
|
|
1890
|
-
}
|
|
1891
|
-
const baseUrl = `https://api.polygon.io/v2/aggs/grouped/locale/us/market/stocks/${date}`;
|
|
1892
|
-
const params = new URLSearchParams({
|
|
1893
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1894
|
-
adjusted: options?.adjusted !== false ? 'true' : 'false',
|
|
1895
|
-
include_otc: options?.includeOTC ? 'true' : 'false',
|
|
1896
|
-
});
|
|
1897
|
-
return polygonLimit(async () => {
|
|
1898
|
-
try {
|
|
1899
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1900
|
-
const data = await response.json();
|
|
1901
|
-
if (data.status !== 'OK') {
|
|
1902
|
-
throw new Error(`Polygon.io API responded with status: ${data.status}`);
|
|
1903
|
-
}
|
|
1904
|
-
return {
|
|
1905
|
-
adjusted: data.adjusted,
|
|
1906
|
-
queryCount: data.queryCount,
|
|
1907
|
-
request_id: data.request_id,
|
|
1908
|
-
resultsCount: data.resultsCount,
|
|
1909
|
-
status: data.status,
|
|
1910
|
-
results: data.results.map((result) => ({
|
|
1911
|
-
symbol: result.T,
|
|
1912
|
-
timeStamp: result.t,
|
|
1913
|
-
open: result.o,
|
|
1914
|
-
high: result.h,
|
|
1915
|
-
low: result.l,
|
|
1916
|
-
close: result.c,
|
|
1917
|
-
vol: result.v,
|
|
1918
|
-
vwap: result.vw,
|
|
1919
|
-
trades: result.n,
|
|
1920
|
-
})),
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
catch (error) {
|
|
1924
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1925
|
-
const contextualMessage = `Error fetching grouped daily data for ${date}`;
|
|
1926
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1927
|
-
date,
|
|
1928
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1929
|
-
? 'AUTH_ERROR'
|
|
1930
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1931
|
-
? 'RATE_LIMIT'
|
|
1932
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1933
|
-
? 'NETWORK_ERROR'
|
|
1934
|
-
: 'UNKNOWN',
|
|
1935
|
-
url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
|
|
1936
|
-
source: 'PolygonAPI.fetchGroupedDaily',
|
|
1937
|
-
timestamp: new Date().toISOString(),
|
|
1938
|
-
});
|
|
1939
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
};
|
|
1943
|
-
/**
|
|
1944
|
-
* Formats the price data into a readable string.
|
|
1945
|
-
* @param {PolygonPriceData[]} priceData - The price data to format.
|
|
1946
|
-
* @returns {string} The formatted price data.
|
|
1947
|
-
*/
|
|
1948
|
-
function formatPriceData(priceData) {
|
|
1949
|
-
if (!priceData || priceData.length === 0)
|
|
1950
|
-
return 'No price data available';
|
|
1951
|
-
return priceData
|
|
1952
|
-
.map((d) => {
|
|
1953
|
-
// For daily data, remove the time portion if it's all zeros
|
|
1954
|
-
const dateStr = d.date.includes(', 00:00:00') ? d.date.split(', 00:00:00')[0] : d.date;
|
|
1955
|
-
return [
|
|
1956
|
-
dateStr,
|
|
1957
|
-
`O: ${formatCurrency(d.open)}`,
|
|
1958
|
-
`H: ${formatCurrency(d.high)}`,
|
|
1959
|
-
`L: ${formatCurrency(d.low)}`,
|
|
1960
|
-
`C: ${formatCurrency(d.close)}`,
|
|
1961
|
-
`Vol: ${d.vol}`,
|
|
1962
|
-
].join(' | ');
|
|
1963
|
-
})
|
|
1964
|
-
.join('\n');
|
|
1965
|
-
}
|
|
1966
|
-
const fetchDailyOpenClose = async (
|
|
1967
|
-
/**
|
|
1968
|
-
* Fetches the daily open and close data for a given stock ticker.
|
|
1969
|
-
* @param {string} symbol - The stock ticker symbol to fetch data for.
|
|
1970
|
-
* @param {Date} [date=new Date()] - The date to fetch data for.
|
|
1971
|
-
* @param {Object} [options] - Optional parameters.
|
|
1972
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1973
|
-
* @param {boolean} [options.adjusted] - Whether to adjust the data.
|
|
1974
|
-
* @returns {Promise<PolygonDailyOpenClose>} The daily open and close data.
|
|
1975
|
-
*/
|
|
1976
|
-
symbol, date = new Date(), options) => {
|
|
1977
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1978
|
-
throw new Error('Polygon API key is missing');
|
|
1979
|
-
}
|
|
1980
|
-
const formattedDate = date.toISOString().split('T')[0]; // Format as YYYY-MM-DD
|
|
1981
|
-
const baseUrl = `https://api.polygon.io/v1/open-close/${encodeURIComponent(symbol)}/${formattedDate}`;
|
|
1982
|
-
const params = new URLSearchParams({
|
|
1983
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1984
|
-
adjusted: (options?.adjusted ?? true).toString(),
|
|
1985
|
-
});
|
|
1986
|
-
return polygonLimit(async () => {
|
|
1987
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1988
|
-
const data = await response.json();
|
|
1989
|
-
if (data.status !== 'OK') {
|
|
1990
|
-
throw new Error(`Failed to fetch daily open/close data for ${symbol}: ${data.status}`);
|
|
1991
|
-
}
|
|
1992
|
-
return data;
|
|
1993
|
-
});
|
|
1994
|
-
};
|
|
1995
|
-
/**
|
|
1996
|
-
* Gets the previous close price for a given stock ticker.
|
|
1997
|
-
* @param {string} symbol - The stock ticker symbol to fetch the previous close for.
|
|
1998
|
-
* @param {Date} [referenceDate] - The reference date to use for fetching the previous close.
|
|
1999
|
-
* @returns {Promise<{ close: number; date: Date }>} The previous close price and date.
|
|
2000
|
-
*/
|
|
2001
|
-
async function getPreviousClose(symbol, referenceDate, options) {
|
|
2002
|
-
const previousDate = getLastFullTradingDate(referenceDate);
|
|
2003
|
-
const lastOpenClose = await fetchDailyOpenClose(symbol, previousDate, options);
|
|
2004
|
-
if (!lastOpenClose) {
|
|
2005
|
-
throw new Error(`Could not fetch last trade price for ${symbol}`);
|
|
2006
|
-
}
|
|
2007
|
-
return {
|
|
2008
|
-
close: lastOpenClose.close,
|
|
2009
|
-
date: previousDate,
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
/**
|
|
2013
|
-
* Fetches trade data for a given stock ticker.
|
|
2014
|
-
* @param {string} symbol - The stock ticker symbol to fetch trades for.
|
|
2015
|
-
* @param {Object} [options] - Optional parameters.
|
|
2016
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
2017
|
-
* @param {string | number} [options.timestamp] - The timestamp for fetching trades.
|
|
2018
|
-
* @param {string | number} [options.timestampgt] - Greater than timestamp for fetching trades.
|
|
2019
|
-
* @param {string | number} [options.timestampgte] - Greater than or equal to timestamp for fetching trades.
|
|
2020
|
-
* @param {string | number} [options.timestamplt] - Less than timestamp for fetching trades.
|
|
2021
|
-
* @param {string | number} [options.timestamplte] - Less than or equal to timestamp for fetching trades.
|
|
2022
|
-
* @param {'asc' | 'desc'} [options.order] - The order of the trades.
|
|
2023
|
-
* @param {number} [options.limit] - The maximum number of trades to fetch.
|
|
2024
|
-
* @param {string} [options.sort] - The sort order for the trades.
|
|
2025
|
-
* @returns {Promise<PolygonTradesResponse>} The fetched trades response.
|
|
2026
|
-
*/
|
|
2027
|
-
const fetchTrades = async (symbol, options) => {
|
|
2028
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
2029
|
-
throw new Error('Polygon API key is missing');
|
|
2030
|
-
}
|
|
2031
|
-
const baseUrl = `https://api.polygon.io/v3/trades/${encodeURIComponent(symbol)}`;
|
|
2032
|
-
const params = new URLSearchParams({
|
|
2033
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
2034
|
-
});
|
|
2035
|
-
// Add optional parameters if they exist
|
|
2036
|
-
if (options?.timestamp)
|
|
2037
|
-
params.append('timestamp', options.timestamp.toString());
|
|
2038
|
-
if (options?.timestampgt)
|
|
2039
|
-
params.append('timestamp.gt', options.timestampgt.toString());
|
|
2040
|
-
if (options?.timestampgte)
|
|
2041
|
-
params.append('timestamp.gte', options.timestampgte.toString());
|
|
2042
|
-
if (options?.timestamplt)
|
|
2043
|
-
params.append('timestamp.lt', options.timestamplt.toString());
|
|
2044
|
-
if (options?.timestamplte)
|
|
2045
|
-
params.append('timestamp.lte', options.timestamplte.toString());
|
|
2046
|
-
if (options?.order)
|
|
2047
|
-
params.append('order', options.order);
|
|
2048
|
-
if (options?.limit)
|
|
2049
|
-
params.append('limit', options.limit.toString());
|
|
2050
|
-
if (options?.sort)
|
|
2051
|
-
params.append('sort', options.sort);
|
|
2052
|
-
return polygonLimit(async () => {
|
|
2053
|
-
const url = `${baseUrl}?${params.toString()}`;
|
|
2054
|
-
try {
|
|
2055
|
-
console.log(`[DEBUG] Fetching trades for ${symbol} from ${url}`);
|
|
2056
|
-
const response = await fetchWithRetry(url, {}, 3, 1000);
|
|
2057
|
-
const data = (await response.json());
|
|
2058
|
-
if ('message' in data) {
|
|
2059
|
-
// This is an error response
|
|
2060
|
-
throw new Error(`Polygon API Error: ${data.message}`);
|
|
2061
|
-
}
|
|
2062
|
-
return data;
|
|
2063
|
-
}
|
|
2064
|
-
catch (error) {
|
|
2065
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2066
|
-
const contextualMessage = `Error fetching trades for ${symbol}`;
|
|
2067
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
2068
|
-
symbol,
|
|
2069
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
2070
|
-
? 'AUTH_ERROR'
|
|
2071
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
2072
|
-
? 'RATE_LIMIT'
|
|
2073
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
2074
|
-
? 'NETWORK_ERROR'
|
|
2075
|
-
: 'UNKNOWN',
|
|
2076
|
-
url: hideApiKeyFromurl(url),
|
|
2077
|
-
source: 'PolygonAPI.fetchTrades',
|
|
2078
|
-
timestamp: new Date().toISOString(),
|
|
2079
|
-
});
|
|
2080
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
2081
|
-
}
|
|
2082
|
-
});
|
|
2083
|
-
};
|
|
2084
|
-
|
|
2085
|
-
/**
|
|
2086
|
-
* Polygon Indices API Implementation
|
|
2087
|
-
*
|
|
2088
|
-
* This module provides functions to interact with the Polygon.io Indices API.
|
|
2089
|
-
*/
|
|
2090
|
-
// Constants from environment variables
|
|
2091
|
-
const { ALPACA_INDICES_API_KEY } = process.env;
|
|
2092
|
-
// Define concurrency limits for API
|
|
2093
|
-
const POLYGON_INDICES_CONCURRENCY_LIMIT = 5;
|
|
2094
|
-
const polygonIndicesLimit = pLimit(POLYGON_INDICES_CONCURRENCY_LIMIT);
|
|
2095
|
-
// Base URL for Polygon API
|
|
2096
|
-
const POLYGON_API_BASE_URL = 'https://api.polygon.io';
|
|
2097
|
-
/**
|
|
2098
|
-
* Validates that an API key is available
|
|
2099
|
-
* @param {string | undefined} apiKey - Optional API key to use
|
|
2100
|
-
* @throws {Error} If no API key is available
|
|
2101
|
-
*/
|
|
2102
|
-
const validateApiKey = (apiKey) => {
|
|
2103
|
-
const key = apiKey || ALPACA_INDICES_API_KEY;
|
|
2104
|
-
if (!key) {
|
|
2105
|
-
throw new Error('Polygon Indices API key is missing');
|
|
2106
|
-
}
|
|
2107
|
-
return key;
|
|
2108
|
-
};
|
|
2109
|
-
/**
|
|
2110
|
-
* Fetches aggregate bars for an index over a given date range in custom time window sizes.
|
|
2111
|
-
*
|
|
2112
|
-
* @param {PolygonIndicesAggregatesParams} params - Parameters for the aggregates request
|
|
2113
|
-
* @param {Object} [options] - Optional parameters
|
|
2114
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2115
|
-
* @returns {Promise<PolygonIndicesAggregatesResponse>} The aggregates response
|
|
2116
|
-
*/
|
|
2117
|
-
const fetchIndicesAggregates = async (params, options) => {
|
|
2118
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2119
|
-
const { indicesTicker, multiplier, timespan, from, to, sort = 'asc', limit } = params;
|
|
2120
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v2/aggs/ticker/${encodeURIComponent(indicesTicker)}/range/${multiplier}/${timespan}/${from}/${to}`);
|
|
2121
|
-
const queryParams = new URLSearchParams();
|
|
2122
|
-
queryParams.append('apiKey', apiKey);
|
|
2123
|
-
if (sort) {
|
|
2124
|
-
queryParams.append('sort', sort);
|
|
2125
|
-
}
|
|
2126
|
-
if (limit) {
|
|
2127
|
-
queryParams.append('limit', limit.toString());
|
|
2128
|
-
}
|
|
2129
|
-
url.search = queryParams.toString();
|
|
2130
|
-
return polygonIndicesLimit(async () => {
|
|
2131
|
-
try {
|
|
2132
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2133
|
-
const data = await response.json();
|
|
2134
|
-
if (data.status === 'ERROR') {
|
|
2135
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2136
|
-
}
|
|
2137
|
-
return data;
|
|
2138
|
-
}
|
|
2139
|
-
catch (error) {
|
|
2140
|
-
console.error('Error fetching indices aggregates:', error);
|
|
2141
|
-
throw error;
|
|
2142
|
-
}
|
|
2143
|
-
});
|
|
2144
|
-
};
|
|
2145
|
-
/**
|
|
2146
|
-
* Gets the previous day's open, high, low, and close (OHLC) for the specified index.
|
|
2147
|
-
*
|
|
2148
|
-
* @param {string} indicesTicker - The ticker symbol of the index
|
|
2149
|
-
* @param {Object} [options] - Optional parameters
|
|
2150
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2151
|
-
* @returns {Promise<PolygonIndicesPrevCloseResponse>} The previous close response
|
|
2152
|
-
*/
|
|
2153
|
-
const fetchIndicesPreviousClose = async (indicesTicker, options) => {
|
|
2154
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2155
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v2/aggs/ticker/${encodeURIComponent(indicesTicker)}/prev`);
|
|
2156
|
-
const queryParams = new URLSearchParams();
|
|
2157
|
-
queryParams.append('apiKey', apiKey);
|
|
2158
|
-
url.search = queryParams.toString();
|
|
2159
|
-
return polygonIndicesLimit(async () => {
|
|
2160
|
-
try {
|
|
2161
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2162
|
-
const data = await response.json();
|
|
2163
|
-
if (data.status === 'ERROR') {
|
|
2164
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2165
|
-
}
|
|
2166
|
-
return data;
|
|
2167
|
-
}
|
|
2168
|
-
catch (error) {
|
|
2169
|
-
console.error('Error fetching indices previous close:', error);
|
|
2170
|
-
throw error;
|
|
2171
|
-
}
|
|
2172
|
-
});
|
|
2173
|
-
};
|
|
2174
|
-
/**
|
|
2175
|
-
* Gets the open, close and afterhours values of an index symbol on a certain date.
|
|
2176
|
-
*
|
|
2177
|
-
* @param {string} indicesTicker - The ticker symbol of the index
|
|
2178
|
-
* @param {string} date - The date in YYYY-MM-DD format
|
|
2179
|
-
* @param {Object} [options] - Optional parameters
|
|
2180
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2181
|
-
* @returns {Promise<PolygonIndicesDailyOpenCloseResponse>} The daily open/close response
|
|
2182
|
-
*/
|
|
2183
|
-
const fetchIndicesDailyOpenClose = async (indicesTicker, date, options) => {
|
|
2184
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2185
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v1/open-close/${encodeURIComponent(indicesTicker)}/${date}`);
|
|
2186
|
-
const queryParams = new URLSearchParams();
|
|
2187
|
-
queryParams.append('apiKey', apiKey);
|
|
2188
|
-
url.search = queryParams.toString();
|
|
2189
|
-
return polygonIndicesLimit(async () => {
|
|
2190
|
-
try {
|
|
2191
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2192
|
-
const data = await response.json();
|
|
2193
|
-
if (data.status === 'ERROR') {
|
|
2194
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2195
|
-
}
|
|
2196
|
-
return data;
|
|
2197
|
-
}
|
|
2198
|
-
catch (error) {
|
|
2199
|
-
console.error('Error fetching indices daily open/close:', error);
|
|
2200
|
-
throw error;
|
|
2201
|
-
}
|
|
2202
|
-
});
|
|
2203
|
-
};
|
|
2204
|
-
/**
|
|
2205
|
-
* Gets a snapshot of indices data for specified tickers.
|
|
2206
|
-
*
|
|
2207
|
-
* @param {PolygonIndicesSnapshotParams} [params] - Parameters for the snapshot request
|
|
2208
|
-
* @param {Object} [options] - Optional parameters
|
|
2209
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2210
|
-
* @returns {Promise<PolygonIndicesSnapshotResponse>} The indices snapshot response
|
|
2211
|
-
*/
|
|
2212
|
-
const fetchIndicesSnapshot = async (params, options) => {
|
|
2213
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2214
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v3/snapshot/indices`);
|
|
2215
|
-
const queryParams = new URLSearchParams();
|
|
2216
|
-
queryParams.append('apiKey', apiKey);
|
|
2217
|
-
if (params?.tickers?.length) {
|
|
2218
|
-
queryParams.append('ticker.any_of', params.tickers.join(','));
|
|
2219
|
-
}
|
|
2220
|
-
if (params?.order) {
|
|
2221
|
-
queryParams.append('order', params.order);
|
|
2222
|
-
}
|
|
2223
|
-
if (params?.limit) {
|
|
2224
|
-
queryParams.append('limit', params.limit.toString());
|
|
2225
|
-
}
|
|
2226
|
-
if (params?.sort) {
|
|
2227
|
-
queryParams.append('sort', params.sort);
|
|
2228
|
-
}
|
|
2229
|
-
url.search = queryParams.toString();
|
|
2230
|
-
return polygonIndicesLimit(async () => {
|
|
2231
|
-
try {
|
|
2232
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2233
|
-
const data = await response.json();
|
|
2234
|
-
if (data.status === 'ERROR') {
|
|
2235
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2236
|
-
}
|
|
2237
|
-
return data;
|
|
2238
|
-
}
|
|
2239
|
-
catch (error) {
|
|
2240
|
-
console.error('Error fetching indices snapshot:', error);
|
|
2241
|
-
throw error;
|
|
2242
|
-
}
|
|
2243
|
-
});
|
|
2244
|
-
};
|
|
2245
|
-
/**
|
|
2246
|
-
* Gets snapshots for assets of all types, including indices.
|
|
2247
|
-
*
|
|
2248
|
-
* @param {string[]} tickers - Array of tickers to fetch snapshots for
|
|
2249
|
-
* @param {Object} [options] - Optional parameters
|
|
2250
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2251
|
-
* @param {string} [options.type] - Filter by asset type
|
|
2252
|
-
* @param {string} [options.order] - Order results
|
|
2253
|
-
* @param {number} [options.limit] - Limit the number of results
|
|
2254
|
-
* @param {string} [options.sort] - Sort field
|
|
2255
|
-
* @returns {Promise<any>} The universal snapshot response
|
|
2256
|
-
*/
|
|
2257
|
-
const fetchUniversalSnapshot = async (tickers, options) => {
|
|
2258
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2259
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v3/snapshot`);
|
|
2260
|
-
const queryParams = new URLSearchParams();
|
|
2261
|
-
queryParams.append('apiKey', apiKey);
|
|
2262
|
-
if (tickers.length) {
|
|
2263
|
-
queryParams.append('ticker.any_of', tickers.join(','));
|
|
2264
|
-
}
|
|
2265
|
-
if (options?.type) {
|
|
2266
|
-
queryParams.append('type', options.type);
|
|
2267
|
-
}
|
|
2268
|
-
if (options?.order) {
|
|
2269
|
-
queryParams.append('order', options.order);
|
|
2270
|
-
}
|
|
2271
|
-
if (options?.limit) {
|
|
2272
|
-
queryParams.append('limit', options.limit.toString());
|
|
2273
|
-
}
|
|
2274
|
-
if (options?.sort) {
|
|
2275
|
-
queryParams.append('sort', options.sort);
|
|
2276
|
-
}
|
|
2277
|
-
url.search = queryParams.toString();
|
|
2278
|
-
return polygonIndicesLimit(async () => {
|
|
2279
|
-
try {
|
|
2280
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2281
|
-
const data = await response.json();
|
|
2282
|
-
if (data.status === 'ERROR') {
|
|
2283
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2284
|
-
}
|
|
2285
|
-
return data;
|
|
2286
|
-
}
|
|
2287
|
-
catch (error) {
|
|
2288
|
-
console.error('Error fetching universal snapshot:', error);
|
|
2289
|
-
throw error;
|
|
2290
|
-
}
|
|
2291
|
-
});
|
|
2292
|
-
};
|
|
2293
|
-
/**
|
|
2294
|
-
* Converts Polygon Indices bar data to a more standardized format
|
|
2295
|
-
*
|
|
2296
|
-
* @param {PolygonIndicesAggregatesResponse} data - The raw aggregates response
|
|
2297
|
-
* @returns {Array<{date: string, open: number, high: number, low: number, close: number, timestamp: number}>} Formatted bar data
|
|
2298
|
-
*/
|
|
2299
|
-
const formatIndicesBarData = (data) => {
|
|
2300
|
-
return data.results.map((bar) => {
|
|
2301
|
-
const date = new Date(bar.t);
|
|
2302
|
-
return {
|
|
2303
|
-
date: date.toISOString().split('T')[0],
|
|
2304
|
-
open: bar.o,
|
|
2305
|
-
high: bar.h,
|
|
2306
|
-
low: bar.l,
|
|
2307
|
-
close: bar.c,
|
|
2308
|
-
timestamp: bar.t,
|
|
2309
|
-
};
|
|
2310
|
-
});
|
|
2311
|
-
};
|
|
2312
1385
|
|
|
2313
1386
|
function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
2314
1387
|
if (typeof state === "function" ? receiver !== state || true : !state.has(receiver))
|
|
@@ -11061,428 +10134,6 @@ class PerformanceTimer {
|
|
|
11061
10134
|
}
|
|
11062
10135
|
}
|
|
11063
10136
|
|
|
11064
|
-
/**
|
|
11065
|
-
* Calculates Bollinger Bands for a given set of price data.
|
|
11066
|
-
* Bollinger Bands consist of a middle band (SMA) and two outer bands
|
|
11067
|
-
* that are standard deviations away from the middle band.
|
|
11068
|
-
*
|
|
11069
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
11070
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11071
|
-
* @param params.period - The number of periods to use for the SMA (default is 20).
|
|
11072
|
-
* @param params.standardDeviations - The number of standard deviations for the outer bands (default is 2).
|
|
11073
|
-
* @returns An array of BollingerBandsData objects containing the calculated bands.
|
|
11074
|
-
*/
|
|
11075
|
-
function calculateBollingerBands(priceData, { period = 20, standardDeviations = 2 } = {}) {
|
|
11076
|
-
if (priceData.length < period) {
|
|
11077
|
-
logIfDebug(`Insufficient data for Bollinger Bands calculation: required periods: ${period}, but only received ${priceData.length} periods of data`);
|
|
11078
|
-
return [];
|
|
11079
|
-
}
|
|
11080
|
-
const result = [];
|
|
11081
|
-
for (let i = period - 1; i < priceData.length; i++) {
|
|
11082
|
-
const periodSlice = priceData.slice(i - period + 1, i + 1);
|
|
11083
|
-
const prices = periodSlice.map((d) => d.close);
|
|
11084
|
-
// Calculate middle band (SMA)
|
|
11085
|
-
const sum = prices.reduce((acc, price) => acc + price, 0);
|
|
11086
|
-
const sma = sum / period;
|
|
11087
|
-
// Calculate standard deviation
|
|
11088
|
-
const squaredDifferences = prices.map((price) => Math.pow(price - sma, 2));
|
|
11089
|
-
const variance = squaredDifferences.reduce((acc, val) => acc + val, 0) / period;
|
|
11090
|
-
const standardDeviation = Math.sqrt(variance);
|
|
11091
|
-
// Calculate bands
|
|
11092
|
-
const upperBand = sma + standardDeviation * standardDeviations;
|
|
11093
|
-
const lowerBand = sma - standardDeviation * standardDeviations;
|
|
11094
|
-
result.push({
|
|
11095
|
-
date: priceData[i].date,
|
|
11096
|
-
middle: parseFloat(sma.toFixed(2)),
|
|
11097
|
-
upper: parseFloat(upperBand.toFixed(2)),
|
|
11098
|
-
lower: parseFloat(lowerBand.toFixed(2)),
|
|
11099
|
-
close: priceData[i].close,
|
|
11100
|
-
});
|
|
11101
|
-
}
|
|
11102
|
-
// logIfDebug(`Calculated Bollinger Bands for ${result.length} periods`);
|
|
11103
|
-
return result;
|
|
11104
|
-
}
|
|
11105
|
-
/**
|
|
11106
|
-
* Calculates the Exponential Moving Average (EMA) for a given set of price data.
|
|
11107
|
-
* The EMA gives more weight to recent prices, making it more responsive to new information.
|
|
11108
|
-
*
|
|
11109
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
11110
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11111
|
-
* @param params.period - The number of periods to use for the EMA (default is 20).
|
|
11112
|
-
* @param params.period2 - An optional second period for a second EMA (default is 9).
|
|
11113
|
-
* @returns An array of EMAData objects containing the calculated EMA values.
|
|
11114
|
-
*/
|
|
11115
|
-
function calculateEMA(priceData, { period = 20, period2 = 9 } = {}) {
|
|
11116
|
-
if (priceData.length < period || (period2 && priceData.length < period2)) {
|
|
11117
|
-
logIfDebug(`Insufficient data for EMA calculation: required periods: ${period}, ${period2}, but only received ${priceData.length} periods of data`);
|
|
11118
|
-
return [];
|
|
11119
|
-
}
|
|
11120
|
-
const result = [];
|
|
11121
|
-
const multiplier = 2 / (period + 1);
|
|
11122
|
-
const multiplier2 = period2 ? 2 / (period2 + 1) : 0;
|
|
11123
|
-
// Calculate initial SMA for first period
|
|
11124
|
-
let sum = 0;
|
|
11125
|
-
for (let i = 0; i < period; i++) {
|
|
11126
|
-
sum += priceData[i].close;
|
|
11127
|
-
}
|
|
11128
|
-
let prevEMA = sum / period;
|
|
11129
|
-
// Calculate initial SMA for second period if needed
|
|
11130
|
-
let prevEMA2;
|
|
11131
|
-
if (period2) {
|
|
11132
|
-
sum = 0;
|
|
11133
|
-
for (let i = 0; i < period2; i++) {
|
|
11134
|
-
sum += priceData[i].close;
|
|
11135
|
-
}
|
|
11136
|
-
prevEMA2 = sum / period2;
|
|
11137
|
-
}
|
|
11138
|
-
// Add first EMA(s)
|
|
11139
|
-
const firstEntry = {
|
|
11140
|
-
date: priceData[Math.max(period, period2 || 0) - 1].date,
|
|
11141
|
-
ema: parseFloat(prevEMA.toFixed(2)),
|
|
11142
|
-
close: priceData[Math.max(period, period2 || 0) - 1].close,
|
|
11143
|
-
};
|
|
11144
|
-
if (period2) {
|
|
11145
|
-
firstEntry.ema2 = parseFloat(prevEMA2.toFixed(2));
|
|
11146
|
-
}
|
|
11147
|
-
result.push(firstEntry);
|
|
11148
|
-
// Calculate EMA for remaining periods
|
|
11149
|
-
for (let i = Math.max(period, period2 || 0); i < priceData.length; i++) {
|
|
11150
|
-
const currentClose = priceData[i].close;
|
|
11151
|
-
const currentEMA = (currentClose - prevEMA) * multiplier + prevEMA;
|
|
11152
|
-
prevEMA = currentEMA;
|
|
11153
|
-
const entry = {
|
|
11154
|
-
date: priceData[i].date,
|
|
11155
|
-
ema: parseFloat(currentEMA.toFixed(2)),
|
|
11156
|
-
close: currentClose,
|
|
11157
|
-
};
|
|
11158
|
-
if (period2) {
|
|
11159
|
-
const currentEMA2 = (currentClose - prevEMA2) * multiplier2 + prevEMA2;
|
|
11160
|
-
prevEMA2 = currentEMA2;
|
|
11161
|
-
entry.ema2 = parseFloat(currentEMA2.toFixed(2));
|
|
11162
|
-
}
|
|
11163
|
-
result.push(entry);
|
|
11164
|
-
}
|
|
11165
|
-
// logIfDebug(`Calculated EMA for ${result.length} periods`);
|
|
11166
|
-
return result;
|
|
11167
|
-
}
|
|
11168
|
-
/**
|
|
11169
|
-
* Calculates Fibonacci retracement and extension levels based on price data.
|
|
11170
|
-
* Fibonacci levels are used to identify potential support and resistance levels.
|
|
11171
|
-
*
|
|
11172
|
-
* @param priceData - An array of price data objects containing high and low prices.
|
|
11173
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11174
|
-
* @param params.lookbackPeriod - The number of periods to look back for swing high/low (default is 20).
|
|
11175
|
-
* @param params.retracementLevels - An array of retracement levels to calculate (default is [0.236, 0.382, 0.5, 0.618, 0.786]).
|
|
11176
|
-
* @param params.extensionLevels - An array of extension levels to calculate (default is [1.272, 1.618, 2.618]).
|
|
11177
|
-
* @param params.reverseDirection - A boolean indicating if the trend is reversed (default is false).
|
|
11178
|
-
* @returns An array of FibonacciData objects containing the calculated levels.
|
|
11179
|
-
*/
|
|
11180
|
-
function calculateFibonacciLevels(priceData, { lookbackPeriod = 20, retracementLevels = [0.236, 0.382, 0.5, 0.618, 0.786], extensionLevels = [1.272, 1.618, 2.618], reverseDirection = false, } = {}) {
|
|
11181
|
-
const result = [];
|
|
11182
|
-
for (let i = 0; i < priceData.length; i++) {
|
|
11183
|
-
const periodSlice = priceData.slice(Math.max(0, i - lookbackPeriod + 1), i + 1);
|
|
11184
|
-
const swingHigh = Math.max(...periodSlice.map((d) => d.high));
|
|
11185
|
-
const swingLow = Math.min(...periodSlice.map((d) => d.low));
|
|
11186
|
-
const priceRange = swingHigh - swingLow;
|
|
11187
|
-
const trend = reverseDirection ? 'downtrend' : 'uptrend';
|
|
11188
|
-
let levels = [];
|
|
11189
|
-
if (priceRange > 0) {
|
|
11190
|
-
// Calculate retracement levels
|
|
11191
|
-
retracementLevels.forEach((level) => {
|
|
11192
|
-
const price = reverseDirection ? swingLow + priceRange * level : swingHigh - priceRange * level;
|
|
11193
|
-
levels.push({
|
|
11194
|
-
level,
|
|
11195
|
-
price: parseFloat(price.toFixed(2)),
|
|
11196
|
-
type: 'retracement',
|
|
11197
|
-
});
|
|
11198
|
-
});
|
|
11199
|
-
// Calculate extension levels
|
|
11200
|
-
extensionLevels.forEach((level) => {
|
|
11201
|
-
const price = reverseDirection
|
|
11202
|
-
? swingHigh - priceRange * (level - 1) // For downtrend
|
|
11203
|
-
: swingHigh + priceRange * (level - 1); // For uptrend
|
|
11204
|
-
levels.push({
|
|
11205
|
-
level,
|
|
11206
|
-
price: parseFloat(price.toFixed(2)),
|
|
11207
|
-
type: 'extension',
|
|
11208
|
-
});
|
|
11209
|
-
});
|
|
11210
|
-
// Sort levels by price
|
|
11211
|
-
levels.sort((a, b) => (reverseDirection ? b.price - a.price : a.price - b.price));
|
|
11212
|
-
}
|
|
11213
|
-
else {
|
|
11214
|
-
logIfDebug(`Price range is zero on date ${priceData[i].date}; no levels calculated.`);
|
|
11215
|
-
}
|
|
11216
|
-
result.push({
|
|
11217
|
-
date: priceData[i].date,
|
|
11218
|
-
levels,
|
|
11219
|
-
swingHigh,
|
|
11220
|
-
swingLow,
|
|
11221
|
-
trend,
|
|
11222
|
-
close: priceData[i].close,
|
|
11223
|
-
});
|
|
11224
|
-
}
|
|
11225
|
-
// logIfDebug(`Calculated Fibonacci levels for ${result.length} periods`);
|
|
11226
|
-
return result;
|
|
11227
|
-
}
|
|
11228
|
-
/**
|
|
11229
|
-
* Calculates the Moving Average Convergence Divergence (MACD) for a given set of price data.
|
|
11230
|
-
* MACD is a trend-following momentum indicator that shows the relationship between two EMAs.
|
|
11231
|
-
*
|
|
11232
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
11233
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11234
|
-
* @param params.shortPeriod - The short EMA period (default is 12).
|
|
11235
|
-
* @param params.longPeriod - The long EMA period (default is 26).
|
|
11236
|
-
* @param params.signalPeriod - The signal line period (default is 9).
|
|
11237
|
-
* @returns An array of MACDData objects containing the calculated MACD values.
|
|
11238
|
-
*/
|
|
11239
|
-
function calculateMACD(priceData, { shortPeriod = 12, longPeriod = 26, signalPeriod = 9 } = {}) {
|
|
11240
|
-
if (priceData.length < longPeriod + signalPeriod) {
|
|
11241
|
-
logIfDebug(`Insufficient data for MACD calculation: required periods: ${longPeriod + signalPeriod}, but only received ${priceData.length} periods of data`);
|
|
11242
|
-
return [];
|
|
11243
|
-
}
|
|
11244
|
-
const emaShort = calculateEMA(priceData, { period: shortPeriod });
|
|
11245
|
-
const emaLong = calculateEMA(priceData, { period: longPeriod });
|
|
11246
|
-
// Align EMAs by trimming the beginning of emaShort to match emaLong length
|
|
11247
|
-
if (emaShort.length < emaLong.length) {
|
|
11248
|
-
logIfDebug('Short EMA length is less than Long EMA length for MACD calculation');
|
|
11249
|
-
return [];
|
|
11250
|
-
}
|
|
11251
|
-
const emaShortAligned = emaShort.slice(emaShort.length - emaLong.length);
|
|
11252
|
-
const macdLine = emaShortAligned.map((short, i) => short.ema - emaLong[i].ema);
|
|
11253
|
-
const result = [];
|
|
11254
|
-
if (macdLine.length < signalPeriod) {
|
|
11255
|
-
logIfDebug(`Insufficient MACD data for Signal Line calculation: required periods: ${signalPeriod}, but only received ${macdLine.length} periods of data`);
|
|
11256
|
-
return [];
|
|
11257
|
-
}
|
|
11258
|
-
const signalMultiplier = 2 / (signalPeriod + 1);
|
|
11259
|
-
let signalEMA = macdLine.slice(0, signalPeriod).reduce((sum, val) => sum + val, 0) / signalPeriod;
|
|
11260
|
-
for (let i = signalPeriod; i < macdLine.length; i++) {
|
|
11261
|
-
const macdValue = macdLine[i];
|
|
11262
|
-
signalEMA = (macdValue - signalEMA) * signalMultiplier + signalEMA;
|
|
11263
|
-
const hist = macdValue - signalEMA;
|
|
11264
|
-
result.push({
|
|
11265
|
-
date: emaLong[i].date, // Use emaLong's date for alignment
|
|
11266
|
-
macd: parseFloat(macdValue.toFixed(2)),
|
|
11267
|
-
signal: parseFloat(signalEMA.toFixed(2)),
|
|
11268
|
-
histogram: parseFloat(hist.toFixed(2)),
|
|
11269
|
-
close: emaLong[i].close,
|
|
11270
|
-
});
|
|
11271
|
-
}
|
|
11272
|
-
// logIfDebug(`Calculated MACD for ${result.length} periods`);
|
|
11273
|
-
return result;
|
|
11274
|
-
}
|
|
11275
|
-
/**
|
|
11276
|
-
* Calculates the Relative Strength Index (RSI) for a given set of price data.
|
|
11277
|
-
* RSI is a momentum oscillator that measures the speed and change of price movements.
|
|
11278
|
-
*
|
|
11279
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
11280
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11281
|
-
* @param params.period - The number of periods to use for the RSI (default is 14).
|
|
11282
|
-
* @returns An array of RSIData objects containing the calculated RSI values.
|
|
11283
|
-
*/
|
|
11284
|
-
function calculateRSI(priceData, { period = 14 } = {}) {
|
|
11285
|
-
if (priceData.length < period + 1) {
|
|
11286
|
-
logIfDebug(`Insufficient data for RSI calculation: required periods: ${period + 1}, but only received ${priceData.length} periods of data`);
|
|
11287
|
-
return [];
|
|
11288
|
-
}
|
|
11289
|
-
const result = [];
|
|
11290
|
-
let avgGain = 0;
|
|
11291
|
-
let avgLoss = 0;
|
|
11292
|
-
// Calculate first average gain and loss
|
|
11293
|
-
for (let i = 1; i <= period; i++) {
|
|
11294
|
-
const change = priceData[i].close - priceData[i - 1].close;
|
|
11295
|
-
if (change >= 0) {
|
|
11296
|
-
avgGain += change;
|
|
11297
|
-
}
|
|
11298
|
-
else {
|
|
11299
|
-
avgLoss += Math.abs(change);
|
|
11300
|
-
}
|
|
11301
|
-
}
|
|
11302
|
-
avgGain = avgGain / period;
|
|
11303
|
-
avgLoss = avgLoss / period;
|
|
11304
|
-
// Calculate RSI for the first period
|
|
11305
|
-
let rs = avgGain / avgLoss;
|
|
11306
|
-
let rsi = 100 - 100 / (1 + rs);
|
|
11307
|
-
result.push({
|
|
11308
|
-
date: priceData[period].date,
|
|
11309
|
-
rsi: parseFloat(rsi.toFixed(2)),
|
|
11310
|
-
close: priceData[period].close,
|
|
11311
|
-
});
|
|
11312
|
-
// Calculate subsequent periods using smoothed averages
|
|
11313
|
-
for (let i = period + 1; i < priceData.length; i++) {
|
|
11314
|
-
const change = priceData[i].close - priceData[i - 1].close;
|
|
11315
|
-
const gain = change >= 0 ? change : 0;
|
|
11316
|
-
const loss = change < 0 ? Math.abs(change) : 0;
|
|
11317
|
-
// Use smoothed averages
|
|
11318
|
-
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
11319
|
-
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
11320
|
-
rs = avgGain / avgLoss;
|
|
11321
|
-
rsi = 100 - 100 / (1 + rs);
|
|
11322
|
-
result.push({
|
|
11323
|
-
date: priceData[i].date,
|
|
11324
|
-
rsi: parseFloat(rsi.toFixed(2)),
|
|
11325
|
-
close: priceData[i].close,
|
|
11326
|
-
});
|
|
11327
|
-
}
|
|
11328
|
-
// logIfDebug(`Calculated RSI for ${result.length} periods`);
|
|
11329
|
-
return result;
|
|
11330
|
-
}
|
|
11331
|
-
/**
|
|
11332
|
-
* Calculates the Stochastic Oscillator for a given set of price data.
|
|
11333
|
-
* The Stochastic Oscillator compares a particular closing price of a security to a range of its prices over a certain period of time.
|
|
11334
|
-
*
|
|
11335
|
-
* @param priceData - An array of price data objects containing high, low, and closing prices.
|
|
11336
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11337
|
-
* @param params.lookbackPeriod - The number of periods to look back for the calculation of %K (default is 5).
|
|
11338
|
-
* @param params.signalPeriod - The number of periods for the %D signal line (default is 3).
|
|
11339
|
-
* @param params.smoothingFactor - The smoothing factor for %K (default is 3).
|
|
11340
|
-
* @returns An array of StochData objects containing the calculated %K and %D values.
|
|
11341
|
-
*/
|
|
11342
|
-
function calculateStochasticOscillator(priceData, { lookbackPeriod = 5, signalPeriod = 3, smoothingFactor = 3 } = {}) {
|
|
11343
|
-
if (priceData.length < lookbackPeriod) {
|
|
11344
|
-
logIfDebug(`Insufficient data for Stochastic Oscillator calculation: required periods: ${lookbackPeriod}, but only received ${priceData.length} periods of data`);
|
|
11345
|
-
return [];
|
|
11346
|
-
}
|
|
11347
|
-
const kValues = [];
|
|
11348
|
-
const result = [];
|
|
11349
|
-
let kSum = 0;
|
|
11350
|
-
let dSum = 0;
|
|
11351
|
-
for (let i = lookbackPeriod - 1; i < priceData.length; i++) {
|
|
11352
|
-
const periodSlice = priceData.slice(i - lookbackPeriod + 1, i + 1);
|
|
11353
|
-
const currentClose = periodSlice[periodSlice.length - 1].close;
|
|
11354
|
-
const highPrices = periodSlice.map((d) => d.high);
|
|
11355
|
-
const lowPrices = periodSlice.map((d) => d.low);
|
|
11356
|
-
const highestHigh = Math.max(...highPrices);
|
|
11357
|
-
const lowestLow = Math.min(...lowPrices);
|
|
11358
|
-
const k = highestHigh === lowestLow ? 0 : ((currentClose - lowestLow) / (highestHigh - lowestLow)) * 100;
|
|
11359
|
-
kValues.push(k);
|
|
11360
|
-
kSum += k;
|
|
11361
|
-
if (kValues.length > smoothingFactor)
|
|
11362
|
-
kSum -= kValues[kValues.length - smoothingFactor - 1];
|
|
11363
|
-
const smoothedK = kSum / Math.min(kValues.length, smoothingFactor);
|
|
11364
|
-
dSum += smoothedK;
|
|
11365
|
-
if (kValues.length > smoothingFactor + signalPeriod - 1)
|
|
11366
|
-
dSum -= kValues[kValues.length - smoothingFactor - signalPeriod];
|
|
11367
|
-
const smoothedD = dSum / Math.min(kValues.length - smoothingFactor + 1, signalPeriod);
|
|
11368
|
-
if (kValues.length >= smoothingFactor + signalPeriod - 1) {
|
|
11369
|
-
result.push({
|
|
11370
|
-
date: priceData[i].date,
|
|
11371
|
-
slowK: parseFloat(smoothedK.toFixed(2)),
|
|
11372
|
-
slowD: parseFloat(smoothedD.toFixed(2)),
|
|
11373
|
-
close: currentClose,
|
|
11374
|
-
});
|
|
11375
|
-
}
|
|
11376
|
-
}
|
|
11377
|
-
// logIfDebug(`Calculated Stochastic Oscillator for ${result.length} periods`);
|
|
11378
|
-
return result;
|
|
11379
|
-
}
|
|
11380
|
-
/**
|
|
11381
|
-
* Calculates support and resistance levels based on price data.
|
|
11382
|
-
* Support and resistance levels are price levels at which a stock tends to stop and reverse.
|
|
11383
|
-
*
|
|
11384
|
-
* @param priceData - An array of price data objects containing high, low, and closing prices.
|
|
11385
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11386
|
-
* @param params.maxLevels - The maximum number of support/resistance levels to return (default is 5).
|
|
11387
|
-
* @param params.lookbackPeriod - The number of periods to look back for pivot points (default is 10).
|
|
11388
|
-
* @returns An array of SupportResistanceData objects containing the calculated levels.
|
|
11389
|
-
*/
|
|
11390
|
-
function calculateSupportAndResistance(priceData, { maxLevels = 5, lookbackPeriod = 10 } = {}) {
|
|
11391
|
-
const result = [];
|
|
11392
|
-
for (let i = 0; i < priceData.length; i++) {
|
|
11393
|
-
const startIdx = Math.max(0, i - lookbackPeriod);
|
|
11394
|
-
const analysisWindow = priceData.slice(startIdx, i + 1);
|
|
11395
|
-
const pivotPoints = [];
|
|
11396
|
-
// **Compute Volatility Metrics**
|
|
11397
|
-
const priceChanges = analysisWindow.slice(1).map((bar, idx) => Math.abs(bar.close - analysisWindow[idx].close));
|
|
11398
|
-
const avgPriceChange = priceChanges.reduce((sum, change) => sum + change, 0) / priceChanges.length;
|
|
11399
|
-
const volatility = avgPriceChange / analysisWindow[0].close; // Relative volatility
|
|
11400
|
-
// **Adjust Sensitivity and minGapBetweenLevels Dynamically**
|
|
11401
|
-
const sensitivity = volatility * 2; // Adjust the multiplier as needed
|
|
11402
|
-
const minGapBetweenLevels = volatility * 100; // Convert to percentage
|
|
11403
|
-
// Analyze each point in window for pivot status
|
|
11404
|
-
for (let j = 1; j < analysisWindow.length - 1; j++) {
|
|
11405
|
-
const curr = analysisWindow[j];
|
|
11406
|
-
const prevBar = analysisWindow[j - 1];
|
|
11407
|
-
const nextBar = analysisWindow[j + 1];
|
|
11408
|
-
// Check for high pivot
|
|
11409
|
-
if (curr.high > prevBar.high && curr.high > nextBar.high) {
|
|
11410
|
-
const existingPivot = pivotPoints.find((p) => Math.abs(p.price - curr.high) / curr.high < sensitivity);
|
|
11411
|
-
if (existingPivot) {
|
|
11412
|
-
existingPivot.count++;
|
|
11413
|
-
existingPivot.volume += curr.vol; // **Include Volume**
|
|
11414
|
-
}
|
|
11415
|
-
else {
|
|
11416
|
-
pivotPoints.push({ price: curr.high, count: 1, volume: curr.vol });
|
|
11417
|
-
}
|
|
11418
|
-
}
|
|
11419
|
-
// Check for low pivot
|
|
11420
|
-
if (curr.low < prevBar.low && curr.low < nextBar.low) {
|
|
11421
|
-
const existingPivot = pivotPoints.find((p) => Math.abs(p.price - curr.low) / curr.low < sensitivity);
|
|
11422
|
-
if (existingPivot) {
|
|
11423
|
-
existingPivot.count++;
|
|
11424
|
-
existingPivot.volume += curr.vol; // **Include Volume**
|
|
11425
|
-
}
|
|
11426
|
-
else {
|
|
11427
|
-
pivotPoints.push({ price: curr.low, count: 1, volume: curr.vol });
|
|
11428
|
-
}
|
|
11429
|
-
}
|
|
11430
|
-
}
|
|
11431
|
-
// Group nearby levels
|
|
11432
|
-
const currentPrice = priceData[i].close;
|
|
11433
|
-
const levels = [];
|
|
11434
|
-
// Sort pivots by price
|
|
11435
|
-
pivotPoints.sort((a, b) => a.price - b.price);
|
|
11436
|
-
// Group close pivots
|
|
11437
|
-
let currentGroup = [];
|
|
11438
|
-
for (let j = 0; j < pivotPoints.length; j++) {
|
|
11439
|
-
if (currentGroup.length === 0) {
|
|
11440
|
-
currentGroup.push(pivotPoints[j]);
|
|
11441
|
-
}
|
|
11442
|
-
else {
|
|
11443
|
-
const lastPrice = currentGroup[currentGroup.length - 1].price;
|
|
11444
|
-
if ((Math.abs(pivotPoints[j].price - lastPrice) / lastPrice) * 100 <= minGapBetweenLevels) {
|
|
11445
|
-
currentGroup.push(pivotPoints[j]);
|
|
11446
|
-
}
|
|
11447
|
-
else {
|
|
11448
|
-
// Process current group
|
|
11449
|
-
if (currentGroup.length > 0) {
|
|
11450
|
-
const totalVolume = currentGroup.reduce((sum, p) => sum + p.volume, 0);
|
|
11451
|
-
const avgPrice = currentGroup.reduce((sum, p) => sum + p.price * p.volume, 0) / totalVolume;
|
|
11452
|
-
const totalStrength = currentGroup.reduce((sum, p) => sum + p.count * (p.volume / totalVolume), 0);
|
|
11453
|
-
levels.push({
|
|
11454
|
-
price: parseFloat(avgPrice.toFixed(2)),
|
|
11455
|
-
strength: parseFloat(totalStrength.toFixed(2)),
|
|
11456
|
-
type: avgPrice > currentPrice ? 'resistance' : 'support',
|
|
11457
|
-
});
|
|
11458
|
-
}
|
|
11459
|
-
currentGroup = [pivotPoints[j]];
|
|
11460
|
-
}
|
|
11461
|
-
}
|
|
11462
|
-
}
|
|
11463
|
-
// Process final group
|
|
11464
|
-
if (currentGroup.length > 0) {
|
|
11465
|
-
const totalVolume = currentGroup.reduce((sum, p) => sum + p.volume, 0);
|
|
11466
|
-
const avgPrice = currentGroup.reduce((sum, p) => sum + p.price * p.volume, 0) / totalVolume;
|
|
11467
|
-
const totalStrength = currentGroup.reduce((sum, p) => sum + p.count * (p.volume / totalVolume), 0);
|
|
11468
|
-
levels.push({
|
|
11469
|
-
price: parseFloat(avgPrice.toFixed(2)),
|
|
11470
|
-
strength: parseFloat(totalStrength.toFixed(2)),
|
|
11471
|
-
type: avgPrice > currentPrice ? 'resistance' : 'support',
|
|
11472
|
-
});
|
|
11473
|
-
}
|
|
11474
|
-
// Sort by strength and limit
|
|
11475
|
-
const finalLevels = levels.sort((a, b) => b.strength - a.strength).slice(0, maxLevels);
|
|
11476
|
-
result.push({
|
|
11477
|
-
date: priceData[i].date,
|
|
11478
|
-
levels: finalLevels,
|
|
11479
|
-
close: currentPrice,
|
|
11480
|
-
});
|
|
11481
|
-
}
|
|
11482
|
-
logIfDebug(`Found ${result.reduce((sum, r) => sum + r.levels.length, 0)} support/resistance levels across ${result.length} periods`);
|
|
11483
|
-
return result;
|
|
11484
|
-
}
|
|
11485
|
-
|
|
11486
10137
|
function getDefaultExportFromCjs (x) {
|
|
11487
10138
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
11488
10139
|
}
|
|
@@ -19888,40 +18539,12 @@ const disco = {
|
|
|
19888
18539
|
pct: formatPercentage,
|
|
19889
18540
|
dateTimeForGS: dateTimeForGS,
|
|
19890
18541
|
},
|
|
19891
|
-
indices: {
|
|
19892
|
-
fetchAggregates: fetchIndicesAggregates,
|
|
19893
|
-
fetchPreviousClose: fetchIndicesPreviousClose,
|
|
19894
|
-
fetchDailyOpenClose: fetchIndicesDailyOpenClose,
|
|
19895
|
-
fetchSnapshot: fetchIndicesSnapshot,
|
|
19896
|
-
fetchUniversalSnapshot: fetchUniversalSnapshot,
|
|
19897
|
-
formatBarData: formatIndicesBarData,
|
|
19898
|
-
},
|
|
19899
18542
|
llm: {
|
|
19900
18543
|
call: makeLLMCall,
|
|
19901
18544
|
seek: makeDeepseekCall,
|
|
19902
18545
|
images: makeImagesCall,
|
|
19903
18546
|
open: makeOpenRouterCall,
|
|
19904
18547
|
},
|
|
19905
|
-
polygon: {
|
|
19906
|
-
fetchTickerInfo: fetchTickerInfo,
|
|
19907
|
-
fetchGroupedDaily: fetchGroupedDaily,
|
|
19908
|
-
fetchLastTrade: fetchLastTrade,
|
|
19909
|
-
fetchTrades: fetchTrades,
|
|
19910
|
-
fetchPrices: fetchPrices,
|
|
19911
|
-
analysePolygonPriceData: analysePolygonPriceData,
|
|
19912
|
-
formatPriceData: formatPriceData,
|
|
19913
|
-
fetchDailyOpenClose: fetchDailyOpenClose,
|
|
19914
|
-
getPreviousClose: getPreviousClose,
|
|
19915
|
-
},
|
|
19916
|
-
ta: {
|
|
19917
|
-
calculateEMA: calculateEMA,
|
|
19918
|
-
calculateMACD: calculateMACD,
|
|
19919
|
-
calculateRSI: calculateRSI,
|
|
19920
|
-
calculateStochasticOscillator: calculateStochasticOscillator,
|
|
19921
|
-
calculateBollingerBands: calculateBollingerBands,
|
|
19922
|
-
calculateSupportAndResistance: calculateSupportAndResistance,
|
|
19923
|
-
calculateFibonacciLevels: calculateFibonacciLevels,
|
|
19924
|
-
},
|
|
19925
18548
|
time: {
|
|
19926
18549
|
convertDateToMarketTimeZone: convertDateToMarketTimeZone,
|
|
19927
18550
|
getStartAndEndDates: getStartAndEndDates,
|
|
@@ -19944,7 +18567,6 @@ const disco = {
|
|
|
19944
18567
|
utils: {
|
|
19945
18568
|
logIfDebug: logIfDebug,
|
|
19946
18569
|
fetchWithRetry: fetchWithRetry,
|
|
19947
|
-
validatePolygonApiKey: validatePolygonApiKey,
|
|
19948
18570
|
Timer: PerformanceTimer,
|
|
19949
18571
|
},
|
|
19950
18572
|
};
|