From 144479b89cefbabc838aac02d449be7c4ce2c828 Mon Sep 17 00:00:00 2001 From: Oguzhan Date: Mon, 27 Oct 2025 02:17:45 +0300 Subject: [PATCH 01/11] add: max redirect 5 limit for infinity loops --- src/miscRequests.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/miscRequests.js b/src/miscRequests.js index cfb83340..147ecd9d 100644 --- a/src/miscRequests.js +++ b/src/miscRequests.js @@ -427,7 +427,11 @@ module.exports = { * @param {string} [location] Auth page location (For france: https://fr.tradingview.com/) * @returns {Promise} Token */ - async getUser(session, signature = '', location = 'https://www.tradingview.com/') { + async getUser(session, signature = '', location = 'https://www.tradingview.com/', redirectCount = 0) { + if (redirectCount > 5) { + throw new Error('Too many redirects - invalid session/signature'); + } + const { data, headers } = await axios.get(location, { headers: { cookie: genAuthCookies(session, signature), @@ -459,7 +463,7 @@ module.exports = { } if (headers.location !== location) { - return this.getUser(session, signature, headers.location); + return this.getUser(session, signature, headers.location, redirectCount + 1); } throw new Error('Wrong or expired sessionid/signature'); From 8215fb90c9e764a4d8f9e29a1cdf740bd45ef994 Mon Sep 17 00:00:00 2001 From: Oguzhan Date: Mon, 27 Oct 2025 02:18:39 +0300 Subject: [PATCH 02/11] add: more headers for waf bypass --- src/client.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index c38aa6a3..d32ccee8 100644 --- a/src/client.js +++ b/src/client.js @@ -158,6 +158,7 @@ module.exports = class Client { if (!this.isOpen) return; protocol.parseWSPacket(str).forEach((packet) => { + console.dir(packet, { depth: null }); if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet); if (typeof packet === 'number') { // Ping this.#ws.send(protocol.formatWSPacket(`~h~${packet}`)); @@ -228,8 +229,14 @@ module.exports = class Client { if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG; const server = clientOptions.server || 'data'; - this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?type=chart`, { + this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?from=chart&type=chart`, { origin: 'https://www.tradingview.com', + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + }, }); if (clientOptions.token) { From 9205c6548fbc293874786aeac15ebc3f41357a89 Mon Sep 17 00:00:00 2001 From: Oguzhan Date: Mon, 27 Oct 2025 02:20:58 +0300 Subject: [PATCH 03/11] remove: console dir --- src/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.js b/src/client.js index d32ccee8..feb41e20 100644 --- a/src/client.js +++ b/src/client.js @@ -158,7 +158,6 @@ module.exports = class Client { if (!this.isOpen) return; protocol.parseWSPacket(str).forEach((packet) => { - console.dir(packet, { depth: null }); if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet); if (typeof packet === 'number') { // Ping this.#ws.send(protocol.formatWSPacket(`~h~${packet}`)); From 6f1e0762f24cdab1270c0c03fbfb4d28c21caf7f Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sat, 11 Apr 2026 18:34:50 +0900 Subject: [PATCH 04/11] Update redirect error message and add unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change error message to "possible WAF or geo-restriction" since redirect loops are caused by WAF/geo-blocking, not invalid credentials - Add unit test verifying redirect loop is capped at ≤10 calls Co-Authored-By: Claude Opus 4.6 (1M context) --- src/miscRequests.js | 2 +- tests/getUser-redirect.test.ts | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/getUser-redirect.test.ts diff --git a/src/miscRequests.js b/src/miscRequests.js index ea2c5baa..a9707f2d 100644 --- a/src/miscRequests.js +++ b/src/miscRequests.js @@ -431,7 +431,7 @@ module.exports = { */ async getUser(session, signature = '', location = 'https://www.tradingview.com/', redirectCount = 0) { if (redirectCount > 5) { - throw new Error('Too many redirects - invalid session/signature'); + throw new Error('Too many redirects - possible WAF or geo-restriction'); } const { data, headers } = await axios.get(location, { diff --git a/tests/getUser-redirect.test.ts b/tests/getUser-redirect.test.ts new file mode 100644 index 00000000..4d8faa5c --- /dev/null +++ b/tests/getUser-redirect.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; + +describe('getUser redirect protection', () => { + it('should not loop infinitely on repeated redirects', async () => { + // Monkey-patch axios in the require cache to simulate redirect loop + let callCount = 0; + const axiosMock = { + get: async (url: string) => { + callCount++; + const isA = url === 'https://www.tradingview.com/'; + return { + data: 'no auth here', + headers: { + location: isA + ? 'https://www.tradingview.com/accounts/signin/' + : 'https://www.tradingview.com/', + }, + }; + }, + }; + + const axiosPath = require.resolve('axios'); + const originalModule = require.cache[axiosPath]; + require.cache[axiosPath] = { + id: axiosPath, + filename: axiosPath, + loaded: true, + exports: axiosMock, + } as any; + + const miscPath = require.resolve('../src/miscRequests'); + delete require.cache[miscPath]; + + try { + const misc = require('../src/miscRequests'); + await expect( + misc.getUser('fake_session', 'fake_signature'), + ).rejects.toThrow('Too many redirects'); + + expect(callCount).toBeGreaterThan(0); + expect(callCount).toBeLessThanOrEqual(10); + } finally { + if (originalModule) require.cache[axiosPath] = originalModule; + else delete require.cache[axiosPath]; + delete require.cache[miscPath]; + } + }, 5000); +}); From 2b94d348e6ed909fa07c87dedf3727a85e6360dc Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sat, 11 Apr 2026 18:36:22 +0900 Subject: [PATCH 05/11] Fix CodeFactor lint warnings in redirect test - Replace ++ with += 1 (no-plusplus) - Add eslint-disable for global-require (needed for cache patching) Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/getUser-redirect.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/getUser-redirect.test.ts b/tests/getUser-redirect.test.ts index 4d8faa5c..505b2cbe 100644 --- a/tests/getUser-redirect.test.ts +++ b/tests/getUser-redirect.test.ts @@ -6,7 +6,7 @@ describe('getUser redirect protection', () => { let callCount = 0; const axiosMock = { get: async (url: string) => { - callCount++; + callCount += 1; const isA = url === 'https://www.tradingview.com/'; return { data: 'no auth here', @@ -32,6 +32,7 @@ describe('getUser redirect protection', () => { delete require.cache[miscPath]; try { + // eslint-disable-next-line global-require const misc = require('../src/miscRequests'); await expect( misc.getUser('fake_session', 'fake_signature'), From a074b0ca09adf61d31bdcadeefb8b8d741ec3203 Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sat, 11 Apr 2026 19:07:17 +0900 Subject: [PATCH 06/11] Make WebSocket headers configurable with sensible defaults - Provide default browser-like headers (required by TradingView WAF) - Allow users to override via clientOptions.headers - Update User-Agent to Windows/Chrome 131 (more common, more recent) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index feb41e20..8eb014f0 100644 --- a/src/client.js +++ b/src/client.js @@ -218,6 +218,7 @@ module.exports = class Client { * @prop {boolean} [DEBUG] Enable debug mode * @prop {'data' | 'prodata' | 'widgetdata'} [server] Server type * @prop {string} [location] Auth page location (For france: https://fr.tradingview.com/) + * @prop {Object} [headers] Custom headers for the WebSocket connection (e.g. for WAF bypass) */ /** @@ -228,14 +229,16 @@ module.exports = class Client { if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG; const server = clientOptions.server || 'data'; + const defaultHeaders = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + }; + this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?from=chart&type=chart`, { origin: 'https://www.tradingview.com', - headers: { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - Pragma: 'no-cache', - }, + headers: { ...defaultHeaders, ...clientOptions.headers }, }); if (clientOptions.token) { From 9c353d621247de4efec7580417e7e2720e418d8e Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sat, 11 Apr 2026 19:08:55 +0900 Subject: [PATCH 07/11] Fix max-len lint warning in JSDoc Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 8eb014f0..7cb70a40 100644 --- a/src/client.js +++ b/src/client.js @@ -218,7 +218,7 @@ module.exports = class Client { * @prop {boolean} [DEBUG] Enable debug mode * @prop {'data' | 'prodata' | 'widgetdata'} [server] Server type * @prop {string} [location] Auth page location (For france: https://fr.tradingview.com/) - * @prop {Object} [headers] Custom headers for the WebSocket connection (e.g. for WAF bypass) + * @prop {Object} [headers] Custom WebSocket headers */ /** From de1d8669b5e82c348da01619d2169faf0e551789 Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sat, 11 Apr 2026 19:10:32 +0900 Subject: [PATCH 08/11] Disable max-len for User-Agent string Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.js b/src/client.js index 7cb70a40..718d15c7 100644 --- a/src/client.js +++ b/src/client.js @@ -230,6 +230,7 @@ module.exports = class Client { const server = clientOptions.server || 'data'; const defaultHeaders = { + // eslint-disable-next-line max-len 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'no-cache', From 90f8033974eaba039d435af4fb3e9f5dd4fe64c5 Mon Sep 17 00:00:00 2001 From: Nox Date: Mon, 22 Jun 2026 22:52:21 +0200 Subject: [PATCH 09/11] Fix TradingView indicator CI tests Authored-by: Nox --- .github/workflows/tests.yml | 2 +- src/chart/study.js | 17 +++++++++--- src/protocol.js | 54 +++++++++++++++++++++++++++++++++---- tests/indicators.test.ts | 8 +++--- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9582148e..c2d60096 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: npm ci - name: Run tests - run: npm test + run: npm test -- --run env: SESSION: ${{ secrets.TW_SESSION }} SIGNATURE: ${{ secrets.TW_SIGNATURE }} diff --git a/src/chart/study.js b/src/chart/study.js index 4735d818..4e5cfbfe 100644 --- a/src/chart/study.js +++ b/src/chart/study.js @@ -64,7 +64,7 @@ const parseTrades = (trades) => trades.reverse().map((t) => ({ * @prop {Object} exit Trade exit * @prop {'' | string} exit.name Trade name ('' if false exit) - * @prop {number} exit.value Exit price value + * @prop {number} exit.value Exit value * @prop {number} exit.time Exit timestamp * @prop {number} quantity Trade quantity @@ -92,8 +92,7 @@ const parseTrades = (trades) => trades.reverse().map((t) => ({ * @prop {number} grossProfitPercent Gross profit percent * @prop {number} largestLosTrade Largest losing trade gain * @prop {number} largestLosTradePercent Largent losing trade performance (percentage) - * @prop {number} largestWinTrade Largest winning trade gain - * @prop {number} largestWinTradePercent Largest winning trade performance (percentage) + * @prop {number} largestWinTrade Largest winning trade performance (percentage) * @prop {number} marginCalls Margin calls * @prop {number} maxContractsHeld Max Contracts Held * @prop {number} netProfit Net profit @@ -341,7 +340,17 @@ module.exports = (chartSession) => class ChartStudy { }; if (parsed.dataCompressed) { - updateStrategyReport((await parseCompressed(parsed.dataCompressed)).report); + try { + const compressedData = await parseCompressed(parsed.dataCompressed); + if (compressedData && compressedData.report) { + updateStrategyReport(compressedData.report); + } + } catch (error) { + this.#handleError( + 'Unable to parse compressed strategy report:', + error.message || error, + ); + } } if (parsed.data && parsed.data.report) updateStrategyReport(parsed.data.report); diff --git a/src/protocol.js b/src/protocol.js index 73f543d7..6ce42cd8 100644 --- a/src/protocol.js +++ b/src/protocol.js @@ -1,3 +1,4 @@ +const zlib = require('zlib'); const JSZip = require('jszip'); /** @@ -9,6 +10,41 @@ const JSZip = require('jszip'); const cleanerRgx = /~h~/g; const splitterRgx = /~m~[0-9]{1,}~m~/g; +/** + * Normalise base64 data received from TradingView. + * TradingView occasionally omits padding and may use URL-safe characters. + * @param {string} data Base64 payload + * @returns {string} Normalised base64 payload + */ +function normaliseBase64(data) { + const normalised = data.replace(/-/g, '+').replace(/_/g, '/'); + return normalised.padEnd(normalised.length + ((4 - (normalised.length % 4)) % 4), '='); +} + +/** + * Parse JSON from a decoded buffer, optionally trying common compression wrappers. + * @param {Buffer} buffer Decoded compressed payload + * @returns {Object | undefined} Parsed JSON when a format matches + */ +function parseDecodedCompressed(buffer) { + const readers = [ + () => buffer, + () => zlib.inflateSync(buffer), + () => zlib.inflateRawSync(buffer), + () => zlib.gunzipSync(buffer), + ]; + + for (const read of readers) { + try { + return JSON.parse(read().toString('utf8')); + } catch (error) { + // Try the next known TradingView payload format. + } + } + + return undefined; +} + module.exports = { /** * Parse websocket packet @@ -50,11 +86,19 @@ module.exports = { * @returns {Promise<{}>} Parsed data */ async parseCompressed(data) { + const normalised = normaliseBase64(data); const zip = new JSZip(); - return JSON.parse( - await ( - await zip.loadAsync(data, { base64: true }) - ).file('').async('text'), - ); + + try { + const archive = await zip.loadAsync(normalised, { base64: true }); + const file = archive.file('') || archive.file(/.*/)[0]; + if (!file) throw new Error('Compressed payload does not contain a file'); + return JSON.parse(await file.async('text')); + } catch (zipError) { + const decoded = Buffer.from(normalised, 'base64'); + const parsed = parseDecodedCompressed(decoded); + if (parsed) return parsed; + throw zipError; + } }, }; diff --git a/tests/indicators.test.ts b/tests/indicators.test.ts index f7a5ec91..3860744a 100644 --- a/tests/indicators.test.ts +++ b/tests/indicators.test.ts @@ -65,7 +65,7 @@ describe('Indicators', () => { expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); }); - it.skipIf(noAuth).concurrent('gets performance data from SuperTrend strategy', async () => { + it.skipIf(noAuth)('gets performance data from SuperTrend strategy', async () => { const SuperTrend = new chart.Study(indicators.SuperTrend); let QTY = 10; @@ -95,7 +95,7 @@ describe('Indicators', () => { }, }); - if (QTY >= 50) { + if (perfReport?.all?.totalTrades !== undefined && QTY >= 50) { resolve(true); return; } @@ -112,9 +112,9 @@ describe('Indicators', () => { expect(perfResult).toBe(true); SuperTrend.remove(); - }, 10000); + }, 30000); - it.skipIf(noAuth).concurrent('gets data from MarketCipher B study', async () => { + it.skipIf(noAuth)('gets data from MarketCipher B study', async () => { const CipherB = new chart.Study(indicators.CipherB); const lastResult: any = await new Promise((resolve) => { From b89a0d09f4c444ed5b1a6c486f468448c8679589 Mon Sep 17 00:00:00 2001 From: Nox Date: Mon, 22 Jun 2026 22:54:51 +0200 Subject: [PATCH 10/11] Restore study report JSDoc Authored-by: Nox --- src/chart/study.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/chart/study.js b/src/chart/study.js index 4e5cfbfe..d39d8c16 100644 --- a/src/chart/study.js +++ b/src/chart/study.js @@ -64,7 +64,7 @@ const parseTrades = (trades) => trades.reverse().map((t) => ({ * @prop {Object} exit Trade exit * @prop {'' | string} exit.name Trade name ('' if false exit) - * @prop {number} exit.value Exit value + * @prop {number} exit.value Exit price value * @prop {number} exit.time Exit timestamp * @prop {number} quantity Trade quantity @@ -92,7 +92,8 @@ const parseTrades = (trades) => trades.reverse().map((t) => ({ * @prop {number} grossProfitPercent Gross profit percent * @prop {number} largestLosTrade Largest losing trade gain * @prop {number} largestLosTradePercent Largent losing trade performance (percentage) - * @prop {number} largestWinTrade Largest winning trade performance (percentage) + * @prop {number} largestWinTrade Largest winning trade gain + * @prop {number} largestWinTradePercent Largest winning trade performance (percentage) * @prop {number} marginCalls Margin calls * @prop {number} maxContractsHeld Max Contracts Held * @prop {number} netProfit Net profit From d7c555379d2f7c7ce3ec1c3b0b4147d184788d2e Mon Sep 17 00:00:00 2001 From: Nox Date: Tue, 23 Jun 2026 00:40:52 +0200 Subject: [PATCH 11/11] Stabilize TradingView indicator CI tests Signed-off-by: Nox --- .github/workflows/tests.yml | 3 +++ tests/indicators.test.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2d60096..42df0864 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,9 @@ jobs: runs-on: ubuntu-latest strategy: + # TradingView authenticated tests use the same account/session secrets. + # Running every Node matrix job at once makes the external websocket tests flaky. + max-parallel: 1 matrix: node-version: [14.x, 18.x, 19.x] diff --git a/tests/indicators.test.ts b/tests/indicators.test.ts index 3860744a..d835a03e 100644 --- a/tests/indicators.test.ts +++ b/tests/indicators.test.ts @@ -119,7 +119,11 @@ describe('Indicators', () => { const lastResult: any = await new Promise((resolve) => { CipherB.onUpdate(() => { - resolve(CipherB.periods[0]); + const lastPeriod = CipherB.periods[0]; + + if (lastPeriod?.VWAP !== undefined && lastPeriod?.rsiMFI !== undefined) { + resolve(lastPeriod); + } }); }); @@ -135,7 +139,7 @@ describe('Indicators', () => { expect(lastResult.Buy_and_sell_circle).toBeTypeOf('number'); CipherB.remove(); - }); + }, 30000); it.skipIf(noAuth)('removes chart', () => { console.log('Closing the chart...');