├── .env.sample ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── README.md ├── docs └── DOCS.md ├── examples ├── AllPrivateIndicators.js ├── BuiltInIndicator.js ├── CustomChartType.js ├── CustomTimeframe.js ├── Errors.js ├── FakeReplayMode.js ├── FromToData.js ├── GetDrawings.js ├── GraphicIndicator.js ├── MultipleSyncFetch.js ├── PinePermManage.js ├── ReplayMode.js ├── Search.js ├── SimpleChart.js └── UserLogin.js ├── main.js ├── package-lock.json ├── package.json ├── src ├── chart │ ├── graphicParser.js │ ├── session.js │ └── study.js ├── classes │ ├── BuiltInIndicator.js │ ├── PineIndicator.js │ └── PinePermManager.js ├── client.js ├── miscRequests.js ├── protocol.js ├── quote │ ├── market.js │ └── session.js ├── types.js └── utils.js ├── tests ├── allErrors.test.ts ├── authenticated.test.ts ├── builtInIndicator.test.ts ├── customChartTypes.test.ts ├── indicators.test.ts ├── quoteSession.test.ts ├── replayMode.test.ts ├── search.test.ts ├── simpleChart.test.ts └── utils.ts └── vite.config.js /.env.sample: -------------------------------------------------------------------------------- 1 | SESSION=... # Your sessionid cookie 2 | SIGNATURE=... # Your signature cookie 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | parser: '@babel/eslint-parser', 11 | parserOptions: { 12 | ecmaVersion: 12, 13 | requireConfigFile: false, 14 | }, 15 | rules: { 16 | 'no-console': 'off', 17 | 'import/no-extraneous-dependencies': [ 18 | 'error', 19 | { 20 | devDependencies: ['./test.js', './tests/**'], 21 | }, 22 | ], 23 | 'no-restricted-syntax': 'off', 24 | 'no-await-in-loop': 'off', 25 | 'no-continue': 'off', 26 | 'guard-for-in': 'off', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Mathieu2301 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: Mathieu2301 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Code to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Environment:** 23 | - OS: [e.g. Windows 11] 24 | - Node version: [e.g. v16.3.0] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: Mathieu2301 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | schedule: 10 | - cron: '0 0 * * *' 11 | 12 | jobs: 13 | Tests: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 18.x, 19.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - name: Install dependencies 28 | run: npm ci 29 | - name: Run tests 30 | run: npm test 31 | env: 32 | SESSION: ${{ secrets.TW_SESSION }} 33 | SIGNATURE: ${{ secrets.TW_SIGNATURE }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .github/ 3 | tests/ 4 | .env 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TradingView API [![GitHub stars](https://img.shields.io/github/stars/Mathieu2301/TradingView-API.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/Mathieu2301/TradingView-API/stargazers/) 2 | 3 | [![Tests](https://github.com/Mathieu2301/TradingView-API/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/Mathieu2301/TradingView-API/actions/workflows/tests.yml) 4 | [![CodeFactor](https://www.codefactor.io/repository/github/mathieu2301/tradingview-api/badge/main)](https://www.codefactor.io/repository/github/mathieu2301/tradingview-api/overview/main) 5 | [![GitHub latest commit](https://img.shields.io/github/last-commit/Mathieu2301/TradingView-API)](https://GitHub.com/Mathieu2301/TradingView-API/commit/) 6 | [![Npm package yearly downloads](https://badgen.net/npm/dt/@mathieuc/tradingview)](https://npmjs.com/@mathieuc/tradingview) 7 | [![Minimum node.js version](https://badgen.net/npm/node/@mathieuc/tradingview)](https://npmjs.com/@mathieuc/tradingview) 8 | [![Npm package version](https://badgen.net/npm/v/@mathieuc/tradingview)](https://npmjs.com/package/@mathieuc/tradingview) 9 | 10 | Get realtime market prices and indicator values from Tradingview ! 11 | 12 | ## 🟢 Need help with your project? 13 | 14 | 🚀 Click [here](https://forms.gle/qPp5RKo8L55C5oJE7) for personalized assistance on your project. 15 | 16 | ## 🔵 Telegram group 17 | 18 | 👉 To get help, exchange tips, find collaborators, developers, missions, etc... 19 | 20 | Join the Telegram group of the TradingView-API Community: [t.me/tradingview_api](https://t.me/tradingview_api) 21 | 22 | ## Features 23 | 24 | - [x] Premium features 25 | - [x] Automatically backtest many strategies and try many settings in a very little time 26 | - [x] Get drawings you made on your chart 27 | - [x] Works with invite-only indicators 28 | - [x] Unlimited simultaneous indicators 29 | - [x] Realtime 30 | - [x] Get TradingView's technical analysis 31 | - [x] Replay mode + Fake Replay mode (for free plan) 32 | - [x] Get values from a specific date range 33 | - [ ] TradingView socket server emulation 34 | - [ ] Interract with public chats 35 | - [ ] Get Screener top values 36 | - [ ] Get Hotlists 37 | - [ ] Get Calendar 38 | - IF YOU WANT A FEATURE, ASK ME ! 39 | 40 | ## Possibilities 41 | 42 | - Trading bot 43 | - Discord alerts 44 | - Hard backtest 45 | - Machine Learning based indicator 46 | - Free replay mode for all timeframes 47 | 48 | ___ 49 | 50 | ## Installation 51 | 52 | Stable version: 53 | 54 | ```ruby 55 | npm i @mathieuc/tradingview 56 | ``` 57 | 58 | Last version: 59 | 60 | ```ruby 61 | npm i github:Mathieu2301/TradingView-API 62 | ``` 63 | 64 | ## Examples 65 | 66 | You can find all the examples and snippets in `./examples` folder. 67 | 68 | ## Before opening an issue 69 | 70 | Please look at examples and previously resolved issues before opening a new one. I can't help everyone (especially for questions that are not library related but JavaScript related). Thank you for your understanding. 71 | ___ 72 | 73 | ## Problems 74 | 75 | If you have errors in console or unwanted behavior, 76 | please create an issue [here](https://github.com/Mathieu2301/Tradingview-API/issues). 77 | -------------------------------------------------------------------------------- /docs/DOCS.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ### All the project is now JSDoc-ed :) 4 | -------------------------------------------------------------------------------- /examples/AllPrivateIndicators.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example creates a chart with all user's private indicators 5 | */ 6 | 7 | if (!process.env.SESSION || !process.env.SIGNATURE) { 8 | throw Error('Please set your sessionid and signature cookies'); 9 | } 10 | 11 | const client = new TradingView.Client({ 12 | token: process.env.SESSION, 13 | signature: process.env.SIGNATURE, 14 | }); 15 | 16 | const chart = new client.Session.Chart(); 17 | chart.setMarket('BINANCE:BTCEUR', { 18 | timeframe: 'D', 19 | }); 20 | 21 | (async () => { 22 | const indicList = await TradingView.getPrivateIndicators(process.argv[2]); 23 | 24 | if (!indicList.length) { 25 | console.error('Your account has no private indicators'); 26 | process.exit(0); 27 | } 28 | 29 | for (const indic of indicList) { 30 | const privateIndic = await indic.get(); 31 | console.log('Loading indicator', indic.name, '...'); 32 | 33 | const indicator = new chart.Study(privateIndic); 34 | 35 | indicator.onReady(() => { 36 | console.log('Indicator', indic.name, 'loaded !'); 37 | }); 38 | 39 | indicator.onUpdate(() => { 40 | console.log('Plot values', indicator.periods); 41 | console.log('Strategy report', indicator.strategyReport); 42 | }); 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /examples/BuiltInIndicator.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests built-in indicators like volume-based indicators 5 | */ 6 | 7 | const volumeProfile = new TradingView.BuiltInIndicator('VbPFixed@tv-basicstudies-241!'); 8 | 9 | const needAuth = ![ 10 | 'VbPFixed@tv-basicstudies-241', 11 | 'VbPFixed@tv-basicstudies-241!', 12 | 'Volume@tv-basicstudies-241', 13 | ].includes(volumeProfile.type); 14 | 15 | if (needAuth && (!process.env.SESSION || !process.env.SIGNATURE)) { 16 | throw Error('Please set your sessionid and signature cookies'); 17 | } 18 | 19 | const client = new TradingView.Client( 20 | needAuth 21 | ? { 22 | token: process.env.SESSION, 23 | signature: process.env.SIGNATURE, 24 | } 25 | : {}, 26 | ); 27 | 28 | const chart = new client.Session.Chart(); 29 | chart.setMarket('BINANCE:BTCEUR', { 30 | timeframe: '60', 31 | range: 1, 32 | }); 33 | 34 | /* Required or not, depending on the indicator */ 35 | volumeProfile.setOption('first_bar_time', Date.now() - 10 ** 8); 36 | // volumeProfile.setOption('first_visible_bar_time', Date.now() - 10 ** 8); 37 | 38 | const VOL = new chart.Study(volumeProfile); 39 | VOL.onUpdate(() => { 40 | VOL.graphic.horizHists 41 | .filter((h) => h.lastBarTime === 0) // We only keep recent volume infos 42 | .sort((a, b) => b.priceHigh - a.priceHigh) 43 | .forEach((h) => { 44 | console.log( 45 | `~ ${Math.round((h.priceHigh + h.priceLow) / 2)} € :`, 46 | `${'_'.repeat(h.rate[0] / 3)}${'_'.repeat(h.rate[1] / 3)}`, 47 | ); 48 | }); 49 | 50 | client.end(); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/CustomChartType.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example creates charts of custom types such as 'HeikinAshi', 'Renko', 5 | * 'LineBreak', 'Kagi', 'PointAndFigure', and 'Range' with default settings. 6 | */ 7 | 8 | const client = new TradingView.Client({ 9 | /* 10 | Token and signature are only required if you want to use 11 | intraday timeframes (if you have a paid TradingView account) 12 | */ 13 | token: process.env.SESSION, 14 | signature: process.env.SIGNATURE, 15 | }); 16 | 17 | const chart = new client.Session.Chart(); 18 | 19 | chart.onError((...err) => { 20 | console.log('Chart error:', ...err); 21 | process.exit(1); 22 | }); 23 | 24 | chart.onUpdate(() => { 25 | if (!chart.periods[0]) return; 26 | console.log('Last period', chart.periods[0]); 27 | }); 28 | 29 | /* (0s) Heikin Ashi chart */ 30 | setTimeout(() => { 31 | console.log('\nSetting chart type to: HeikinAshi'); 32 | 33 | chart.setMarket('BINANCE:BTCEUR', { 34 | type: 'HeikinAshi', 35 | timeframe: 'D', 36 | }); 37 | }, 0); 38 | 39 | /* (5s) Renko chart */ 40 | setTimeout(() => { 41 | console.log('\nSetting chart type to: Renko'); 42 | 43 | chart.setMarket('BINANCE:BTCEUR', { 44 | type: 'Renko', 45 | timeframe: 'D', 46 | inputs: { 47 | source: 'close', 48 | sources: 'Close', 49 | boxSize: 3, 50 | style: 'ATR', 51 | atrLength: 14, 52 | wicks: true, 53 | }, 54 | }); 55 | }, 5000); 56 | 57 | /* (10s) Line Break chart */ 58 | setTimeout(() => { 59 | console.log('\nSetting chart type to: LineBreak'); 60 | 61 | chart.setMarket('BINANCE:BTCEUR', { 62 | type: 'LineBreak', 63 | timeframe: 'D', 64 | inputs: { 65 | source: 'close', 66 | lb: 3, 67 | }, 68 | }); 69 | }, 10000); 70 | 71 | /* (15s) Kagi chart */ 72 | setTimeout(() => { 73 | console.log('\nSetting chart type to: Kagi'); 74 | 75 | chart.setMarket('BINANCE:BTCEUR', { 76 | type: 'Kagi', 77 | timeframe: 'D', 78 | inputs: { 79 | source: 'close', 80 | style: 'ATR', 81 | atrLength: 14, 82 | reversalAmount: 1, 83 | }, 84 | }); 85 | }, 15000); 86 | 87 | /* (20s) Point & Figure chart */ 88 | setTimeout(() => { 89 | console.log('\nSetting chart type to: PointAndFigure'); 90 | 91 | chart.setMarket('BINANCE:BTCEUR', { 92 | type: 'PointAndFigure', 93 | timeframe: 'D', 94 | inputs: { 95 | sources: 'Close', 96 | reversalAmount: 3, 97 | boxSize: 1, 98 | style: 'ATR', 99 | atrLength: 14, 100 | oneStepBackBuilding: false, 101 | }, 102 | }); 103 | }, 20000); 104 | 105 | /* (25s) Range chart */ 106 | setTimeout(() => { 107 | console.log('\nSetting chart type to: Range'); 108 | 109 | chart.setMarket('BINANCE:BTCEUR', { 110 | type: 'Range', 111 | timeframe: 'D', 112 | inputs: { 113 | range: 1, 114 | phantomBars: false, 115 | }, 116 | }); 117 | }, 25000); 118 | 119 | /* (30s) Delete chart, close client */ 120 | setTimeout(() => { 121 | console.log('\nClosing client...'); 122 | client.end(); 123 | }, 30000); 124 | -------------------------------------------------------------------------------- /examples/CustomTimeframe.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests custom timeframes like 1 second 5 | */ 6 | 7 | if (!process.env.SESSION || !process.env.SIGNATURE) { 8 | throw Error('Please set your sessionid and signature cookies'); 9 | } 10 | 11 | const client = new TradingView.Client({ 12 | token: process.env.SESSION, 13 | signature: process.env.SIGNATURE, 14 | }); 15 | 16 | const chart = new client.Session.Chart(); 17 | chart.setTimezone('Europe/Paris'); 18 | 19 | chart.setMarket('CAPITALCOM:US100', { 20 | timeframe: '1S', 21 | range: 10, 22 | }); 23 | 24 | chart.onSymbolLoaded(() => { 25 | console.log(chart.infos.name, 'loaded !'); 26 | }); 27 | 28 | chart.onUpdate(() => { 29 | console.log('OK', chart.periods); 30 | client.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/Errors.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests many types of errors 5 | */ 6 | 7 | if (!process.env.SESSION || !process.env.SIGNATURE) { 8 | throw Error('Please set your sessionid and signature cookies'); 9 | } 10 | 11 | // Creates a websocket client 12 | const client = new TradingView.Client({ 13 | token: process.env.SESSION, 14 | signature: process.env.SIGNATURE, 15 | }); 16 | 17 | const tests = [ 18 | (next) => { /* Testing "Credentials error" */ 19 | console.info('\nTesting "Credentials error" error:'); 20 | 21 | const client2 = new TradingView.Client({ 22 | token: 'FAKE_CREDENTIALS', // Set wrong credentials 23 | }); 24 | 25 | client2.onError((...err) => { 26 | console.error(' => Client error:', err); 27 | client2.end(); 28 | next(); 29 | }); 30 | }, 31 | 32 | (next) => { /* Testing "Invalid symbol" */ 33 | console.info('\nTesting "Invalid symbol" error:'); 34 | 35 | const chart = new client.Session.Chart(); 36 | chart.onError((...err) => { // Listen for errors 37 | console.error(' => Chart error:', err); 38 | chart.delete(); 39 | next(); 40 | }); 41 | 42 | chart.setMarket('XXXXX'); // Set a wrong market 43 | }, 44 | 45 | (next) => { /* Testing "Invalid timezone" */ 46 | console.info('\nTesting "Invalid timezone" error:'); 47 | 48 | const chart = new client.Session.Chart(); 49 | chart.onError((...err) => { // Listen for errors 50 | console.error(' => Chart error:', err); 51 | next(); 52 | }); 53 | 54 | chart.setMarket('BINANCE:BTCEUR'); // Set a market 55 | chart.setTimezone('Nowhere/Nowhere'); // Set a fake timezone 56 | }, 57 | 58 | (next) => { /* Testing "Custom timeframe" */ 59 | console.info('\nTesting "Custom timeframe" error:'); 60 | 61 | const chart = new client.Session.Chart(); 62 | chart.onError((...err) => { // Listen for errors 63 | console.error(' => Chart error:', err); 64 | chart.delete(); 65 | next(); 66 | }); 67 | 68 | chart.setMarket('BINANCE:BTCEUR', { // Set a market 69 | timeframe: '20', // Set a custom timeframe 70 | /* 71 | Timeframe '20' isn't available because we are 72 | not logged in as a premium TradingView account 73 | */ 74 | }); 75 | }, 76 | 77 | (next) => { /* Testing "Invalid timeframe" */ 78 | console.info('\nTesting "Invalid timeframe" error:'); 79 | 80 | const chart = new client.Session.Chart(); 81 | chart.onError((...err) => { // Listen for errors 82 | console.error(' => Chart error:', err); 83 | next(); 84 | }); 85 | 86 | chart.setMarket('BINANCE:BTCEUR', { // Set a market 87 | timeframe: 'XX', // Set a wrong timeframe 88 | }); 89 | }, 90 | 91 | (next) => { /* Testing "Study not auth" */ 92 | console.info('\nTesting "Study not auth" error:'); 93 | 94 | const chart = new client.Session.Chart(); 95 | chart.onError((...err) => { // Listen for errors 96 | console.error(' => Chart error:', err); 97 | next(); 98 | }); 99 | 100 | chart.setMarket('BINANCE:BTCEUR', { // Set a market 101 | timeframe: '15', 102 | type: 'Renko', 103 | }); 104 | 105 | chart.onUpdate(() => { 106 | console.log('DATA', chart.periods[0]); 107 | }); 108 | }, 109 | 110 | (next) => { /* Testing "Set the market before" */ 111 | console.info('\nTesting "Set the market before..." error:'); 112 | 113 | const chart = new client.Session.Chart(); 114 | chart.onError((...err) => { // Listen for errors 115 | console.error(' => Chart error:', err); 116 | chart.delete(); 117 | next(); 118 | }); 119 | 120 | chart.setSeries('15'); // Set series before setting the market 121 | }, 122 | 123 | (next) => { /* Testing "Inexistent indicator" */ 124 | console.info('\nTesting "Inexistent indicator" error:'); 125 | 126 | TradingView.getIndicator('STD;XXXXXXX') 127 | .catch((err) => { 128 | console.error(' => API error:', [err.message]); 129 | next(); 130 | }); 131 | }, 132 | 133 | async (next) => { /* Testing "Invalid value" */ 134 | console.info('\nTesting "Invalid value" error:'); 135 | 136 | const chart = new client.Session.Chart(); 137 | chart.setMarket('BINANCE:BTCEUR'); // Set a market 138 | 139 | const ST = await TradingView.getIndicator('STD;Supertrend'); 140 | ST.setOption('Factor', -1); // This will cause an error 141 | 142 | const Supertrend = new chart.Study(ST); 143 | Supertrend.onError((...err) => { 144 | console.error(' => Study error:', err); 145 | chart.delete(); 146 | next(); 147 | }); 148 | }, 149 | ]; 150 | 151 | (async () => { 152 | // eslint-disable-next-line no-restricted-syntax, no-await-in-loop 153 | for (const t of tests) await new Promise(t); 154 | console.log(`\nTests ${tests.length}/${tests.length} done !`); 155 | })(); 156 | -------------------------------------------------------------------------------- /examples/FakeReplayMode.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('../main'); 2 | 3 | /** 4 | * This example tests the fake replay mode which 5 | * works in intraday even with free plan 6 | */ 7 | 8 | console.log('----- Testing FakeReplayMode: -----'); 9 | 10 | const client = new Client(); 11 | const chart = new client.Session.Chart(); 12 | 13 | chart.setMarket('BINANCE:BTCEUR', { 14 | timeframe: '240', 15 | range: -1, // Range is negative, so 'to' means 'from' 16 | to: Math.round(Date.now() / 1000) - 86400 * 7, // Seven days before now 17 | // to: 1600000000, 18 | }); 19 | 20 | let interval = NaN; 21 | 22 | chart.onUpdate(async () => { 23 | const times = chart.periods.map((p) => p.time); 24 | 25 | const intrval = times[0] - times[1]; 26 | if (Number.isNaN(interval) && times.length >= 2) interval = intrval; 27 | 28 | if (!Number.isNaN(interval) && interval !== intrval) { 29 | throw new Error(`Wrong interval: ${intrval} (should be ${interval})`); 30 | } 31 | 32 | console.log('Next ->', times[0]); 33 | 34 | if (times[0] > ((Date.now() / 1000) - 86400 * 1)) { 35 | await client.end(); 36 | console.log('Done !', times.length); 37 | } 38 | 39 | chart.fetchMore(-2); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/FromToData.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests fetching chart data of a number 5 | * of candles before or after a timestamp 6 | */ 7 | 8 | if (!process.env.SESSION || !process.env.SIGNATURE) { 9 | throw Error('Please set your sessionid and signature cookies'); 10 | } 11 | 12 | const client = new TradingView.Client({ 13 | token: process.env.SESSION, 14 | signature: process.env.SIGNATURE, 15 | }); 16 | 17 | const chart = new client.Session.Chart(); 18 | chart.setMarket('BINANCE:BTCEUR', { 19 | timeframe: '240', 20 | range: 2, // Can be positive to get before or negative to get after 21 | to: 1700000000, 22 | }); 23 | 24 | // This works with indicators 25 | 26 | TradingView.getIndicator('STD;Supertrend').then(async (indic) => { 27 | console.log(`Loading '${indic.description}' study...`); 28 | const SUPERTREND = new chart.Study(indic); 29 | 30 | SUPERTREND.onUpdate(() => { 31 | console.log('Prices periods:', chart.periods); 32 | console.log('Study periods:', SUPERTREND.periods); 33 | client.end(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/GetDrawings.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests the getDrawings function 5 | */ 6 | 7 | // First parameter must be the layoutID 8 | // If the layout is private: 9 | // - Second parameter must be the userid (you can use getUser function) 10 | // - You should provide your sessionid and signature cookies in .env file 11 | 12 | if (!process.argv[2]) throw Error('Please specify a layoutID'); 13 | 14 | TradingView.getDrawings(process.argv[2], null, { 15 | session: process.env.SESSION, 16 | signature: process.env.SIGNATURE, 17 | id: process.argv[3], 18 | }).then((drawings) => { 19 | console.log(`Found ${drawings.length} drawings:`, drawings.map((d) => ({ 20 | id: d.id, 21 | symbol: d.symbol, 22 | type: d.type, 23 | text: d.state.text, 24 | }))); 25 | }).catch((err) => { 26 | console.error('Error:', err.message); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/GraphicIndicator.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests an indicator that sends graphic data such 5 | * as 'lines', 'labels', 'boxes', 'tables', 'polygons', etc... 6 | */ 7 | 8 | if (!process.env.SESSION || !process.env.SIGNATURE) { 9 | throw Error('Please set your sessionid and signature cookies'); 10 | } 11 | 12 | const client = new TradingView.Client({ 13 | token: process.env.SESSION, 14 | signature: process.env.SIGNATURE, 15 | }); 16 | 17 | const chart = new client.Session.Chart(); 18 | chart.setMarket('BINANCE:BTCEUR', { 19 | timeframe: '5', 20 | range: 10000, 21 | }); 22 | 23 | // TradingView.getIndicator('USER;01efac32df544348810bc843a7515f36').then((indic) => { 24 | // TradingView.getIndicator('PUB;5xi4DbWeuIQrU0Fx6ZKiI2odDvIW9q2j').then((indic) => { 25 | TradingView.getIndicator('STD;Zig_Zag').then((indic) => { 26 | const STD = new chart.Study(indic); 27 | 28 | STD.onError((...err) => { 29 | console.log('Study error:', ...err); 30 | }); 31 | 32 | STD.onReady(() => { 33 | console.log(`STD '${STD.instance.description}' Loaded !`); 34 | }); 35 | 36 | STD.onUpdate(() => { 37 | console.log('Graphic data:', STD.graphic); 38 | // console.log('Tables:', changes, STD.graphic.tables); 39 | // console.log('Cells:', STD.graphic.tables[0].cells()); 40 | client.end(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/MultipleSyncFetch.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This examples synchronously fetches data from 3 indicators 5 | */ 6 | 7 | if (!process.env.SESSION || !process.env.SIGNATURE) { 8 | throw Error('Please set your sessionid and signature cookies'); 9 | } 10 | 11 | const client = new TradingView.Client({ 12 | token: process.env.SESSION, 13 | signature: process.env.SIGNATURE, 14 | }); 15 | 16 | function getIndicData(indicator) { 17 | const chart = new client.Session.Chart(); 18 | chart.setMarket('BINANCE:DOTUSDT'); 19 | const STD = new chart.Study(indicator); 20 | 21 | console.log(`Getting "${indicator.description}"...`); 22 | 23 | return new Promise((res) => { 24 | STD.onUpdate(() => { 25 | res(STD.periods); 26 | console.log(`"${indicator.description}" done !`); 27 | }); 28 | }); 29 | } 30 | 31 | (async () => { 32 | console.log('Getting all indicators...'); 33 | 34 | const indicData = await Promise.all([ 35 | await TradingView.getIndicator('PUB;3lEKXjKWycY5fFZRYYujEy8fxzRRUyF3'), 36 | await TradingView.getIndicator('PUB;5nawr3gCESvSHQfOhrLPqQqT4zM23w3X'), 37 | await TradingView.getIndicator('PUB;vrOJcNRPULteowIsuP6iHn3GIxBJdXwT'), 38 | ].map(getIndicData)); 39 | 40 | console.log(indicData); 41 | console.log('All done !'); 42 | 43 | client.end(); 44 | })(); 45 | -------------------------------------------------------------------------------- /examples/PinePermManage.js: -------------------------------------------------------------------------------- 1 | const { PinePermManager } = require('../main'); 2 | 3 | /** 4 | * This example creates a pine permission manager 5 | * and tests all the available functions 6 | */ 7 | 8 | if (!process.env.SESSION || !process.env.SIGNATURE) { 9 | throw Error('Please set your sessionid and signature cookies'); 10 | } 11 | 12 | const pineid = process.argv[2]; 13 | 14 | if (!pineid) throw Error('Please specify a pine id as first argument'); 15 | 16 | console.log('Pine ID:', pineid); 17 | 18 | const manager = new PinePermManager( 19 | process.env.SESSION, 20 | process.env.SIGNATURE, 21 | pineid, 22 | ); 23 | 24 | (async () => { 25 | console.log('Users:', await manager.getUsers()); 26 | 27 | console.log('Adding user \'TradingView\'...'); 28 | 29 | switch (await manager.addUser('TradingView')) { 30 | case 'ok': 31 | console.log('Done !'); 32 | break; 33 | case 'exists': 34 | console.log('This user is already authorized'); 35 | break; 36 | default: 37 | console.error('Unknown error...'); 38 | break; 39 | } 40 | 41 | console.log('Users:', await manager.getUsers()); 42 | 43 | console.log('Modifying expiration date...'); 44 | 45 | const newDate = new Date(Date.now() + 24 * 3600000); // Add one day 46 | if (await manager.modifyExpiration('TradingView', newDate) === 'ok') { 47 | console.log('Done !'); 48 | } else console.error('Unknown error...'); 49 | 50 | console.log('Users:', await manager.getUsers()); 51 | 52 | console.log('Removing expiration date...'); 53 | 54 | if (await manager.modifyExpiration('TradingView') === 'ok') { 55 | console.log('Done !'); 56 | } else console.error('Unknown error...'); 57 | 58 | console.log('Users:', await manager.getUsers()); 59 | 60 | console.log('Removing user \'TradingView\'...'); 61 | 62 | if (await manager.removeUser('TradingView') === 'ok') { 63 | console.log('Done !'); 64 | } else console.error('Unknown error...'); 65 | 66 | console.log('Users:', await manager.getUsers()); 67 | })(); 68 | -------------------------------------------------------------------------------- /examples/ReplayMode.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests the real replay mode by fetching 5 | * indicator data and stores it in a 'periods' variable 6 | */ 7 | 8 | if (!process.env.SESSION || !process.env.SIGNATURE) { 9 | throw Error('Please set your sessionid and signature cookies'); 10 | } 11 | 12 | console.log('----- Testing ReplayMode: -----'); 13 | 14 | const client = new TradingView.Client({ 15 | token: process.env.SESSION, 16 | signature: process.env.SIGNATURE, 17 | }); 18 | const chart = new client.Session.Chart(); 19 | 20 | const config = { 21 | symbol: 'BINANCE:BTCEUR', 22 | timeframe: 'D', 23 | startFrom: Math.round(Date.now() / 1000) - 86400 * 7, // Seven days before now 24 | // startFrom: 1600000000, 25 | }; 26 | 27 | chart.setMarket(config.symbol, { 28 | timeframe: config.timeframe, 29 | replay: config.startFrom, 30 | range: 1, 31 | }); 32 | 33 | // chart.onReplayLoaded(() => { 34 | // console.log('STARTING REPLAY MODE'); 35 | // chart.replayStart(1000); 36 | 37 | // // setTimeout(() => { 38 | // // console.log('STOPPING REPLAY MODE'); 39 | // // chart.replayStop(); 40 | // // }, 7000); 41 | // }); 42 | 43 | let loading = 0; 44 | const indicators = []; 45 | const periods = {}; 46 | 47 | let interval = NaN; 48 | 49 | async function step() { 50 | const period = { ...chart.periods[0] }; 51 | 52 | const times = Object.keys(periods); 53 | const intrval = times[times.length - 1] - times[times.length - 2]; 54 | if (Number.isNaN(interval) && times.length >= 2) interval = intrval; 55 | 56 | if (!Number.isNaN(interval) && interval !== intrval) { 57 | throw new Error(`Wrong interval: ${intrval} (should be ${interval})`); 58 | } 59 | 60 | indicators.forEach(([n, i]) => { 61 | const plots = { ...i.periods[0] }; 62 | delete plots.$time; 63 | period[n] = { plots }; 64 | 65 | Object.keys(i.graphic).forEach((g) => { 66 | if (!i.graphic[g].length) return; 67 | if (!period[n].graphics) period[n].graphics = {}; 68 | period[n].graphics[g] = i.graphic[g]; 69 | }); 70 | }); 71 | 72 | periods[period.time] = period; 73 | 74 | console.log('Next ->', period.time, times.length); 75 | 76 | await chart.replayStep(1); 77 | step(); 78 | } 79 | 80 | chart.onReplayEnd(async () => { 81 | await client.end(); 82 | console.log('Done !', Object.keys(periods).length); 83 | }); 84 | 85 | async function addIndicator(name, pineId, options = {}) { 86 | loading += 1; 87 | 88 | const indic = pineId.includes('@') 89 | ? new TradingView.BuiltInIndicator(pineId) 90 | : await TradingView.getIndicator(pineId); 91 | Object.keys(options).forEach((o) => { indic.setOption(o, options[o]); }); 92 | 93 | const std = new chart.Study(indic); 94 | 95 | std.onReady(() => { 96 | indicators.push([name, std]); 97 | if (loading === indicators.length) step(); 98 | }); 99 | } 100 | 101 | addIndicator('Volume', 'Volume@tv-basicstudies-241'); 102 | addIndicator('EMA_50', 'STD;EMA', { Length: 50 }); 103 | addIndicator('EMA_200', 'STD;EMA', { Length: 200 }); 104 | -------------------------------------------------------------------------------- /examples/Search.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests the searching functions such 5 | * as 'searchMarketV3' and 'searchIndicator' 6 | */ 7 | 8 | TradingView.searchMarketV3('BINANCE:').then((rs) => { 9 | console.log('Found Markets:', rs); 10 | }); 11 | 12 | TradingView.searchIndicator('RSI').then((rs) => { 13 | console.log('Found Indicators:', rs); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/SimpleChart.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example creates a BTCEUR daily chart 5 | */ 6 | 7 | const client = new TradingView.Client(); // Creates a websocket client 8 | 9 | const chart = new client.Session.Chart(); // Init a Chart session 10 | 11 | chart.setMarket('BINANCE:BTCEUR', { // Set the market 12 | timeframe: 'D', 13 | }); 14 | 15 | chart.onError((...err) => { // Listen for errors (can avoid crash) 16 | console.error('Chart error:', ...err); 17 | // Do something... 18 | }); 19 | 20 | chart.onSymbolLoaded(() => { // When the symbol is successfully loaded 21 | console.log(`Market "${chart.infos.description}" loaded !`); 22 | }); 23 | 24 | chart.onUpdate(() => { // When price changes 25 | if (!chart.periods[0]) return; 26 | console.log(`[${chart.infos.description}]: ${chart.periods[0].close} ${chart.infos.currency_id}`); 27 | // Do something... 28 | }); 29 | 30 | // Wait 5 seconds and set the market to BINANCE:ETHEUR 31 | setTimeout(() => { 32 | console.log('\nSetting market to BINANCE:ETHEUR...'); 33 | chart.setMarket('BINANCE:ETHEUR', { 34 | timeframe: 'D', 35 | }); 36 | }, 5000); 37 | 38 | // Wait 10 seconds and set the timeframe to 15 minutes 39 | setTimeout(() => { 40 | console.log('\nSetting timeframe to 15 minutes...'); 41 | chart.setSeries('15'); 42 | }, 10000); 43 | 44 | // Wait 15 seconds and set the chart type to "Heikin Ashi" 45 | setTimeout(() => { 46 | console.log('\nSetting the chart type to "Heikin Ashi"s...'); 47 | chart.setMarket('BINANCE:ETHEUR', { 48 | timeframe: 'D', 49 | type: 'HeikinAshi', 50 | }); 51 | }, 15000); 52 | 53 | // Wait 20 seconds and close the chart 54 | setTimeout(() => { 55 | console.log('\nClosing the chart...'); 56 | chart.delete(); 57 | }, 20000); 58 | 59 | // Wait 25 seconds and close the client 60 | setTimeout(() => { 61 | console.log('\nClosing the client...'); 62 | client.end(); 63 | }, 25000); 64 | -------------------------------------------------------------------------------- /examples/UserLogin.js: -------------------------------------------------------------------------------- 1 | const TradingView = require('../main'); 2 | 3 | /** 4 | * This example tests the user login function 5 | */ 6 | 7 | if (!process.argv[2]) throw Error('Please specify your username/email'); 8 | if (!process.argv[3]) throw Error('Please specify your password'); 9 | 10 | TradingView.loginUser(process.argv[2], process.argv[3], false).then((user) => { 11 | console.log('User:', user); 12 | console.log('Sessionid:', user.session); 13 | console.log('Signature:', user.signature); 14 | }).catch((err) => { 15 | console.error('Login error:', err.message); 16 | }); 17 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const miscRequests = require('./src/miscRequests'); 2 | const Client = require('./src/client'); 3 | const BuiltInIndicator = require('./src/classes/BuiltInIndicator'); 4 | const PineIndicator = require('./src/classes/PineIndicator'); 5 | const PinePermManager = require('./src/classes/PinePermManager'); 6 | 7 | module.exports = { ...miscRequests }; 8 | module.exports.Client = Client; 9 | module.exports.BuiltInIndicator = BuiltInIndicator; 10 | module.exports.PineIndicator = PineIndicator; 11 | module.exports.PinePermManager = PinePermManager; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mathieuc/tradingview", 3 | "version": "3.5.1", 4 | "description": "Tradingview instant stocks API, indicator alerts, trading bot, and more !", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "vitest", 8 | "example": "node --env-file=.env", 9 | "example:dev": "nodemon --env-file=.env" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Mathieu2301/TradingView-API.git" 14 | }, 15 | "engines": { 16 | "node": ">=14.0.0" 17 | }, 18 | "keywords": [ 19 | "tradingwiew", 20 | "market", 21 | "stocks", 22 | "crypto", 23 | "forex", 24 | "indicator", 25 | "bitcoin", 26 | "api" 27 | ], 28 | "author": "Mathieu Colmon", 29 | "license": "ISC", 30 | "dependencies": { 31 | "axios": "^1.5.0", 32 | "jszip": "^3.7.1", 33 | "ws": "^7.4.3" 34 | }, 35 | "devDependencies": { 36 | "@babel/eslint-parser": "^7.15.7", 37 | "@mathieuc/console": "^1.0.1", 38 | "dotenv": "^16.3.1", 39 | "eslint": "^7.25.0", 40 | "eslint-config-airbnb-base": "^14.2.1", 41 | "eslint-plugin-import": "^2.22.1", 42 | "vitest": "^0.31.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/chart/graphicParser.js: -------------------------------------------------------------------------------- 1 | const TRANSLATOR = { 2 | /** @typedef {'right' | 'left' | 'both' | 'none'} ExtendValue */ 3 | extend: { 4 | r: 'right', 5 | l: 'left', 6 | b: 'both', 7 | n: 'none', 8 | }, 9 | 10 | /** @typedef {'price' | 'abovebar' | 'belowbar'} yLocValue */ 11 | yLoc: { 12 | pr: 'price', 13 | ab: 'abovebar', 14 | bl: 'belowbar', 15 | }, 16 | 17 | /** 18 | * @typedef {'none' | 'xcross' | 'cross' | 'triangleup' 19 | * | 'triangledown' | 'flag' | 'circle' | 'arrowup' 20 | * | 'arrowdown' | 'label_up' | 'label_down' | 'label_left' 21 | * | 'label_right' | 'label_lower_left' | 'label_lower_right' 22 | * | 'label_upper_left' | 'label_upper_right' | 'label_center' 23 | * | 'square' | 'diamond' 24 | * } LabelStyleValue 25 | * */ 26 | labelStyle: { 27 | n: 'none', 28 | xcr: 'xcross', 29 | cr: 'cross', 30 | tup: 'triangleup', 31 | tdn: 'triangledown', 32 | flg: 'flag', 33 | cir: 'circle', 34 | aup: 'arrowup', 35 | adn: 'arrowdown', 36 | lup: 'label_up', 37 | ldn: 'label_down', 38 | llf: 'label_left', 39 | lrg: 'label_right', 40 | llwlf: 'label_lower_left', 41 | llwrg: 'label_lower_right', 42 | luplf: 'label_upper_left', 43 | luprg: 'label_upper_right', 44 | lcn: 'label_center', 45 | sq: 'square', 46 | dia: 'diamond', 47 | }, 48 | 49 | /** 50 | * @typedef {'solid' | 'dotted' | 'dashed'| 'arrow_left' 51 | * | 'arrow_right' | 'arrow_both'} LineStyleValue 52 | */ 53 | lineStyle: { 54 | sol: 'solid', 55 | dot: 'dotted', 56 | dsh: 'dashed', 57 | al: 'arrow_left', 58 | ar: 'arrow_right', 59 | ab: 'arrow_both', 60 | }, 61 | 62 | /** @typedef {'solid' | 'dotted' | 'dashed'} BoxStyleValue */ 63 | boxStyle: { 64 | sol: 'solid', 65 | dot: 'dotted', 66 | dsh: 'dashed', 67 | }, 68 | }; 69 | 70 | /** 71 | * @typedef {'auto' | 'huge' | 'large' 72 | * | 'normal' | 'small' | 'tiny'} SizeValue 73 | */ 74 | /** @typedef {'top' | 'center' | 'bottom'} VAlignValue */ 75 | /** @typedef {'left' | 'center' | 'right'} HAlignValue */ 76 | /** @typedef {'none' | 'auto'} TextWrapValue */ 77 | /** 78 | * @typedef {'top_left' | 'top_center' | 'top_right' 79 | * | 'middle_left' | 'middle_center' | 'middle_right' 80 | * | 'bottom_left' | 'bottom_center' | 'bottom_right' 81 | * } TablePositionValue 82 | */ 83 | 84 | /** 85 | * @typedef {Object} GraphicLabel 86 | * @prop {number} id Drawing ID 87 | * @prop {number} x Label x position 88 | * @prop {number} y Label y position 89 | * @prop {yLocValue} yLoc yLoc mode 90 | * @prop {string} text Label text 91 | * @prop {LabelStyleValue} style Label style 92 | * @prop {number} color 93 | * @prop {number} textColor 94 | * @prop {SizeValue} size Label size 95 | * @prop {HAlignValue} textAlign Text horizontal align 96 | * @prop {string} toolTip Tooltip text 97 | */ 98 | 99 | /** 100 | * @typedef {Object} GraphicLine 101 | * @prop {number} id Drawing ID 102 | * @prop {number} x1 First x position 103 | * @prop {number} y1 First y position 104 | * @prop {number} x2 Second x position 105 | * @prop {number} y2 Second y position 106 | * @prop {ExtendValue} extend Horizontal extend 107 | * @prop {LineStyleValue} style Line style 108 | * @prop {number} color Line color 109 | * @prop {number} width Line width 110 | */ 111 | 112 | /** 113 | * @typedef {Object} GraphicBox 114 | * @prop {number} id Drawing ID 115 | * @prop {number} x1 First x position 116 | * @prop {number} y1 First y position 117 | * @prop {number} x2 Second x position 118 | * @prop {number} y2 Second y position 119 | * @prop {number} color Box color 120 | * @prop {number} bgColor Background color 121 | * @prop {ExtendValue} extend Horizontal extend 122 | * @prop {BoxStyleValue} style Box style 123 | * @prop {number} width Box width 124 | * @prop {string} text Text 125 | * @prop {SizeValue} textSize Text size 126 | * @prop {number} textColor Text color 127 | * @prop {VAlignValue} textVAlign Text vertical align 128 | * @prop {HAlignValue} textHAlign Text horizontal align 129 | * @prop {TextWrapValue} textWrap Text wrap 130 | */ 131 | 132 | /** 133 | * @typedef {Object} TableCell 134 | * @prop {number} id Drawing ID 135 | * @prop {string} text Cell text 136 | * @prop {number} width Cell width 137 | * @prop {number} height Cell height 138 | * @prop {number} textColor Text color 139 | * @prop {HAlignValue} textHAlign Text horizontal align 140 | * @prop {VAlignValue} textVAlign Text Vertical align 141 | * @prop {SizeValue} textSize Text size 142 | * @prop {number} bgColor Background color 143 | */ 144 | 145 | /** 146 | * @typedef {Object} GraphicTable 147 | * @prop {number} id Drawing ID 148 | * @prop {TablePositionValue} position Table position 149 | * @prop {number} rows Number of rows 150 | * @prop {number} columns Number of columns 151 | * @prop {number} bgColor Background color 152 | * @prop {number} frameColor Frame color 153 | * @prop {number} frameWidth Frame width 154 | * @prop {number} borderColor Border color 155 | * @prop {number} borderWidth Border width 156 | * @prop {() => TableCell[][]} cells Table cells matrix 157 | */ 158 | 159 | /** 160 | * @typedef {Object} GraphicHorizline 161 | * @prop {number} id Drawing ID 162 | * @prop {number} level Y position of the line 163 | * @prop {number} startIndex Start index of the line (`chart.periods[line.startIndex]`) 164 | * @prop {number} endIndex End index of the line (`chart.periods[line.endIndex]`) 165 | * @prop {boolean} extendRight Is the line extended to the right 166 | * @prop {boolean} extendLeft Is the line extended to the left 167 | */ 168 | 169 | /** 170 | * @typedef {Object} GraphicPoint 171 | * @prop {number} index X position of the point 172 | * @prop {number} level Y position of the point 173 | */ 174 | 175 | /** 176 | * @typedef {Object} GraphicPolygon 177 | * @prop {number} id Drawing ID 178 | * @prop {GraphicPoint[]} points List of polygon points 179 | */ 180 | 181 | /** 182 | * @typedef {Object} GraphicHorizHist 183 | * @prop {number} id Drawing ID 184 | * @prop {number} priceLow Low Y position 185 | * @prop {number} priceHigh High Y position 186 | * @prop {number} firstBarTime First X position 187 | * @prop {number} lastBarTime Last X position 188 | * @prop {number[]} rate List of values 189 | */ 190 | 191 | /** 192 | * @typedef {Object} GraphicData List of drawings indexed by type 193 | * @prop {GraphicLabel[]} labels List of labels drawings 194 | * @prop {GraphicLine[]} lines List of lines drawings 195 | * @prop {GraphicBox[]} boxes List of boxes drawings 196 | * @prop {GraphicTable[]} tables List of tables drawings 197 | * @prop {GraphicPolygon[]} polygons List of polygons drawings 198 | * @prop {GraphicHorizHist[]} horizHists List of horizontal histograms drawings 199 | * @prop {GraphicHorizline[]} horizLines List of horizontal lines drawings 200 | */ 201 | 202 | /** 203 | * @param {Object} rawGraphic Raw graphic data 204 | * @param {Object} indexes Drawings xPos indexes 205 | * @returns {GraphicData} 206 | */ 207 | module.exports = function graphicParse(rawGraphic = {}, indexes = []) { 208 | // console.log('indexes', indexes); 209 | return { 210 | labels: Object.values(rawGraphic.dwglabels ?? {}).map((l) => ({ 211 | id: l.id, 212 | x: indexes[l.x], 213 | y: l.y, 214 | yLoc: TRANSLATOR.yLoc[l.yl] ?? l.yl, 215 | text: l.t, 216 | style: TRANSLATOR.labelStyle[l.st] ?? l.st, 217 | color: l.ci, 218 | textColor: l.tci, 219 | size: l.sz, 220 | textAlign: l.ta, 221 | toolTip: l.tt, 222 | })), 223 | 224 | lines: Object.values(rawGraphic.dwglines ?? {}).map((l) => ({ 225 | id: l.id, 226 | x1: indexes[l.x1], 227 | y1: l.y1, 228 | x2: indexes[l.x2], 229 | y2: l.y2, 230 | extend: TRANSLATOR.extend[l.ex] ?? l.ex, 231 | style: TRANSLATOR.lineStyle[l.st] ?? l.st, 232 | color: l.ci, 233 | width: l.w, 234 | })), 235 | 236 | boxes: Object.values(rawGraphic.dwgboxes ?? {}).map((b) => ({ 237 | id: b.id, 238 | x1: indexes[b.x1], 239 | y1: b.y1, 240 | x2: indexes[b.x2], 241 | y2: b.y2, 242 | color: b.c, 243 | bgColor: b.bc, 244 | extend: TRANSLATOR.extend[b.ex] ?? b.ex, 245 | style: TRANSLATOR.boxStyle[b.st] ?? b.st, 246 | width: b.w, 247 | text: b.t, 248 | textSize: b.ts, 249 | textColor: b.tc, 250 | textVAlign: b.tva, 251 | textHAlign: b.tha, 252 | textWrap: b.tw, 253 | })), 254 | 255 | tables: Object.values(rawGraphic.dwgtables ?? {}).map((t) => ({ 256 | id: t.id, 257 | position: t.pos, 258 | rows: t.rows, 259 | columns: t.cols, 260 | bgColor: t.bgc, 261 | frameColor: t.frmc, 262 | frameWidth: t.frmw, 263 | borderColor: t.brdc, 264 | borderWidth: t.brdw, 265 | cells: () => { 266 | const matrix = []; 267 | Object.values(rawGraphic.dwgtablecells ?? {}).forEach((cell) => { 268 | if (cell.tid !== t.id) return; 269 | if (!matrix[cell.row]) matrix[cell.row] = []; 270 | matrix[cell.row][cell.col] = { 271 | id: cell.id, 272 | text: cell.t, 273 | width: cell.w, 274 | height: cell.h, 275 | textColor: cell.tc, 276 | textHAlign: cell.tha, 277 | textVAlign: cell.tva, 278 | textSize: cell.ts, 279 | bgColor: cell.bgc, 280 | }; 281 | }); 282 | return matrix; 283 | }, 284 | })), 285 | 286 | horizLines: Object.values(rawGraphic.horizlines ?? {}).map((h) => ({ 287 | ...h, 288 | startIndex: indexes[h.startIndex], 289 | endIndex: indexes[h.endIndex], 290 | })), 291 | 292 | polygons: Object.values(rawGraphic.polygons ?? {}).map((p) => ({ 293 | ...p, 294 | points: p.points.map((pt) => ({ 295 | ...pt, 296 | index: indexes[pt.index], 297 | })), 298 | })), 299 | 300 | horizHists: Object.values(rawGraphic.hhists ?? {}).map((h) => ({ 301 | ...h, 302 | firstBarTime: indexes[h.firstBarTime], 303 | lastBarTime: indexes[h.lastBarTime], 304 | })), 305 | 306 | raw: () => rawGraphic, 307 | }; 308 | }; 309 | -------------------------------------------------------------------------------- /src/chart/session.js: -------------------------------------------------------------------------------- 1 | const { genSessionID } = require('../utils'); 2 | 3 | const studyConstructor = require('./study'); 4 | 5 | /** 6 | * @typedef {'HeikinAshi' | 'Renko' | 'LineBreak' | 'Kagi' | 'PointAndFigure' 7 | * | 'Range'} ChartType Custom chart type 8 | */ 9 | 10 | const ChartTypes = { 11 | HeikinAshi: 'BarSetHeikenAshi@tv-basicstudies-60!', 12 | Renko: 'BarSetRenko@tv-prostudies-40!', 13 | LineBreak: 'BarSetPriceBreak@tv-prostudies-34!', 14 | Kagi: 'BarSetKagi@tv-prostudies-34!', 15 | PointAndFigure: 'BarSetPnF@tv-prostudies-34!', 16 | Range: 'BarSetRange@tv-basicstudies-72!', 17 | }; 18 | 19 | /** 20 | * @typedef {Object} ChartInputs Custom chart type 21 | * @prop {number} [atrLength] Renko/Kagi/PointAndFigure ATR length 22 | * @prop {'open' | 'high' | 'low' | 'close' | 'hl2' 23 | * | 'hlc3' | 'ohlc4'} [source] Renko/LineBreak/Kagi source 24 | * @prop {'ATR' | string} [style] Renko/Kagi/PointAndFigure style 25 | * @prop {number} [boxSize] Renko/PointAndFigure box size 26 | * @prop {number} [reversalAmount] Kagi/PointAndFigure reversal amount 27 | * @prop {'Close'} [sources] Renko/PointAndFigure sources 28 | * @prop {boolean} [wicks] Renko wicks 29 | * @prop {number} [lb] LineBreak Line break 30 | * @prop {boolean} [oneStepBackBuilding] PointAndFigure oneStepBackBuilding 31 | * @prop {boolean} [phantomBars] Range phantom bars 32 | * @prop {number} [range] Range range 33 | */ 34 | 35 | /** @typedef {Object} StudyListeners */ 36 | 37 | /** 38 | * @typedef {Object} ChartSessionBridge 39 | * @prop {string} sessionID 40 | * @prop {StudyListeners} studyListeners 41 | * @prop {Object} indexes 42 | * @prop {import('../client').SendPacket} send 43 | */ 44 | 45 | /** 46 | * @typedef {'seriesLoaded' | 'symbolLoaded' | 'update' | 'error'} ChartEvent 47 | */ 48 | 49 | /** 50 | * @typedef {Object} PricePeriod 51 | * @prop {number} time Period timestamp 52 | * @prop {number} open Period open value 53 | * @prop {number} close Period close value 54 | * @prop {number} max Period max value 55 | * @prop {number} min Period min value 56 | * @prop {number} volume Period volume value 57 | */ 58 | 59 | /** 60 | * @typedef {Object} Subsession 61 | * @prop {string} id Subsession ID (ex: 'regular') 62 | * @prop {string} description Subsession description (ex: 'Regular') 63 | * @prop {boolean} private If private 64 | * @prop {string} session Session (ex: '24x7') 65 | * @prop {string} session-correction Session correction 66 | * @prop {string} session-display Session display (ex: '24x7') 67 | */ 68 | 69 | /** 70 | * @typedef {Object} MarketInfos 71 | * @prop {string} series_id Used series (ex: 'ser_1') 72 | * @prop {string} base_currency Base currency (ex: 'BTC') 73 | * @prop {string} base_currency_id Base currency ID (ex: 'XTVCBTC') 74 | * @prop {string} name Market short name (ex: 'BTCEUR') 75 | * @prop {string} full_name Market full name (ex: 'COINBASE:BTCEUR') 76 | * @prop {string} pro_name Market pro name (ex: 'COINBASE:BTCEUR') 77 | * @prop {string} description Market symbol description (ex: 'BTC/EUR') 78 | * @prop {string} short_description Market symbol short description (ex: 'BTC/EUR') 79 | * @prop {string} exchange Market exchange (ex: 'COINBASE') 80 | * @prop {string} listed_exchange Market exchange (ex: 'COINBASE') 81 | * @prop {string} provider_id Values provider ID (ex: 'coinbase') 82 | * @prop {string} currency_id Used currency ID (ex: 'EUR') 83 | * @prop {string} currency_code Used currency code (ex: 'EUR') 84 | * @prop {string} variable_tick_size Variable tick size 85 | * @prop {number} pricescale Price scale 86 | * @prop {number} pointvalue Point value 87 | * @prop {string} session Session (ex: '24x7') 88 | * @prop {string} session_display Session display (ex: '24x7') 89 | * @prop {string} type Market type (ex: 'crypto') 90 | * @prop {boolean} has_intraday If intraday values are available 91 | * @prop {boolean} fractional If market is fractional 92 | * @prop {boolean} is_tradable If the market is curently tradable 93 | * @prop {number} minmov Minimum move value 94 | * @prop {number} minmove2 Minimum move value 2 95 | * @prop {string} timezone Used timezone 96 | * @prop {boolean} is_replayable If the replay mode is available 97 | * @prop {boolean} has_adjustment If the adjustment mode is enabled 98 | * @prop {boolean} has_extended_hours Has extended hours 99 | * @prop {string} bar_source Bar source 100 | * @prop {string} bar_transform Bar transform 101 | * @prop {boolean} bar_fillgaps Bar fill gaps 102 | * @prop {string} allowed_adjustment Allowed adjustment (ex: 'none') 103 | * @prop {string} subsession_id Subsession ID (ex: 'regular') 104 | * @prop {string} pro_perm Pro permission (ex: '') 105 | * @prop {[]} base_name Base name (ex: ['COINBASE:BTCEUR']) 106 | * @prop {[]} legs Legs (ex: ['COINBASE:BTCEUR']) 107 | * @prop {Subsession[]} subsessions Sub sessions 108 | * @prop {[]} typespecs Typespecs (ex: []) 109 | * @prop {[]} resolutions Resolutions (ex: []) 110 | * @prop {[]} aliases Aliases (ex: []) 111 | * @prop {[]} alternatives Alternatives (ex: []) 112 | */ 113 | 114 | /** 115 | * @param {import('../client').ClientBridge} client 116 | */ 117 | module.exports = (client) => class ChartSession { 118 | #chartSessionID = genSessionID('cs'); 119 | 120 | #replaySessionID = genSessionID('rs'); 121 | 122 | #replayMode = false; 123 | 124 | /** @type {Object} */ 125 | #replayOKCB = {}; 126 | 127 | /** Parent client */ 128 | #client = client; 129 | 130 | /** @type {StudyListeners} */ 131 | #studyListeners = {}; 132 | 133 | /** 134 | * Table of periods values indexed by timestamp 135 | * @type {Object} 136 | */ 137 | #periods = {}; 138 | 139 | /** @return {PricePeriod[]} List of periods values */ 140 | get periods() { 141 | return Object.values(this.#periods).sort((a, b) => b.time - a.time); 142 | } 143 | 144 | /** 145 | * Current market infos 146 | * @type {MarketInfos} 147 | */ 148 | #infos = {}; 149 | 150 | /** @return {MarketInfos} Current market infos */ 151 | get infos() { 152 | return this.#infos; 153 | } 154 | 155 | #callbacks = { 156 | seriesLoaded: [], 157 | symbolLoaded: [], 158 | update: [], 159 | 160 | replayLoaded: [], 161 | replayPoint: [], 162 | replayResolution: [], 163 | replayEnd: [], 164 | 165 | event: [], 166 | error: [], 167 | }; 168 | 169 | /** 170 | * @param {ChartEvent} ev Client event 171 | * @param {...{}} data Packet data 172 | */ 173 | #handleEvent(ev, ...data) { 174 | this.#callbacks[ev].forEach((e) => e(...data)); 175 | this.#callbacks.event.forEach((e) => e(ev, ...data)); 176 | } 177 | 178 | #handleError(...msgs) { 179 | if (this.#callbacks.error.length === 0) console.error(...msgs); 180 | else this.#handleEvent('error', ...msgs); 181 | } 182 | 183 | constructor() { 184 | this.#client.sessions[this.#chartSessionID] = { 185 | type: 'chart', 186 | onData: (packet) => { 187 | if (global.TW_DEBUG) console.log('§90§30§106 CHART SESSION §0 DATA', packet); 188 | 189 | if (typeof packet.data[1] === 'string' && this.#studyListeners[packet.data[1]]) { 190 | this.#studyListeners[packet.data[1]](packet); 191 | return; 192 | } 193 | 194 | if (packet.type === 'symbol_resolved') { 195 | this.#infos = { 196 | series_id: packet.data[1], 197 | ...packet.data[2], 198 | }; 199 | 200 | this.#handleEvent('symbolLoaded'); 201 | return; 202 | } 203 | 204 | if (['timescale_update', 'du'].includes(packet.type)) { 205 | const changes = []; 206 | 207 | Object.keys(packet.data[1]).forEach((k) => { 208 | changes.push(k); 209 | if (k === '$prices') { 210 | const periods = packet.data[1].$prices; 211 | if (!periods || !periods.s) return; 212 | 213 | periods.s.forEach((p) => { 214 | [this.#chartSession.indexes[p.i]] = p.v; 215 | this.#periods[p.v[0]] = { 216 | time: p.v[0], 217 | open: p.v[1], 218 | close: p.v[4], 219 | max: p.v[2], 220 | min: p.v[3], 221 | volume: Math.round(p.v[5] * 100) / 100, 222 | }; 223 | }); 224 | 225 | return; 226 | } 227 | 228 | if (this.#studyListeners[k]) this.#studyListeners[k](packet); 229 | }); 230 | 231 | this.#handleEvent('update', changes); 232 | return; 233 | } 234 | 235 | if (packet.type === 'symbol_error') { 236 | this.#handleError(`(${packet.data[1]}) Symbol error:`, packet.data[2]); 237 | return; 238 | } 239 | 240 | if (packet.type === 'series_error') { 241 | this.#handleError('Series error:', packet.data[3]); 242 | return; 243 | } 244 | 245 | if (packet.type === 'critical_error') { 246 | const [, name, description] = packet.data; 247 | this.#handleError('Critical error:', name, description); 248 | } 249 | }, 250 | }; 251 | 252 | this.#client.sessions[this.#replaySessionID] = { 253 | type: 'replay', 254 | onData: (packet) => { 255 | if (global.TW_DEBUG) console.log('§90§30§106 REPLAY SESSION §0 DATA', packet); 256 | 257 | if (packet.type === 'replay_ok') { 258 | if (this.#replayOKCB[packet.data[1]]) { 259 | this.#replayOKCB[packet.data[1]](); 260 | delete this.#replayOKCB[packet.data[1]]; 261 | } 262 | return; 263 | } 264 | 265 | if (packet.type === 'replay_instance_id') { 266 | this.#handleEvent('replayLoaded', packet.data[1]); 267 | return; 268 | } 269 | 270 | if (packet.type === 'replay_point') { 271 | this.#handleEvent('replayPoint', packet.data[1]); 272 | return; 273 | } 274 | 275 | if (packet.type === 'replay_resolutions') { 276 | this.#handleEvent('replayResolution', packet.data[1], packet.data[2]); 277 | return; 278 | } 279 | 280 | if (packet.type === 'replay_data_end') { 281 | this.#handleEvent('replayEnd'); 282 | return; 283 | } 284 | 285 | if (packet.type === 'critical_error') { 286 | const [, name, description] = packet.data; 287 | this.#handleError('Critical error:', name, description); 288 | } 289 | }, 290 | }; 291 | 292 | this.#client.send('chart_create_session', [this.#chartSessionID]); 293 | } 294 | 295 | #seriesCreated = false; 296 | 297 | #currentSeries = 0; 298 | 299 | /** 300 | * @param {import('../types').TimeFrame} timeframe Chart period timeframe 301 | * @param {number} [range] Number of loaded periods/candles (Default: 100) 302 | * @param {number} [reference] Reference candle timestamp (Default is now) 303 | */ 304 | setSeries(timeframe = '240', range = 100, reference = null) { 305 | if (!this.#currentSeries) { 306 | this.#handleError('Please set the market before setting series'); 307 | return; 308 | } 309 | 310 | const calcRange = !reference ? range : ['bar_count', reference, range]; 311 | 312 | this.#periods = {}; 313 | 314 | this.#client.send(`${this.#seriesCreated ? 'modify' : 'create'}_series`, [ 315 | this.#chartSessionID, 316 | '$prices', 317 | 's1', 318 | `ser_${this.#currentSeries}`, 319 | timeframe, 320 | this.#seriesCreated ? '' : calcRange, 321 | ]); 322 | 323 | this.#seriesCreated = true; 324 | } 325 | 326 | /** 327 | * Set the chart market 328 | * @param {string} symbol Market symbol 329 | * @param {Object} [options] Chart options 330 | * @param {import('../types').TimeFrame} [options.timeframe] Chart period timeframe 331 | * @param {number} [options.range] Number of loaded periods/candles (Default: 100) 332 | * @param {number} [options.to] Last candle timestamp (Default is now) 333 | * @param {'splits' | 'dividends'} [options.adjustment] Market adjustment 334 | * @param {boolean} [options.backadjustment] Market backadjustment of futures contracts 335 | * @param {'regular' | 'extended'} [options.session] Chart session 336 | * @param {'EUR' | 'USD' | string} [options.currency] Chart currency 337 | * @param {ChartType} [options.type] Chart custom type 338 | * @param {ChartInputs} [options.inputs] Chart custom inputs 339 | * @param {number} [options.replay] Replay mode starting point (Timestamp) 340 | */ 341 | setMarket(symbol, options = {}) { 342 | this.#periods = {}; 343 | 344 | if (this.#replayMode) { 345 | this.#replayMode = false; 346 | this.#client.send('replay_delete_session', [this.#replaySessionID]); 347 | } 348 | 349 | const symbolInit = { 350 | symbol: symbol || 'BTCEUR', 351 | adjustment: options.adjustment || 'splits', 352 | }; 353 | 354 | if (options.backadjustment) symbolInit.backadjustment = 'default'; 355 | if (options.session) symbolInit.session = options.session; 356 | if (options.currency) symbolInit['currency-id'] = options.currency; 357 | 358 | if (options.replay) { 359 | if (!this.#replayMode) { 360 | this.#replayMode = true; 361 | this.#client.send('replay_create_session', [this.#replaySessionID]); 362 | } 363 | 364 | this.#client.send('replay_add_series', [ 365 | this.#replaySessionID, 366 | 'req_replay_addseries', 367 | `=${JSON.stringify(symbolInit)}`, 368 | options.timeframe, 369 | ]); 370 | 371 | this.#client.send('replay_reset', [ 372 | this.#replaySessionID, 373 | 'req_replay_reset', 374 | options.replay, 375 | ]); 376 | } 377 | 378 | const complex = options.type || options.replay; 379 | const chartInit = complex ? {} : symbolInit; 380 | 381 | if (complex) { 382 | if (options.replay) chartInit.replay = this.#replaySessionID; 383 | chartInit.symbol = symbolInit; 384 | chartInit.type = ChartTypes[options.type]; 385 | if (options.type) chartInit.inputs = { ...options.inputs }; 386 | } 387 | 388 | this.#currentSeries += 1; 389 | 390 | this.#client.send('resolve_symbol', [ 391 | this.#chartSessionID, 392 | `ser_${this.#currentSeries}`, 393 | `=${JSON.stringify(chartInit)}`, 394 | ]); 395 | 396 | this.setSeries(options.timeframe, options.range, options.to); 397 | } 398 | 399 | /** 400 | * Set the chart timezone 401 | * @param {import('../types').Timezone} timezone New timezone 402 | */ 403 | setTimezone(timezone) { 404 | this.#periods = {}; 405 | this.#client.send('switch_timezone', [this.#chartSessionID, timezone]); 406 | } 407 | 408 | /** 409 | * Fetch x additional previous periods/candles values 410 | * @param {number} number Number of additional periods/candles you want to fetch 411 | */ 412 | fetchMore(number = 1) { 413 | this.#client.send('request_more_data', [this.#chartSessionID, '$prices', number]); 414 | } 415 | 416 | /** 417 | * Fetch x additional previous periods/candles values 418 | * @param {number} number Number of additional periods/candles you want to fetch 419 | * @returns {Promise} Raise when the data has been fetched 420 | */ 421 | replayStep(number = 1) { 422 | return new Promise((cb) => { 423 | if (!this.#replayMode) { 424 | this.#handleError('No replay session'); 425 | return; 426 | } 427 | 428 | const reqID = genSessionID('rsq_step'); 429 | this.#client.send('replay_step', [this.#replaySessionID, reqID, number]); 430 | this.#replayOKCB[reqID] = () => { cb(); }; 431 | }); 432 | } 433 | 434 | /** 435 | * Start fetching a new period/candle every x ms 436 | * @param {number} interval Number of additional periods/candles you want to fetch 437 | * @returns {Promise} Raise when the replay mode starts 438 | */ 439 | replayStart(interval = 1000) { 440 | return new Promise((cb) => { 441 | if (!this.#replayMode) { 442 | this.#handleError('No replay session'); 443 | return; 444 | } 445 | 446 | const reqID = genSessionID('rsq_start'); 447 | this.#client.send('replay_start', [this.#replaySessionID, reqID, interval]); 448 | this.#replayOKCB[reqID] = () => { cb(); }; 449 | }); 450 | } 451 | 452 | /** 453 | * Stop fetching a new period/candle every x ms 454 | * @returns {Promise} Raise when the replay mode stops 455 | */ 456 | replayStop() { 457 | return new Promise((cb) => { 458 | if (!this.#replayMode) { 459 | this.#handleError('No replay session'); 460 | return; 461 | } 462 | 463 | const reqID = genSessionID('rsq_stop'); 464 | this.#client.send('replay_stop', [this.#replaySessionID, reqID]); 465 | this.#replayOKCB[reqID] = () => { cb(); }; 466 | }); 467 | } 468 | 469 | /** 470 | * When a symbol is loaded 471 | * @param {() => void} cb 472 | * @event 473 | */ 474 | onSymbolLoaded(cb) { 475 | this.#callbacks.symbolLoaded.push(cb); 476 | } 477 | 478 | /** 479 | * When a chart update happens 480 | * @param {(changes: ('$prices' | string)[]) => void} cb 481 | * @event 482 | */ 483 | onUpdate(cb) { 484 | this.#callbacks.update.push(cb); 485 | } 486 | 487 | /** 488 | * When the replay session is ready 489 | * @param {() => void} cb 490 | * @event 491 | */ 492 | onReplayLoaded(cb) { 493 | this.#callbacks.replayLoaded.push(cb); 494 | } 495 | 496 | /** 497 | * When the replay session has new resolution 498 | * @param {( 499 | * timeframe: import('../types').TimeFrame, 500 | * index: number, 501 | * ) => void} cb 502 | * @event 503 | */ 504 | onReplayResolution(cb) { 505 | this.#callbacks.replayResolution.push(cb); 506 | } 507 | 508 | /** 509 | * When the replay session ends 510 | * @param {() => void} cb 511 | * @event 512 | */ 513 | onReplayEnd(cb) { 514 | this.#callbacks.replayEnd.push(cb); 515 | } 516 | 517 | /** 518 | * When the replay session cursor has moved 519 | * @param {(index: number) => void} cb 520 | * @event 521 | */ 522 | onReplayPoint(cb) { 523 | this.#callbacks.replayPoint.push(cb); 524 | } 525 | 526 | /** 527 | * When chart error happens 528 | * @param {(...any) => void} cb Callback 529 | * @event 530 | */ 531 | onError(cb) { 532 | this.#callbacks.error.push(cb); 533 | } 534 | 535 | /** @type {ChartSessionBridge} */ 536 | #chartSession = { 537 | sessionID: this.#chartSessionID, 538 | studyListeners: this.#studyListeners, 539 | indexes: {}, 540 | send: (t, p) => this.#client.send(t, p), 541 | }; 542 | 543 | Study = studyConstructor(this.#chartSession); 544 | 545 | /** Delete the chart session */ 546 | delete() { 547 | if (this.#replayMode) this.#client.send('replay_delete_session', [this.#replaySessionID]); 548 | this.#client.send('chart_delete_session', [this.#chartSessionID]); 549 | delete this.#client.sessions[this.#chartSessionID]; 550 | delete this.#client.sessions[this.#replaySessionID]; 551 | this.#replayMode = false; 552 | } 553 | }; 554 | -------------------------------------------------------------------------------- /src/chart/study.js: -------------------------------------------------------------------------------- 1 | const { genSessionID } = require('../utils'); 2 | const { parseCompressed } = require('../protocol'); 3 | const graphicParser = require('./graphicParser'); 4 | 5 | const PineIndicator = require('../classes/PineIndicator'); 6 | const BuiltInIndicator = require('../classes/BuiltInIndicator'); 7 | 8 | /** 9 | * Get pine inputs 10 | * @param {PineIndicator | BuiltInIndicator} options 11 | */ 12 | function getInputs(options) { 13 | if (options instanceof PineIndicator) { 14 | const pineInputs = { text: options.script }; 15 | 16 | if (options.pineId) pineInputs.pineId = options.pineId; 17 | if (options.pineVersion) pineInputs.pineVersion = options.pineVersion; 18 | 19 | Object.keys(options.inputs).forEach((inputID, n) => { 20 | const input = options.inputs[inputID]; 21 | 22 | pineInputs[inputID] = { 23 | v: (input.type !== 'color') ? input.value : n, 24 | f: input.isFake, 25 | t: input.type, 26 | }; 27 | }); 28 | 29 | return pineInputs; 30 | } 31 | 32 | return options.options; 33 | } 34 | 35 | const parseTrades = (trades) => trades.reverse().map((t) => ({ 36 | entry: { 37 | name: t.e.c, 38 | type: (t.e.tp[0] === 's' ? 'short' : 'long'), 39 | value: t.e.p, 40 | time: t.e.tm, 41 | }, 42 | exit: { 43 | name: t.x.c, 44 | value: t.x.p, 45 | time: t.x.tm, 46 | }, 47 | quantity: t.q, 48 | profit: t.tp, 49 | cumulative: t.cp, 50 | runup: t.rn, 51 | drawdown: t.dd, 52 | })); 53 | 54 | // const historyParser = (history) => history.reverse().map((h) => ({ 55 | 56 | /** 57 | * @typedef {Object} TradeReport Trade report 58 | 59 | * @prop {Object} entry Trade entry 60 | * @prop {string} entry.name Trade name 61 | * @prop {'long' | 'short'} entry.type Entry type (long/short) 62 | * @prop {number} entry.value Entry price value 63 | * @prop {number} entry.time Entry timestamp 64 | 65 | * @prop {Object} exit Trade exit 66 | * @prop {'' | string} exit.name Trade name ('' if false exit) 67 | * @prop {number} exit.value Exit price value 68 | * @prop {number} exit.time Exit timestamp 69 | 70 | * @prop {number} quantity Trade quantity 71 | * @prop {RelAbsValue} profit Trade profit 72 | * @prop {RelAbsValue} cumulative Trade cummulative profit 73 | * @prop {RelAbsValue} runup Trade run-up 74 | * @prop {RelAbsValue} drawdown Trade drawdown 75 | */ 76 | 77 | /** 78 | * @typedef {Object} PerfReport 79 | * @prop {number} avgBarsInTrade Average bars in trade 80 | * @prop {number} avgBarsInWinTrade Average bars in winning trade 81 | * @prop {number} avgBarsInLossTrade Average bars in losing trade 82 | * @prop {number} avgTrade Average trade gain 83 | * @prop {number} avgTradePercent Average trade performace 84 | * @prop {number} avgLosTrade Average losing trade gain 85 | * @prop {number} avgLosTradePercent Average losing trade performace 86 | * @prop {number} avgWinTrade Average winning trade gain 87 | * @prop {number} avgWinTradePercent Average winning trade performace 88 | * @prop {number} commissionPaid Commission paid 89 | * @prop {number} grossLoss Gross loss value 90 | * @prop {number} grossLossPercent Gross loss percent 91 | * @prop {number} grossProfit Gross profit 92 | * @prop {number} grossProfitPercent Gross profit percent 93 | * @prop {number} largestLosTrade Largest losing trade gain 94 | * @prop {number} largestLosTradePercent Largent losing trade performance (percentage) 95 | * @prop {number} largestWinTrade Largest winning trade gain 96 | * @prop {number} largestWinTradePercent Largest winning trade performance (percentage) 97 | * @prop {number} marginCalls Margin calls 98 | * @prop {number} maxContractsHeld Max Contracts Held 99 | * @prop {number} netProfit Net profit 100 | * @prop {number} netProfitPercent Net performance (percentage) 101 | * @prop {number} numberOfLosingTrades Number of losing trades 102 | * @prop {number} numberOfWiningTrades Number of winning trades 103 | * @prop {number} percentProfitable Strategy winrate 104 | * @prop {number} profitFactor Profit factor 105 | * @prop {number} ratioAvgWinAvgLoss Ratio Average Win / Average Loss 106 | * @prop {number} totalOpenTrades Total open trades 107 | * @prop {number} totalTrades Total trades 108 | */ 109 | 110 | /** 111 | * @typedef {Object} FromTo 112 | * @prop {number} from From timestamp 113 | * @prop {number} to To timestamp 114 | */ 115 | 116 | /** 117 | * @typedef {Object} StrategyReport 118 | * @prop {'EUR' | 'USD' | 'JPY' | '' | 'CHF'} [currency] Selected currency 119 | * @prop {Object} [settings] Backtester settings 120 | * @prop {Object} [settings.dateRange] Backtester date range 121 | * @prop {FromTo} [settings.dateRange.backtest] Date range for backtest 122 | * @prop {FromTo} [settings.dateRange.trade] Date range for trade 123 | * @prop {TradeReport[]} trades Trade list starting by the last 124 | * @prop {Object} history History Chart value 125 | * @prop {number[]} [history.buyHold] Buy hold values 126 | * @prop {number[]} [history.buyHoldPercent] Buy hold percent values 127 | * @prop {number[]} [history.drawDown] Drawdown values 128 | * @prop {number[]} [history.drawDownPercent] Drawdown percent values 129 | * @prop {number[]} [history.equity] Equity values 130 | * @prop {number[]} [history.equityPercent] Equity percent values 131 | * @prop {Object} performance Strategy performance 132 | * @prop {PerfReport} [performance.all] Strategy long/short performances 133 | * @prop {PerfReport} [performance.long] Strategy long performances 134 | * @prop {PerfReport} [performance.short] Strategy short performances 135 | * @prop {number} [performance.buyHoldReturn] Strategy Buy & Hold Return 136 | * @prop {number} [performance.buyHoldReturnPercent] Strategy Buy & Hold Return percent 137 | * @prop {number} [performance.maxDrawDown] Strategy max drawdown 138 | * @prop {number} [performance.maxDrawDownPercent] Strategy max drawdown percent 139 | * @prop {number} [performance.openPL] Strategy Open P&L (Profit And Loss) 140 | * @prop {number} [performance.openPLPercent] Strategy Open P&L (Profit And Loss) percent 141 | * @prop {number} [performance.sharpeRatio] Strategy Sharpe Ratio 142 | * @prop {number} [performance.sortinoRatio] Strategy Sortino Ratio 143 | */ 144 | 145 | /** 146 | * @param {import('./session').ChartSessionBridge} chartSession 147 | */ 148 | module.exports = (chartSession) => class ChartStudy { 149 | #studID = genSessionID('st'); 150 | 151 | #studyListeners = chartSession.studyListeners; 152 | 153 | /** 154 | * Table of periods values indexed by timestamp 155 | * @type {Object} 156 | */ 157 | #periods = {}; 158 | 159 | /** @return {{}[]} List of periods values */ 160 | get periods() { 161 | return Object.values(this.#periods).sort((a, b) => b.$time - a.$time); 162 | } 163 | 164 | /** 165 | * List of graphic xPos indexes 166 | * @type {number[]} 167 | */ 168 | #indexes = []; 169 | 170 | /** 171 | * Table of graphic drawings indexed by type and ID 172 | * @type {Object>} 173 | */ 174 | #graphic = {}; 175 | 176 | /** 177 | * Table of graphic drawings indexed by type 178 | * @return {import('./graphicParser').GraphicData} 179 | */ 180 | get graphic() { 181 | const translator = {}; 182 | 183 | Object.keys(chartSession.indexes) 184 | .sort((a, b) => chartSession.indexes[b] - chartSession.indexes[a]) 185 | .forEach((r, n) => { translator[r] = n; }); 186 | 187 | const indexes = this.#indexes.map((i) => translator[i]); 188 | return graphicParser(this.#graphic, indexes); 189 | } 190 | 191 | /** @type {StrategyReport} */ 192 | #strategyReport = { 193 | trades: [], 194 | history: {}, 195 | performance: {}, 196 | }; 197 | 198 | /** @return {StrategyReport} Get the strategy report if available */ 199 | get strategyReport() { 200 | return this.#strategyReport; 201 | } 202 | 203 | #callbacks = { 204 | studyCompleted: [], 205 | update: [], 206 | 207 | event: [], 208 | error: [], 209 | }; 210 | 211 | /** 212 | * @param {ChartEvent} ev Client event 213 | * @param {...{}} data Packet data 214 | */ 215 | #handleEvent(ev, ...data) { 216 | this.#callbacks[ev].forEach((e) => e(...data)); 217 | this.#callbacks.event.forEach((e) => e(ev, ...data)); 218 | } 219 | 220 | #handleError(...msgs) { 221 | if (this.#callbacks.error.length === 0) console.error(...msgs); 222 | else this.#handleEvent('error', ...msgs); 223 | } 224 | 225 | /** 226 | * @param {PineIndicator | BuiltInIndicator} indicator Indicator object instance 227 | */ 228 | constructor(indicator) { 229 | if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) { 230 | throw new Error(`Indicator argument must be an instance of PineIndicator or BuiltInIndicator. 231 | Please use 'TradingView.getIndicator(...)' function.`); 232 | } 233 | 234 | /** @type {PineIndicator | BuiltInIndicator} Indicator instance */ 235 | this.instance = indicator; 236 | 237 | this.#studyListeners[this.#studID] = async (packet) => { 238 | if (global.TW_DEBUG) console.log('§90§30§105 STUDY §0 DATA', packet); 239 | 240 | if (packet.type === 'study_completed') { 241 | this.#handleEvent('studyCompleted'); 242 | return; 243 | } 244 | 245 | if (['timescale_update', 'du'].includes(packet.type)) { 246 | const changes = []; 247 | const data = packet.data[1][this.#studID]; 248 | 249 | if (data && data.st && data.st[0]) { 250 | data.st.forEach((p) => { 251 | const period = {}; 252 | 253 | p.v.forEach((plot, i) => { 254 | if (!this.instance.plots) { 255 | period[i === 0 ? '$time' : `plot_${i - 1}`] = plot; 256 | return; 257 | } 258 | const plotName = (i === 0 ? '$time' : this.instance.plots[`plot_${i - 1}`]); 259 | if (plotName && !period[plotName]) period[plotName] = plot; 260 | else period[`plot_${i - 1}`] = plot; 261 | }); 262 | 263 | this.#periods[p.v[0]] = period; 264 | }); 265 | 266 | changes.push('plots'); 267 | } 268 | 269 | if (data.ns && data.ns.d) { 270 | const parsed = JSON.parse(data.ns.d); 271 | 272 | if (parsed.graphicsCmds) { 273 | if (parsed.graphicsCmds.erase) { 274 | parsed.graphicsCmds.erase.forEach((instruction) => { 275 | // console.log('Erase', instruction); 276 | if (instruction.action === 'all') { 277 | if (!instruction.type) { 278 | Object.keys(this.#graphic).forEach((drawType) => { 279 | this.#graphic[drawType] = {}; 280 | }); 281 | } else delete this.#graphic[instruction.type]; 282 | return; 283 | } 284 | 285 | if (instruction.action === 'one') { 286 | delete this.#graphic[instruction.type][instruction.id]; 287 | } 288 | // Can an 'instruction' contains other things ? 289 | }); 290 | } 291 | 292 | if (parsed.graphicsCmds.create) { 293 | Object.keys(parsed.graphicsCmds.create).forEach((drawType) => { 294 | if (!this.#graphic[drawType]) this.#graphic[drawType] = {}; 295 | parsed.graphicsCmds.create[drawType].forEach((group) => { 296 | group.data.forEach((item) => { 297 | this.#graphic[drawType][item.id] = item; 298 | }); 299 | }); 300 | }); 301 | } 302 | 303 | // console.log('graphicsCmds', Object.keys(parsed.graphicsCmds)); 304 | // Can 'graphicsCmds' contains other things ? 305 | 306 | changes.push('graphic'); 307 | } 308 | 309 | const updateStrategyReport = (report) => { 310 | if (report.currency) { 311 | this.#strategyReport.currency = report.currency; 312 | changes.push('report.currency'); 313 | } 314 | 315 | if (report.settings) { 316 | this.#strategyReport.settings = report.settings; 317 | changes.push('report.settings'); 318 | } 319 | 320 | if (report.performance) { 321 | this.#strategyReport.performance = report.performance; 322 | changes.push('report.perf'); 323 | } 324 | 325 | if (report.trades) { 326 | this.#strategyReport.trades = parseTrades(report.trades); 327 | changes.push('report.trades'); 328 | } 329 | 330 | if (report.equity) { 331 | this.#strategyReport.history = { 332 | buyHold: report.buyHold, 333 | buyHoldPercent: report.buyHoldPercent, 334 | drawDown: report.drawDown, 335 | drawDownPercent: report.drawDownPercent, 336 | equity: report.equity, 337 | equityPercent: report.equityPercent, 338 | }; 339 | changes.push('report.history'); 340 | } 341 | }; 342 | 343 | if (parsed.dataCompressed) { 344 | updateStrategyReport((await parseCompressed(parsed.dataCompressed)).report); 345 | } 346 | 347 | if (parsed.data && parsed.data.report) updateStrategyReport(parsed.data.report); 348 | } 349 | 350 | if (data.ns.indexes && typeof data.ns.indexes === 'object') { 351 | this.#indexes = data.ns.indexes; 352 | } 353 | 354 | this.#handleEvent('update', changes); 355 | return; 356 | } 357 | 358 | if (packet.type === 'study_error') { 359 | this.#handleError(packet.data[3], packet.data[4]); 360 | } 361 | }; 362 | 363 | chartSession.send('create_study', [ 364 | chartSession.sessionID, 365 | `${this.#studID}`, 366 | 'st1', 367 | '$prices', 368 | this.instance.type, 369 | getInputs(this.instance), 370 | ]); 371 | } 372 | 373 | /** 374 | * @param {PineIndicator | BuiltInIndicator} indicator Indicator instance 375 | */ 376 | setIndicator(indicator) { 377 | if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) { 378 | throw new Error(`Indicator argument must be an instance of PineIndicator or BuiltInIndicator. 379 | Please use 'TradingView.getIndicator(...)' function.`); 380 | } 381 | 382 | this.instance = indicator; 383 | 384 | chartSession.send('modify_study', [ 385 | chartSession.sessionID, 386 | `${this.#studID}`, 387 | 'st1', 388 | getInputs(this.instance), 389 | ]); 390 | } 391 | 392 | /** 393 | * When the indicator is ready 394 | * @param {() => void} cb 395 | * @event 396 | */ 397 | onReady(cb) { 398 | this.#callbacks.studyCompleted.push(cb); 399 | } 400 | 401 | /** 402 | * @typedef {'plots' | 'report.currency' 403 | * | 'report.settings' | 'report.perf' 404 | * | 'report.trades' | 'report.history' 405 | * | 'graphic' 406 | * } UpdateChangeType 407 | */ 408 | 409 | /** 410 | * When an indicator update happens 411 | * @param {(changes: UpdateChangeType[]) => void} cb 412 | * @event 413 | */ 414 | onUpdate(cb) { 415 | this.#callbacks.update.push(cb); 416 | } 417 | 418 | /** 419 | * When indicator error happens 420 | * @param {(...any) => void} cb Callback 421 | * @event 422 | */ 423 | onError(cb) { 424 | this.#callbacks.error.push(cb); 425 | } 426 | 427 | /** Remove the study */ 428 | remove() { 429 | chartSession.send('remove_study', [ 430 | chartSession.sessionID, 431 | this.#studID, 432 | ]); 433 | delete this.#studyListeners[this.#studID]; 434 | } 435 | }; 436 | -------------------------------------------------------------------------------- /src/classes/BuiltInIndicator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {'Volume@tv-basicstudies-241' 3 | * | 'VbPFixed@tv-basicstudies-241' 4 | * | 'VbPFixed@tv-basicstudies-241!' 5 | * | 'VbPFixed@tv-volumebyprice-53!' 6 | * | 'VbPSessions@tv-volumebyprice-53' 7 | * | 'VbPSessionsRough@tv-volumebyprice-53!' 8 | * | 'VbPSessionsDetailed@tv-volumebyprice-53!' 9 | * | 'VbPVisible@tv-volumebyprice-53'} BuiltInIndicatorType Built-in indicator type 10 | */ 11 | 12 | /** 13 | * @typedef {'rowsLayout' | 'rows' | 'volume' 14 | * | 'vaVolume' | 'subscribeRealtime' 15 | * | 'first_bar_time' | 'first_visible_bar_time' 16 | * | 'last_bar_time' | 'last_visible_bar_time' 17 | * | 'extendPocRight'} BuiltInIndicatorOption Built-in indicator Option 18 | */ 19 | 20 | const defaultValues = { 21 | 'Volume@tv-basicstudies-241': { 22 | length: 20, 23 | col_prev_close: false, 24 | }, 25 | 'VbPFixed@tv-basicstudies-241': { 26 | rowsLayout: 'Number Of Rows', 27 | rows: 24, 28 | volume: 'Up/Down', 29 | vaVolume: 70, 30 | subscribeRealtime: false, 31 | first_bar_time: NaN, 32 | last_bar_time: Date.now(), 33 | extendToRight: false, 34 | mapRightBoundaryToBarStartTime: true, 35 | }, 36 | 'VbPFixed@tv-basicstudies-241!': { 37 | rowsLayout: 'Number Of Rows', 38 | rows: 24, 39 | volume: 'Up/Down', 40 | vaVolume: 70, 41 | subscribeRealtime: false, 42 | first_bar_time: NaN, 43 | last_bar_time: Date.now(), 44 | }, 45 | 'VbPFixed@tv-volumebyprice-53!': { 46 | rowsLayout: 'Number Of Rows', 47 | rows: 24, 48 | volume: 'Up/Down', 49 | vaVolume: 70, 50 | subscribeRealtime: false, 51 | first_bar_time: NaN, 52 | last_bar_time: Date.now(), 53 | }, 54 | 'VbPSessions@tv-volumebyprice-53': { 55 | rowsLayout: 'Number Of Rows', 56 | rows: 24, 57 | volume: 'Up/Down', 58 | vaVolume: 70, 59 | extendPocRight: false, 60 | }, 61 | 'VbPSessionsRough@tv-volumebyprice-53!': { 62 | volume: 'Up/Down', 63 | vaVolume: 70, 64 | }, 65 | 'VbPSessionsDetailed@tv-volumebyprice-53!': { 66 | volume: 'Up/Down', 67 | vaVolume: 70, 68 | subscribeRealtime: false, 69 | first_visible_bar_time: NaN, 70 | last_visible_bar_time: Date.now(), 71 | }, 72 | 'VbPVisible@tv-volumebyprice-53': { 73 | rowsLayout: 'Number Of Rows', 74 | rows: 24, 75 | volume: 'Up/Down', 76 | vaVolume: 70, 77 | subscribeRealtime: false, 78 | first_visible_bar_time: NaN, 79 | last_visible_bar_time: Date.now(), 80 | }, 81 | }; 82 | 83 | /** @class */ 84 | module.exports = class BuiltInIndicator { 85 | /** @type {BuiltInIndicatorType} */ 86 | #type; 87 | 88 | /** @return {BuiltInIndicatorType} Indicator script */ 89 | get type() { 90 | return this.#type; 91 | } 92 | 93 | /** @type {Object} */ 94 | #options = {}; 95 | 96 | /** @return {Object} Indicator script */ 97 | get options() { 98 | return this.#options; 99 | } 100 | 101 | /** 102 | * @param {BuiltInIndicatorType} type Buit-in indocator raw type 103 | */ 104 | constructor(type = '') { 105 | if (!type) throw new Error(`Wrong buit-in indicator type "${type}".`); 106 | 107 | this.#type = type; 108 | if (defaultValues[type]) this.#options = { ...defaultValues[type] }; 109 | } 110 | 111 | /** 112 | * Set an option 113 | * @param {BuiltInIndicatorOption} key The option you want to change 114 | * @param {*} value The new value of the property 115 | * @param {boolean} FORCE Ignore type and key verifications 116 | */ 117 | setOption(key, value, FORCE = false) { 118 | if (FORCE) { 119 | this.#options[key] = value; 120 | return; 121 | } 122 | 123 | if (defaultValues[this.#type] && defaultValues[this.#type][key] !== undefined) { 124 | const requiredType = typeof defaultValues[this.#type][key]; 125 | const valType = typeof value; 126 | if (requiredType !== valType) { 127 | throw new Error(`Wrong '${key}' value type '${valType}' (must be '${requiredType}')`); 128 | } 129 | } 130 | 131 | if (defaultValues[this.#type] && defaultValues[this.#type][key] === undefined) { 132 | throw new Error(`Option '${key}' is denied with '${this.#type}' indicator`); 133 | } 134 | 135 | this.#options[key] = value; 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /src/classes/PineIndicator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} IndicatorInput 3 | * @property {string} name Input name 4 | * @property {string} inline Input inline name 5 | * @property {string} [internalID] Input internal ID 6 | * @property {string} [tooltip] Input tooltip 7 | * @property {'text' | 'source' | 'integer' 8 | * | 'float' | 'resolution' | 'bool' | 'color' 9 | * } type Input type 10 | * @property {string | number | boolean} value Input default value 11 | * @property {boolean} isHidden If the input is hidden 12 | * @property {boolean} isFake If the input is fake 13 | * @property {string[]} [options] Input options if the input is a select 14 | */ 15 | 16 | /** 17 | * @typedef {Object} Indicator 18 | * @property {string} pineId Indicator ID 19 | * @property {string} pineVersion Indicator version 20 | * @property {string} description Indicator description 21 | * @property {string} shortDescription Indicator short description 22 | * @property {Object} inputs Indicator inputs 23 | * @property {Object} plots Indicator plots 24 | * @property {string} script Indicator script 25 | */ 26 | 27 | /** 28 | * @typedef {'Script@tv-scripting-101!' 29 | * | 'StrategyScript@tv-scripting-101!'} IndicatorType Indicator type 30 | */ 31 | 32 | /** @class */ 33 | module.exports = class PineIndicator { 34 | #options; 35 | 36 | /** @type {IndicatorType} */ 37 | #type = 'Script@tv-scripting-101!'; 38 | 39 | /** @param {Indicator} options Indicator */ 40 | constructor(options) { 41 | this.#options = options; 42 | } 43 | 44 | /** @return {string} Indicator ID */ 45 | get pineId() { 46 | return this.#options.pineId; 47 | } 48 | 49 | /** @return {string} Indicator version */ 50 | get pineVersion() { 51 | return this.#options.pineVersion; 52 | } 53 | 54 | /** @return {string} Indicator description */ 55 | get description() { 56 | return this.#options.description; 57 | } 58 | 59 | /** @return {string} Indicator short description */ 60 | get shortDescription() { 61 | return this.#options.shortDescription; 62 | } 63 | 64 | /** @return {Object} Indicator inputs */ 65 | get inputs() { 66 | return this.#options.inputs; 67 | } 68 | 69 | /** @return {Object} Indicator plots */ 70 | get plots() { 71 | return this.#options.plots; 72 | } 73 | 74 | /** @return {IndicatorType} Indicator script */ 75 | get type() { 76 | return this.#type; 77 | } 78 | 79 | /** 80 | * Set the indicator type 81 | * @param {IndicatorType} type Indicator type 82 | */ 83 | setType(type = 'Script@tv-scripting-101!') { 84 | this.#type = type; 85 | } 86 | 87 | /** @return {string} Indicator script */ 88 | get script() { 89 | return this.#options.script; 90 | } 91 | 92 | /** 93 | * Set an option 94 | * @param {number | string} key The key can be ID of the property (`in_{ID}`), 95 | * the inline name or the internalID. 96 | * @param {*} value The new value of the property 97 | */ 98 | setOption(key, value) { 99 | let propI = ''; 100 | 101 | if (this.#options.inputs[`in_${key}`]) propI = `in_${key}`; 102 | else if (this.#options.inputs[key]) propI = key; 103 | else { 104 | propI = Object.keys(this.#options.inputs).find((I) => ( 105 | this.#options.inputs[I].inline === key 106 | || this.#options.inputs[I].internalID === key 107 | )); 108 | } 109 | 110 | if (propI && this.#options.inputs[propI]) { 111 | const input = this.#options.inputs[propI]; 112 | 113 | const types = { 114 | bool: 'Boolean', 115 | integer: 'Number', 116 | float: 'Number', 117 | text: 'String', 118 | }; 119 | 120 | // eslint-disable-next-line valid-typeof 121 | if (types[input.type] && typeof value !== types[input.type].toLowerCase()) { 122 | throw new Error(`Input '${input.name}' (${propI}) must be a ${types[input.type]} !`); 123 | } 124 | 125 | if (input.options && !input.options.includes(value)) { 126 | throw new Error(`Input '${input.name}' (${propI}) must be one of these values:`, input.options); 127 | } 128 | 129 | input.value = value; 130 | } else throw new Error(`Input '${key}' not found (${propI}).`); 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /src/classes/PinePermManager.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { genAuthCookies } = require('../utils'); 3 | 4 | /** 5 | * @typedef {Object} AuthorizationUser 6 | * @prop {id} id User id 7 | * @prop {string} username User's username 8 | * @prop {string} userpic User's profile picture URL 9 | * @prop {string} expiration Authorization expiration date 10 | * @prop {string} created Authorization creation date 11 | */ 12 | 13 | /** @class */ 14 | class PinePermManager { 15 | sessionId; 16 | 17 | pineId; 18 | 19 | /** 20 | * Creates a PinePermManager instance 21 | * @param {string} sessionId Token from `sessionid` cookie 22 | * @param {string} signature Signature cookie 23 | * @param {string} pineId Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX) 24 | */ 25 | constructor(sessionId, signature, pineId) { 26 | if (!sessionId) throw new Error('Please provide a SessionID'); 27 | if (!signature) throw new Error('Please provide a Signature'); 28 | if (!pineId) throw new Error('Please provide a PineID'); 29 | this.sessionId = sessionId; 30 | this.signature = signature; 31 | this.pineId = pineId; 32 | } 33 | 34 | /** 35 | * Get list of authorized users 36 | * @param {number} limit Fetching limit 37 | * @param {'user__username' 38 | * | '-user__username' 39 | * | 'created' | 'created' 40 | * | 'expiration,user__username' 41 | * | '-expiration,user__username' 42 | * } order Fetching order 43 | * @returns {Promise} 44 | */ 45 | async getUsers(limit = 10, order = '-created') { 46 | try { 47 | const { data } = await axios.post( 48 | `https://www.tradingview.com/pine_perm/list_users/?limit=${limit}&order_by=${order}`, 49 | `pine_id=${this.pineId.replace(/;/g, '%3B')}`, 50 | { 51 | headers: { 52 | origin: 'https://www.tradingview.com', 53 | 'Content-Type': 'application/x-www-form-urlencoded', 54 | cookie: genAuthCookies(this.sessionId, this.signature), 55 | }, 56 | }, 57 | ); 58 | 59 | return data.results; 60 | } catch (e) { 61 | throw new Error(e.response.data.detail || 'Wrong credentials or pineId'); 62 | } 63 | } 64 | 65 | /** 66 | * Adds an user to the authorized list 67 | * @param {string} username User's username 68 | * @param {Date} [expiration] Expiration date 69 | * @returns {Promise<'ok' | 'exists' | null>} 70 | */ 71 | async addUser(username, expiration = null) { 72 | try { 73 | const { data } = await axios.post( 74 | 'https://www.tradingview.com/pine_perm/add/', 75 | `pine_id=${ 76 | this.pineId.replace(/;/g, '%3B') 77 | }&username_recip=${ 78 | username 79 | }${ 80 | expiration && expiration instanceof Date 81 | ? `&expiration=${expiration.toISOString()}` 82 | : '' 83 | }`, 84 | { 85 | headers: { 86 | origin: 'https://www.tradingview.com', 87 | 'Content-Type': 'application/x-www-form-urlencoded', 88 | cookie: genAuthCookies(this.sessionId, this.signature), 89 | }, 90 | }, 91 | ); 92 | 93 | return data.status; 94 | } catch (e) { 95 | throw new Error(e.response.data.detail || 'Wrong credentials or pineId'); 96 | } 97 | } 98 | 99 | /** 100 | * Modify an authorization expiration date 101 | * @param {string} username User's username 102 | * @param {Date} [expiration] New expiration date 103 | * @returns {Promise<'ok' | null>} 104 | */ 105 | async modifyExpiration(username, expiration = null) { 106 | try { 107 | const { data } = await axios.post( 108 | 'https://www.tradingview.com/pine_perm/modify_user_expiration/', 109 | `pine_id=${ 110 | this.pineId.replace(/;/g, '%3B') 111 | }&username_recip=${ 112 | username 113 | }${ 114 | expiration && expiration instanceof Date 115 | ? `&expiration=${expiration.toISOString()}` 116 | : '' 117 | }`, 118 | { 119 | headers: { 120 | origin: 'https://www.tradingview.com', 121 | 'Content-Type': 'application/x-www-form-urlencoded', 122 | cookie: genAuthCookies(this.sessionId, this.signature), 123 | }, 124 | }, 125 | ); 126 | 127 | return data.status; 128 | } catch (e) { 129 | throw new Error(e.response.data.detail || 'Wrong credentials or pineId'); 130 | } 131 | } 132 | 133 | /** 134 | * Removes an user to the authorized list 135 | * @param {string} username User's username 136 | * @returns {Promise<'ok' | null>} 137 | */ 138 | async removeUser(username) { 139 | try { 140 | const { data } = await axios.post( 141 | 'https://www.tradingview.com/pine_perm/remove/', 142 | `pine_id=${this.pineId.replace(/;/g, '%3B')}&username_recip=${username}`, 143 | { 144 | headers: { 145 | origin: 'https://www.tradingview.com', 146 | 'Content-Type': 'application/x-www-form-urlencoded', 147 | cookie: genAuthCookies(this.sessionId, this.signature), 148 | }, 149 | }, 150 | ); 151 | 152 | return data.status; 153 | } catch (e) { 154 | throw new Error(e.response.data.detail || 'Wrong credentials or pineId'); 155 | } 156 | } 157 | } 158 | 159 | module.exports = PinePermManager; 160 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | 3 | const misc = require('./miscRequests'); 4 | const protocol = require('./protocol'); 5 | 6 | const quoteSessionGenerator = require('./quote/session'); 7 | const chartSessionGenerator = require('./chart/session'); 8 | 9 | /** 10 | * @typedef {Object} Session 11 | * @prop {'quote' | 'chart' | 'replay'} type Session type 12 | * @prop {(data: {}) => null} onData When there is a data 13 | */ 14 | 15 | /** @typedef {Object} SessionList Session list */ 16 | 17 | /** 18 | * @callback SendPacket Send a custom packet 19 | * @param {string} t Packet type 20 | * @param {string[]} p Packet data 21 | * @returns {void} 22 | */ 23 | 24 | /** 25 | * @typedef {Object} ClientBridge 26 | * @prop {SessionList} sessions 27 | * @prop {SendPacket} send 28 | */ 29 | 30 | /** 31 | * @typedef { 'connected' | 'disconnected' 32 | * | 'logged' | 'ping' | 'data' 33 | * | 'error' | 'event' 34 | * } ClientEvent 35 | */ 36 | 37 | /** @class */ 38 | module.exports = class Client { 39 | #ws; 40 | 41 | #logged = false; 42 | 43 | /** If the client is logged in */ 44 | get isLogged() { 45 | return this.#logged; 46 | } 47 | 48 | /** If the cient was closed */ 49 | get isOpen() { 50 | return this.#ws.readyState === this.#ws.OPEN; 51 | } 52 | 53 | /** @type {SessionList} */ 54 | #sessions = {}; 55 | 56 | #callbacks = { 57 | connected: [], 58 | disconnected: [], 59 | logged: [], 60 | ping: [], 61 | data: [], 62 | 63 | error: [], 64 | event: [], 65 | }; 66 | 67 | /** 68 | * @param {ClientEvent} ev Client event 69 | * @param {...{}} data Packet data 70 | */ 71 | #handleEvent(ev, ...data) { 72 | this.#callbacks[ev].forEach((e) => e(...data)); 73 | this.#callbacks.event.forEach((e) => e(ev, ...data)); 74 | } 75 | 76 | #handleError(...msgs) { 77 | if (this.#callbacks.error.length === 0) console.error(...msgs); 78 | else this.#handleEvent('error', ...msgs); 79 | } 80 | 81 | /** 82 | * When client is connected 83 | * @param {() => void} cb Callback 84 | * @event onConnected 85 | */ 86 | onConnected(cb) { 87 | this.#callbacks.connected.push(cb); 88 | } 89 | 90 | /** 91 | * When client is disconnected 92 | * @param {() => void} cb Callback 93 | * @event onDisconnected 94 | */ 95 | onDisconnected(cb) { 96 | this.#callbacks.disconnected.push(cb); 97 | } 98 | 99 | /** 100 | * @typedef {Object} SocketSession 101 | * @prop {string} session_id Socket session ID 102 | * @prop {number} timestamp Session start timestamp 103 | * @prop {number} timestampMs Session start milliseconds timestamp 104 | * @prop {string} release Release 105 | * @prop {string} studies_metadata_hash Studies metadata hash 106 | * @prop {'json' | string} protocol Used protocol 107 | * @prop {string} javastudies Javastudies 108 | * @prop {number} auth_scheme_vsn Auth scheme type 109 | * @prop {string} via Socket IP 110 | */ 111 | 112 | /** 113 | * When client is logged 114 | * @param {(SocketSession: SocketSession) => void} cb Callback 115 | * @event onLogged 116 | */ 117 | onLogged(cb) { 118 | this.#callbacks.logged.push(cb); 119 | } 120 | 121 | /** 122 | * When server is pinging the client 123 | * @param {(i: number) => void} cb Callback 124 | * @event onPing 125 | */ 126 | onPing(cb) { 127 | this.#callbacks.ping.push(cb); 128 | } 129 | 130 | /** 131 | * When unparsed data is received 132 | * @param {(...{}) => void} cb Callback 133 | * @event onData 134 | */ 135 | onData(cb) { 136 | this.#callbacks.data.push(cb); 137 | } 138 | 139 | /** 140 | * When a client error happens 141 | * @param {(...{}) => void} cb Callback 142 | * @event onError 143 | */ 144 | onError(cb) { 145 | this.#callbacks.error.push(cb); 146 | } 147 | 148 | /** 149 | * When a client event happens 150 | * @param {(...{}) => void} cb Callback 151 | * @event onEvent 152 | */ 153 | onEvent(cb) { 154 | this.#callbacks.event.push(cb); 155 | } 156 | 157 | #parsePacket(str) { 158 | if (!this.isOpen) return; 159 | 160 | protocol.parseWSPacket(str).forEach((packet) => { 161 | if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet); 162 | if (typeof packet === 'number') { // Ping 163 | this.#ws.send(protocol.formatWSPacket(`~h~${packet}`)); 164 | this.#handleEvent('ping', packet); 165 | return; 166 | } 167 | 168 | if (packet.m === 'protocol_error') { // Error 169 | this.#handleError('Client critical error:', packet.p); 170 | this.#ws.close(); 171 | return; 172 | } 173 | 174 | if (packet.m && packet.p) { // Normal packet 175 | const parsed = { 176 | type: packet.m, 177 | data: packet.p, 178 | }; 179 | 180 | const session = packet.p[0]; 181 | 182 | if (session && this.#sessions[session]) { 183 | this.#sessions[session].onData(parsed); 184 | return; 185 | } 186 | } 187 | 188 | if (!this.#logged) { 189 | this.#handleEvent('logged', packet); 190 | return; 191 | } 192 | 193 | this.#handleEvent('data', packet); 194 | }); 195 | } 196 | 197 | #sendQueue = []; 198 | 199 | /** @type {SendPacket} Send a custom packet */ 200 | send(t, p = []) { 201 | this.#sendQueue.push(protocol.formatWSPacket({ m: t, p })); 202 | this.sendQueue(); 203 | } 204 | 205 | /** Send all waiting packets */ 206 | sendQueue() { 207 | while (this.isOpen && this.#logged && this.#sendQueue.length > 0) { 208 | const packet = this.#sendQueue.shift(); 209 | this.#ws.send(packet); 210 | if (global.TW_DEBUG) console.log('§90§30§107 > §0', packet); 211 | } 212 | } 213 | 214 | /** 215 | * @typedef {Object} ClientOptions 216 | * @prop {string} [token] User auth token (in 'sessionid' cookie) 217 | * @prop {string} [signature] User auth token signature (in 'sessionid_sign' cookie) 218 | * @prop {boolean} [DEBUG] Enable debug mode 219 | * @prop {'data' | 'prodata' | 'widgetdata'} [server] Server type 220 | * @prop {string} [location] Auth page location (For france: https://fr.tradingview.com/) 221 | */ 222 | 223 | /** 224 | * Client object 225 | * @param {ClientOptions} clientOptions TradingView client options 226 | */ 227 | constructor(clientOptions = {}) { 228 | if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG; 229 | 230 | const server = clientOptions.server || 'data'; 231 | this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?type=chart`, { 232 | origin: 'https://www.tradingview.com', 233 | }); 234 | 235 | if (clientOptions.token) { 236 | misc.getUser( 237 | clientOptions.token, 238 | clientOptions.signature ? clientOptions.signature : '', 239 | clientOptions.location ? clientOptions.location : 'https://tradingview.com', 240 | ).then((user) => { 241 | this.#sendQueue.unshift(protocol.formatWSPacket({ 242 | m: 'set_auth_token', 243 | p: [user.authToken], 244 | })); 245 | this.#logged = true; 246 | this.sendQueue(); 247 | }).catch((err) => { 248 | this.#handleError('Credentials error:', err.message); 249 | }); 250 | } else { 251 | this.#sendQueue.unshift(protocol.formatWSPacket({ 252 | m: 'set_auth_token', 253 | p: ['unauthorized_user_token'], 254 | })); 255 | this.#logged = true; 256 | this.sendQueue(); 257 | } 258 | 259 | this.#ws.on('open', () => { 260 | this.#handleEvent('connected'); 261 | this.sendQueue(); 262 | }); 263 | 264 | this.#ws.on('close', () => { 265 | this.#logged = false; 266 | this.#handleEvent('disconnected'); 267 | }); 268 | 269 | this.#ws.on('error', (err) => { 270 | this.#handleError('WebSocket error:', err.message); 271 | }); 272 | 273 | this.#ws.on('message', (data) => this.#parsePacket(data)); 274 | } 275 | 276 | /** @type {ClientBridge} */ 277 | #clientBridge = { 278 | sessions: this.#sessions, 279 | send: (t, p) => this.send(t, p), 280 | }; 281 | 282 | /** @namespace Session */ 283 | Session = { 284 | Quote: quoteSessionGenerator(this.#clientBridge), 285 | Chart: chartSessionGenerator(this.#clientBridge), 286 | }; 287 | 288 | /** 289 | * Close the websocket connection 290 | * @return {Promise} When websocket is closed 291 | */ 292 | end() { 293 | return new Promise((cb) => { 294 | if (this.#ws.readyState) this.#ws.close(); 295 | cb(); 296 | }); 297 | } 298 | }; 299 | -------------------------------------------------------------------------------- /src/miscRequests.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const axios = require('axios'); 3 | 4 | const PineIndicator = require('./classes/PineIndicator'); 5 | const { genAuthCookies } = require('./utils'); 6 | 7 | const validateStatus = (status) => status < 500; 8 | 9 | const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA']; 10 | const builtInIndicList = []; 11 | 12 | async function fetchScanData(tickers = [], columns = []) { 13 | const { data } = await axios.post( 14 | 'https://scanner.tradingview.com/global/scan', 15 | { 16 | symbols: { tickers }, 17 | columns, 18 | }, 19 | { validateStatus }, 20 | ); 21 | 22 | return data; 23 | } 24 | 25 | /** @typedef {number} advice */ 26 | 27 | /** 28 | * @typedef {{ 29 | * Other: advice, 30 | * All: advice, 31 | * MA: advice 32 | * }} Period 33 | */ 34 | 35 | /** 36 | * @typedef {{ 37 | * '1': Period, 38 | * '5': Period, 39 | * '15': Period, 40 | * '60': Period, 41 | * '240': Period, 42 | * '1D': Period, 43 | * '1W': Period, 44 | * '1M': Period 45 | * }} Periods 46 | */ 47 | 48 | module.exports = { 49 | /** 50 | * Get technical analysis 51 | * @function getTA 52 | * @param {string} id Full market id (Example: COINBASE:BTCEUR) 53 | * @returns {Promise} results 54 | */ 55 | async getTA(id) { 56 | const advice = {}; 57 | 58 | const cols = ['1', '5', '15', '60', '240', '1D', '1W', '1M'] 59 | .map((t) => indicators.map((i) => (t !== '1D' ? `${i}|${t}` : i))) 60 | .flat(); 61 | 62 | const rs = await fetchScanData([id], cols); 63 | if (!rs.data || !rs.data[0]) return false; 64 | 65 | rs.data[0].d.forEach((val, i) => { 66 | const [name, period] = cols[i].split('|'); 67 | const pName = period || '1D'; 68 | if (!advice[pName]) advice[pName] = {}; 69 | advice[pName][name.split('.').pop()] = Math.round(val * 1000) / 500; 70 | }); 71 | 72 | return advice; 73 | }, 74 | 75 | /** 76 | * @typedef {Object} SearchMarketResult 77 | * @prop {string} id Market full symbol 78 | * @prop {string} exchange Market exchange name 79 | * @prop {string} fullExchange Market exchange full name 80 | * @prop {string} symbol Market symbol 81 | * @prop {string} description Market name 82 | * @prop {string} type Market type 83 | * @prop {() => Promise} getTA Get market technical analysis 84 | */ 85 | 86 | /** 87 | * Find a symbol (deprecated) 88 | * @function searchMarket 89 | * @param {string} search Keywords 90 | * @param {'stock' 91 | * | 'futures' | 'forex' | 'cfd' 92 | * | 'crypto' | 'index' | 'economic' 93 | * } [filter] Caterogy filter 94 | * @returns {Promise} Search results 95 | * @deprecated Use searchMarketV3 instead 96 | */ 97 | async searchMarket(search, filter = '') { 98 | const { data } = await axios.get( 99 | 'https://symbol-search.tradingview.com/symbol_search', 100 | { 101 | params: { 102 | text: search.replace(/ /g, '%20'), 103 | type: filter, 104 | }, 105 | headers: { 106 | origin: 'https://www.tradingview.com', 107 | }, 108 | validateStatus, 109 | }, 110 | ); 111 | 112 | return data.map((s) => { 113 | const exchange = s.exchange.split(' ')[0]; 114 | const id = `${exchange}:${s.symbol}`; 115 | 116 | return { 117 | id, 118 | exchange, 119 | fullExchange: s.exchange, 120 | symbol: s.symbol, 121 | description: s.description, 122 | type: s.type, 123 | getTA: () => this.getTA(id), 124 | }; 125 | }); 126 | }, 127 | 128 | /** 129 | * Find a symbol 130 | * @function searchMarketV3 131 | * @param {string} search Keywords 132 | * @param {'stock' 133 | * | 'futures' | 'forex' | 'cfd' 134 | * | 'crypto' | 'index' | 'economic' 135 | * } [filter] Caterogy filter 136 | * @returns {Promise} Search results 137 | */ 138 | async searchMarketV3(search, filter = '') { 139 | const splittedSearch = search.toUpperCase().replace(/ /g, '+').split(':'); 140 | 141 | const request = await axios.get( 142 | 'https://symbol-search.tradingview.com/symbol_search/v3', 143 | { 144 | params: { 145 | exchange: (splittedSearch.length === 2 146 | ? splittedSearch[0] 147 | : undefined 148 | ), 149 | text: splittedSearch.pop(), 150 | search_type: filter, 151 | }, 152 | headers: { 153 | origin: 'https://www.tradingview.com', 154 | }, 155 | validateStatus, 156 | }, 157 | ); 158 | 159 | const { data } = request; 160 | 161 | return data.symbols.map((s) => { 162 | const exchange = s.exchange.split(' ')[0]; 163 | const id = s.prefix ? `${s.prefix}:${s.symbol}` : `${exchange.toUpperCase()}:${s.symbol}`; 164 | 165 | return { 166 | id, 167 | exchange, 168 | fullExchange: s.exchange, 169 | symbol: s.symbol, 170 | description: s.description, 171 | type: s.type, 172 | getTA: () => this.getTA(id), 173 | }; 174 | }); 175 | }, 176 | 177 | /** 178 | * @typedef {Object} SearchIndicatorResult 179 | * @prop {string} id Script ID 180 | * @prop {string} version Script version 181 | * @prop {string} name Script complete name 182 | * @prop {{ id: number, username: string }} author Author user ID 183 | * @prop {string} image Image ID https://tradingview.com/i/${image} 184 | * @prop {string | ''} source Script source (if available) 185 | * @prop {'study' | 'strategy'} type Script type (study / strategy) 186 | * @prop {'open_source' | 'closed_source' | 'invite_only' 187 | * | 'private' | 'other'} access Script access type 188 | * @prop {() => Promise} get Get the full indicator informations 189 | */ 190 | 191 | /** 192 | * Find an indicator 193 | * @function searchIndicator 194 | * @param {string} search Keywords 195 | * @returns {Promise} Search results 196 | */ 197 | async searchIndicator(search = '') { 198 | if (!builtInIndicList.length) { 199 | await Promise.all(['standard', 'candlestick', 'fundamental'].map(async (type) => { 200 | const { data } = await axios.get( 201 | 'https://pine-facade.tradingview.com/pine-facade/list', 202 | { 203 | params: { 204 | filter: type, 205 | }, 206 | validateStatus, 207 | }, 208 | ); 209 | builtInIndicList.push(...data); 210 | })); 211 | } 212 | 213 | const { data } = await axios.get( 214 | 'https://www.tradingview.com/pubscripts-suggest-json', 215 | { 216 | params: { 217 | search: search.replace(/ /g, '%20'), 218 | }, 219 | validateStatus, 220 | }, 221 | ); 222 | 223 | function norm(str = '') { 224 | return str.toUpperCase().replace(/[^A-Z]/g, ''); 225 | } 226 | 227 | return [ 228 | ...builtInIndicList.filter((i) => ( 229 | norm(i.scriptName).includes(norm(search)) 230 | || norm(i.extra.shortDescription).includes(norm(search)) 231 | )).map((ind) => ({ 232 | id: ind.scriptIdPart, 233 | version: ind.version, 234 | name: ind.scriptName, 235 | author: { 236 | id: ind.userId, 237 | username: '@TRADINGVIEW@', 238 | }, 239 | image: '', 240 | access: 'closed_source', 241 | source: '', 242 | type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study', 243 | get() { 244 | return module.exports.getIndicator(ind.scriptIdPart, ind.version); 245 | }, 246 | })), 247 | 248 | ...data.results.map((ind) => ({ 249 | id: ind.scriptIdPart, 250 | version: ind.version, 251 | name: ind.scriptName, 252 | author: { 253 | id: ind.author.id, 254 | username: ind.author.username, 255 | }, 256 | image: ind.imageUrl, 257 | access: ['open_source', 'closed_source', 'invite_only'][ind.access - 1] || 'other', 258 | source: ind.scriptSource, 259 | type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study', 260 | get() { 261 | return module.exports.getIndicator(ind.scriptIdPart, ind.version); 262 | }, 263 | })), 264 | ]; 265 | }, 266 | 267 | /** 268 | * Get an indicator 269 | * @function getIndicator 270 | * @param {string} id Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX) 271 | * @param {'last' | string} [version] Wanted version of the indicator 272 | * @param {string} [session] User 'sessionid' cookie 273 | * @param {string} [signature] User 'sessionid_sign' cookie 274 | * @returns {Promise} Indicator 275 | */ 276 | async getIndicator(id, version = 'last', session = '', signature = '') { 277 | const indicID = id.replace(/ |%/g, '%25'); 278 | 279 | const { data } = await axios.get( 280 | `https://pine-facade.tradingview.com/pine-facade/translate/${indicID}/${version}`, 281 | { 282 | headers: { 283 | cookie: genAuthCookies(session, signature), 284 | }, 285 | validateStatus, 286 | }, 287 | ); 288 | 289 | if (!data.success || !data.result.metaInfo || !data.result.metaInfo.inputs) { 290 | throw new Error(`Inexistent or unsupported indicator: "${data.reason}"`); 291 | } 292 | 293 | const inputs = {}; 294 | 295 | data.result.metaInfo.inputs.forEach((input) => { 296 | if (['text', 'pineId', 'pineVersion'].includes(input.id)) return; 297 | 298 | const inlineName = input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, ''); 299 | 300 | inputs[input.id] = { 301 | name: input.name, 302 | inline: input.inline || inlineName, 303 | internalID: input.internalID || inlineName, 304 | tooltip: input.tooltip, 305 | 306 | type: input.type, 307 | value: input.defval, 308 | isHidden: !!input.isHidden, 309 | isFake: !!input.isFake, 310 | }; 311 | 312 | if (input.options) inputs[input.id].options = input.options; 313 | }); 314 | 315 | const plots = {}; 316 | 317 | Object.keys(data.result.metaInfo.styles).forEach((plotId) => { 318 | const plotTitle = data 319 | .result 320 | .metaInfo 321 | .styles[plotId] 322 | .title 323 | .replace(/ /g, '_') 324 | .replace(/[^a-zA-Z0-9_]/g, ''); 325 | 326 | const titles = Object.values(plots); 327 | 328 | if (titles.includes(plotTitle)) { 329 | let i = 2; 330 | while (titles.includes(`${plotTitle}_${i}`)) i += 1; 331 | plots[plotId] = `${plotTitle}_${i}`; 332 | } else plots[plotId] = plotTitle; 333 | }); 334 | 335 | data.result.metaInfo.plots.forEach((plot) => { 336 | if (!plot.target) return; 337 | plots[plot.id] = `${plots[plot.target] ?? plot.target}_${plot.type}`; 338 | }); 339 | 340 | return new PineIndicator({ 341 | pineId: data.result.metaInfo.scriptIdPart || indicID, 342 | pineVersion: data.result.metaInfo.pine.version || version, 343 | description: data.result.metaInfo.description, 344 | shortDescription: data.result.metaInfo.shortDescription, 345 | inputs, 346 | plots, 347 | script: data.result.ilTemplate, 348 | }); 349 | }, 350 | 351 | /** 352 | * @typedef {Object} User Instance of User 353 | * @prop {number} id User ID 354 | * @prop {string} username User username 355 | * @prop {string} firstName User first name 356 | * @prop {string} lastName User last name 357 | * @prop {number} reputation User reputation 358 | * @prop {number} following Number of following accounts 359 | * @prop {number} followers Number of followers 360 | * @prop {Object} notifications User's notifications 361 | * @prop {number} notifications.user User notifications 362 | * @prop {number} notifications.following Notification from following accounts 363 | * @prop {string} session User session 364 | * @prop {string} sessionHash User session hash 365 | * @prop {string} signature User session signature 366 | * @prop {string} privateChannel User private channel 367 | * @prop {string} authToken User auth token 368 | * @prop {Date} joinDate Account creation date 369 | */ 370 | 371 | /** 372 | * Get user and sessionid from username/email and password 373 | * @function loginUser 374 | * @param {string} username User username/email 375 | * @param {string} password User password 376 | * @param {boolean} [remember] Remember the session (default: false) 377 | * @param {string} [UA] Custom UserAgent 378 | * @returns {Promise} Token 379 | */ 380 | async loginUser(username, password, remember = true, UA = 'TWAPI/3.0') { 381 | const { data, headers } = await axios.post( 382 | 'https://www.tradingview.com/accounts/signin/', 383 | `username=${username}&password=${password}${remember ? '&remember=on' : ''}`, 384 | { 385 | headers: { 386 | referer: 'https://www.tradingview.com', 387 | 'Content-Type': 'application/x-www-form-urlencoded', 388 | 'User-agent': `${UA} (${os.version()}; ${os.platform()}; ${os.arch()})`, 389 | }, 390 | validateStatus, 391 | }, 392 | ); 393 | 394 | const cookies = headers['set-cookie']; 395 | 396 | if (data.error) throw new Error(data.error); 397 | 398 | const sessionCookie = cookies.find((c) => c.includes('sessionid=')); 399 | const session = (sessionCookie.match(/sessionid=(.*?);/) ?? [])[1]; 400 | 401 | const signCookie = cookies.find((c) => c.includes('sessionid_sign=')); 402 | const signature = (signCookie.match(/sessionid_sign=(.*?);/) ?? [])[1]; 403 | 404 | return { 405 | id: data.user.id, 406 | username: data.user.username, 407 | firstName: data.user.first_name, 408 | lastName: data.user.last_name, 409 | reputation: data.user.reputation, 410 | following: data.user.following, 411 | followers: data.user.followers, 412 | notifications: data.user.notification_count, 413 | session, 414 | signature, 415 | sessionHash: data.user.session_hash, 416 | privateChannel: data.user.private_channel, 417 | authToken: data.user.auth_token, 418 | joinDate: new Date(data.user.date_joined), 419 | }; 420 | }, 421 | 422 | /** 423 | * Get user from 'sessionid' cookie 424 | * @function getUser 425 | * @param {string} session User 'sessionid' cookie 426 | * @param {string} [signature] User 'sessionid_sign' cookie 427 | * @param {string} [location] Auth page location (For france: https://fr.tradingview.com/) 428 | * @returns {Promise} Token 429 | */ 430 | async getUser(session, signature = '', location = 'https://www.tradingview.com/') { 431 | const { data, headers } = await axios.get(location, { 432 | headers: { 433 | cookie: genAuthCookies(session, signature), 434 | }, 435 | maxRedirects: 0, 436 | validateStatus, 437 | }); 438 | 439 | if (data.includes('auth_token')) { 440 | return { 441 | id: /"id":([0-9]{1,10}),/.exec(data)?.[1], 442 | username: /"username":"(.*?)"/.exec(data)?.[1], 443 | firstName: /"first_name":"(.*?)"/.exec(data)?.[1], 444 | lastName: /"last_name":"(.*?)"/.exec(data)?.[1], 445 | reputation: parseFloat(/"reputation":(.*?),/.exec(data)?.[1] || 0), 446 | following: parseFloat(/,"following":([0-9]*?),/.exec(data)?.[1] || 0), 447 | followers: parseFloat(/,"followers":([0-9]*?),/.exec(data)?.[1] || 0), 448 | notifications: { 449 | following: parseFloat(/"notification_count":\{"following":([0-9]*),/.exec(data)?.[1] ?? 0), 450 | user: parseFloat(/"notification_count":\{"following":[0-9]*,"user":([0-9]*)/.exec(data)?.[1] ?? 0), 451 | }, 452 | session, 453 | signature, 454 | sessionHash: /"session_hash":"(.*?)"/.exec(data)?.[1], 455 | privateChannel: /"private_channel":"(.*?)"/.exec(data)?.[1], 456 | authToken: /"auth_token":"(.*?)"/.exec(data)?.[1], 457 | joinDate: new Date(/"date_joined":"(.*?)"/.exec(data)?.[1] || 0), 458 | }; 459 | } 460 | 461 | if (headers.location !== location) { 462 | return this.getUser(session, signature, headers.location); 463 | } 464 | 465 | throw new Error('Wrong or expired sessionid/signature'); 466 | }, 467 | 468 | /** 469 | * Get user's private indicators from a 'sessionid' cookie 470 | * @function getPrivateIndicators 471 | * @param {string} session User 'sessionid' cookie 472 | * @param {string} [signature] User 'sessionid_sign' cookie 473 | * @returns {Promise} Search results 474 | */ 475 | async getPrivateIndicators(session, signature = '') { 476 | const { data } = await axios.get( 477 | 'https://pine-facade.tradingview.com/pine-facade/list', 478 | { 479 | headers: { 480 | cookie: genAuthCookies(session, signature), 481 | }, 482 | params: { 483 | filter: 'saved', 484 | }, 485 | validateStatus, 486 | }, 487 | ); 488 | 489 | return data.map((ind) => ({ 490 | id: ind.scriptIdPart, 491 | version: ind.version, 492 | name: ind.scriptName, 493 | author: { 494 | id: -1, 495 | username: '@ME@', 496 | }, 497 | image: ind.imageUrl, 498 | access: 'private', 499 | source: ind.scriptSource, 500 | type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study', 501 | get() { 502 | return module.exports.getIndicator( 503 | ind.scriptIdPart, 504 | ind.version, 505 | session, 506 | signature, 507 | ); 508 | }, 509 | })); 510 | }, 511 | 512 | /** 513 | * User credentials 514 | * @typedef {Object} UserCredentials 515 | * @prop {number} id User ID 516 | * @prop {string} session User session ('sessionid' cookie) 517 | * @prop {string} [signature] User session signature ('sessionid_sign' cookie) 518 | */ 519 | 520 | /** 521 | * Get a chart token from a layout ID and the user credentials if the layout is not public 522 | * @function getChartToken 523 | * @param {string} layout The layout ID found in the layout URL (Like: 'XXXXXXXX') 524 | * @param {UserCredentials} [credentials] User credentials (id + session + [signature]) 525 | * @returns {Promise} Token 526 | */ 527 | async getChartToken(layout, credentials = {}) { 528 | const { id, session, signature } = ( 529 | credentials.id && credentials.session 530 | ? credentials 531 | : { id: -1, session: null, signature: null } 532 | ); 533 | 534 | const { data } = await axios.get( 535 | 'https://www.tradingview.com/chart-token', 536 | { 537 | headers: { 538 | cookie: genAuthCookies(session, signature), 539 | }, 540 | params: { 541 | image_url: layout, 542 | user_id: id, 543 | }, 544 | validateStatus, 545 | }, 546 | ); 547 | 548 | if (!data.token) throw new Error('Wrong layout or credentials'); 549 | 550 | return data.token; 551 | }, 552 | 553 | /** 554 | * @typedef {Object} DrawingPoint Drawing poitn 555 | * @prop {number} time_t Point X time position 556 | * @prop {number} price Point Y price position 557 | * @prop {number} offset Point offset 558 | */ 559 | 560 | /** 561 | * @typedef {Object} Drawing 562 | * @prop {string} id Drawing ID (Like: 'XXXXXX') 563 | * @prop {string} symbol Layout market symbol (Like: 'BINANCE:BUCEUR') 564 | * @prop {string} ownerSource Owner user ID (Like: 'XXXXXX') 565 | * @prop {string} serverUpdateTime Drawing last update timestamp 566 | * @prop {string} currencyId Currency ID (Like: 'EUR') 567 | * @prop {any} unitId Unit ID 568 | * @prop {string} type Drawing type 569 | * @prop {DrawingPoint[]} points List of drawing points 570 | * @prop {number} zorder Drawing Z order 571 | * @prop {string} linkKey Drawing link key 572 | * @prop {Object} state Drawing state 573 | */ 574 | 575 | /** 576 | * Get a chart token from a layout ID and the user credentials if the layout is not public 577 | * @function getDrawings 578 | * @param {string} layout The layout ID found in the layout URL (Like: 'XXXXXXXX') 579 | * @param {string | ''} [symbol] Market filter (Like: 'BINANCE:BTCEUR') 580 | * @param {UserCredentials} [credentials] User credentials (id + session + [signature]) 581 | * @param {number} [chartID] Chart ID 582 | * @returns {Promise} Drawings 583 | */ 584 | async getDrawings(layout, symbol = '', credentials = {}, chartID = '_shared') { 585 | const chartToken = await module.exports.getChartToken(layout, credentials); 586 | 587 | const { data } = await axios.get( 588 | `https://charts-storage.tradingview.com/charts-storage/get/layout/${ 589 | layout 590 | }/sources`, 591 | { 592 | params: { 593 | chart_id: chartID, 594 | jwt: chartToken, 595 | symbol, 596 | }, 597 | validateStatus, 598 | }, 599 | ); 600 | 601 | if (!data.payload) throw new Error('Wrong layout, user credentials, or chart id.'); 602 | 603 | return Object.values(data.payload.sources || {}).map((drawing) => ({ 604 | ...drawing, ...drawing.state, 605 | })); 606 | }, 607 | }; 608 | -------------------------------------------------------------------------------- /src/protocol.js: -------------------------------------------------------------------------------- 1 | const JSZip = require('jszip'); 2 | 3 | /** 4 | * @typedef {Object} TWPacket 5 | * @prop {string} [m] Packet type 6 | * @prop {[session: string, {}]} [p] Packet data 7 | */ 8 | 9 | const cleanerRgx = /~h~/g; 10 | const splitterRgx = /~m~[0-9]{1,}~m~/g; 11 | 12 | module.exports = { 13 | /** 14 | * Parse websocket packet 15 | * @function parseWSPacket 16 | * @param {string} str Websocket raw data 17 | * @returns {TWPacket[]} TradingView packets 18 | */ 19 | parseWSPacket(str) { 20 | return str.replace(cleanerRgx, '').split(splitterRgx) 21 | .map((p) => { 22 | if (!p) return false; 23 | try { 24 | return JSON.parse(p); 25 | } catch (error) { 26 | console.warn('Cant parse', p); 27 | return false; 28 | } 29 | }) 30 | .filter((p) => p); 31 | }, 32 | 33 | /** 34 | * Format websocket packet 35 | * @function formatWSPacket 36 | * @param {TWPacket} packet TradingView packet 37 | * @returns {string} Websocket raw data 38 | */ 39 | formatWSPacket(packet) { 40 | const msg = typeof packet === 'object' 41 | ? JSON.stringify(packet) 42 | : packet; 43 | return `~m~${msg.length}~m~${msg}`; 44 | }, 45 | 46 | /** 47 | * Parse compressed data 48 | * @function parseCompressed 49 | * @param {string} data Compressed data 50 | * @returns {Promise<{}>} Parsed data 51 | */ 52 | async parseCompressed(data) { 53 | const zip = new JSZip(); 54 | return JSON.parse( 55 | await ( 56 | await zip.loadAsync(data, { base64: true }) 57 | ).file('').async('text'), 58 | ); 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/quote/market.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {'loaded' | 'data' | 'error'} MarketEvent 3 | */ 4 | 5 | /** 6 | * @param {import('./session').QuoteSessionBridge} quoteSession 7 | */ 8 | 9 | module.exports = (quoteSession) => class QuoteMarket { 10 | #symbolListeners = quoteSession.symbolListeners; 11 | 12 | #symbol; 13 | 14 | #session; 15 | 16 | #symbolKey; 17 | 18 | #symbolListenerID = 0; 19 | 20 | #lastData = {}; 21 | 22 | #callbacks = { 23 | loaded: [], 24 | data: [], 25 | 26 | event: [], 27 | error: [], 28 | }; 29 | 30 | /** 31 | * @param {MarketEvent} ev Client event 32 | * @param {...{}} data Packet data 33 | */ 34 | #handleEvent(ev, ...data) { 35 | this.#callbacks[ev].forEach((e) => e(...data)); 36 | this.#callbacks.event.forEach((e) => e(ev, ...data)); 37 | } 38 | 39 | #handleError(...msgs) { 40 | if (this.#callbacks.error.length === 0) console.error(...msgs); 41 | else this.#handleEvent('error', ...msgs); 42 | } 43 | 44 | /** 45 | * @param {string} symbol Market symbol (like: 'BTCEUR' or 'KRAKEN:BTCEUR') 46 | * @param {string} session Market session (like: 'regular' or 'extended') 47 | */ 48 | constructor(symbol, session = 'regular') { 49 | this.#symbol = symbol; 50 | this.#session = session; 51 | this.#symbolKey = `=${JSON.stringify({ session, symbol })}`; 52 | 53 | if (!this.#symbolListeners[this.#symbolKey]) { 54 | this.#symbolListeners[this.#symbolKey] = []; 55 | quoteSession.send('quote_add_symbols', [ 56 | quoteSession.sessionID, 57 | this.#symbolKey, 58 | ]); 59 | } 60 | 61 | this.#symbolListenerID = this.#symbolListeners[this.#symbolKey].length; 62 | 63 | this.#symbolListeners[this.#symbolKey][this.#symbolListenerID] = (packet) => { 64 | if (global.TW_DEBUG) console.log('§90§30§105 MARKET §0 DATA', packet); 65 | 66 | if (packet.type === 'qsd' && packet.data[1].s === 'ok') { 67 | this.#lastData = { 68 | ...this.#lastData, 69 | ...packet.data[1].v, 70 | }; 71 | this.#handleEvent('data', this.#lastData); 72 | return; 73 | } 74 | 75 | if (packet.type === 'quote_completed') { 76 | this.#handleEvent('loaded'); 77 | return; 78 | } 79 | 80 | if (packet.type === 'qsd' && packet.data[1].s === 'error') { 81 | this.#handleError('Market error', packet.data); 82 | } 83 | }; 84 | } 85 | 86 | /** 87 | * When quote market is loaded 88 | * @param {() => void} cb Callback 89 | * @event 90 | */ 91 | onLoaded(cb) { 92 | this.#callbacks.loaded.push(cb); 93 | } 94 | 95 | /** 96 | * When quote data is received 97 | * @param {(data: {}) => void} cb Callback 98 | * @event 99 | */ 100 | onData(cb) { 101 | this.#callbacks.data.push(cb); 102 | } 103 | 104 | /** 105 | * When quote event happens 106 | * @param {(...any) => void} cb Callback 107 | * @event 108 | */ 109 | onEvent(cb) { 110 | this.#callbacks.event.push(cb); 111 | } 112 | 113 | /** 114 | * When quote error happens 115 | * @param {(...any) => void} cb Callback 116 | * @event 117 | */ 118 | onError(cb) { 119 | this.#callbacks.error.push(cb); 120 | } 121 | 122 | /** Close this listener */ 123 | close() { 124 | if (this.#symbolListeners[this.#symbolKey].length <= 1) { 125 | quoteSession.send('quote_remove_symbols', [ 126 | quoteSession.sessionID, 127 | this.#symbolKey, 128 | ]); 129 | } 130 | delete this.#symbolListeners[this.#symbolKey][this.#symbolListenerID]; 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /src/quote/session.js: -------------------------------------------------------------------------------- 1 | const { genSessionID } = require('../utils'); 2 | 3 | const quoteMarketConstructor = require('./market'); 4 | 5 | /** @typedef {Object} SymbolListeners */ 6 | 7 | /** 8 | * @typedef {Object} QuoteSessionBridge 9 | * @prop {string} sessionID 10 | * @prop {SymbolListeners} symbolListeners 11 | * @prop {import('../client').SendPacket} send 12 | */ 13 | 14 | /** 15 | * @typedef {'base-currency-logoid' 16 | * | 'ch' | 'chp' | 'currency-logoid' | 'provider_id' 17 | * | 'currency_code' | 'current_session' | 'description' 18 | * | 'exchange' | 'format' | 'fractional' | 'is_tradable' 19 | * | 'language' | 'local_description' | 'logoid' | 'lp' 20 | * | 'lp_time' | 'minmov' | 'minmove2' | 'original_name' 21 | * | 'pricescale' | 'pro_name' | 'short_name' | 'type' 22 | * | 'update_mode' | 'volume' | 'ask' | 'bid' | 'fundamentals' 23 | * | 'high_price' | 'low_price' | 'open_price' | 'prev_close_price' 24 | * | 'rch' | 'rchp' | 'rtc' | 'rtc_time' | 'status' | 'industry' 25 | * | 'basic_eps_net_income' | 'beta_1_year' | 'market_cap_basic' 26 | * | 'earnings_per_share_basic_ttm' | 'price_earnings_ttm' 27 | * | 'sector' | 'dividends_yield' | 'timezone' | 'country_code' 28 | * } quoteField Quote data field 29 | */ 30 | 31 | /** @param {'all' | 'price'} fieldsType */ 32 | function getQuoteFields(fieldsType) { 33 | if (fieldsType === 'price') { 34 | return ['lp']; 35 | } 36 | 37 | return [ 38 | 'base-currency-logoid', 'ch', 'chp', 'currency-logoid', 39 | 'currency_code', 'current_session', 'description', 40 | 'exchange', 'format', 'fractional', 'is_tradable', 41 | 'language', 'local_description', 'logoid', 'lp', 42 | 'lp_time', 'minmov', 'minmove2', 'original_name', 43 | 'pricescale', 'pro_name', 'short_name', 'type', 44 | 'update_mode', 'volume', 'ask', 'bid', 'fundamentals', 45 | 'high_price', 'low_price', 'open_price', 'prev_close_price', 46 | 'rch', 'rchp', 'rtc', 'rtc_time', 'status', 'industry', 47 | 'basic_eps_net_income', 'beta_1_year', 'market_cap_basic', 48 | 'earnings_per_share_basic_ttm', 'price_earnings_ttm', 49 | 'sector', 'dividends_yield', 'timezone', 'country_code', 50 | 'provider_id', 51 | ]; 52 | } 53 | 54 | /** 55 | * @param {import('../client').ClientBridge} client 56 | */ 57 | module.exports = (client) => class QuoteSession { 58 | #sessionID = genSessionID('qs'); 59 | 60 | /** Parent client */ 61 | #client = client; 62 | 63 | /** @type {SymbolListeners} */ 64 | #symbolListeners = {}; 65 | 66 | /** 67 | * @typedef {Object} quoteSessionOptions Quote Session options 68 | * @prop {'all' | 'price'} [fields] Asked quote fields 69 | * @prop {quoteField[]} [customFields] List of asked quote fields 70 | */ 71 | 72 | /** 73 | * @param {quoteSessionOptions} options Quote settings options 74 | */ 75 | constructor(options = {}) { 76 | this.#client.sessions[this.#sessionID] = { 77 | type: 'quote', 78 | onData: (packet) => { 79 | if (global.TW_DEBUG) console.log('§90§30§102 QUOTE SESSION §0 DATA', packet); 80 | 81 | if (packet.type === 'quote_completed') { 82 | const symbolKey = packet.data[1]; 83 | if (!this.#symbolListeners[symbolKey]) { 84 | this.#client.send('quote_remove_symbols', [this.#sessionID, symbolKey]); 85 | return; 86 | } 87 | this.#symbolListeners[symbolKey].forEach((h) => h(packet)); 88 | } 89 | 90 | if (packet.type === 'qsd') { 91 | const symbolKey = packet.data[1].n; 92 | if (!this.#symbolListeners[symbolKey]) { 93 | this.#client.send('quote_remove_symbols', [this.#sessionID, symbolKey]); 94 | return; 95 | } 96 | this.#symbolListeners[symbolKey].forEach((h) => h(packet)); 97 | } 98 | }, 99 | }; 100 | 101 | const fields = (options.customFields && options.customFields.length > 0 102 | ? options.customFields 103 | : getQuoteFields(options.fields) 104 | ); 105 | 106 | this.#client.send('quote_create_session', [this.#sessionID]); 107 | this.#client.send('quote_set_fields', [this.#sessionID, ...fields]); 108 | } 109 | 110 | /** @type {QuoteSessionBridge} */ 111 | #quoteSession = { 112 | sessionID: this.#sessionID, 113 | symbolListeners: this.#symbolListeners, 114 | send: (t, p) => this.#client.send(t, p), 115 | }; 116 | 117 | /** @constructor */ 118 | Market = quoteMarketConstructor(this.#quoteSession); 119 | 120 | /** Delete the quote session */ 121 | delete() { 122 | this.#client.send('quote_delete_session', [this.#sessionID]); 123 | delete this.#client.sessions[this.#sessionID]; 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {string} MarketSymbol Market symbol (like: 'BTCEUR' or 'KRAKEN:BTCEUR') 3 | */ 4 | 5 | /** 6 | * @typedef {'Etc/UTC' | 'exchange' 7 | * | 'Pacific/Honolulu' | 'America/Juneau' | 'America/Los_Angeles' 8 | * | 'America/Phoenix' | 'America/Vancouver' | 'US/Mountain' 9 | * | 'America/El_Salvador' | 'America/Bogota' | 'America/Chicago' 10 | * | 'America/Lima' | 'America/Mexico_City' | 'America/Caracas' 11 | * | 'America/New_York' | 'America/Toronto' | 'America/Argentina/Buenos_Aires' 12 | * | 'America/Santiago' | 'America/Sao_Paulo' | 'Atlantic/Reykjavik' 13 | * | 'Europe/Dublin' | 'Africa/Lagos' | 'Europe/Lisbon' | 'Europe/London' 14 | * | 'Europe/Amsterdam' | 'Europe/Belgrade' | 'Europe/Berlin' 15 | * | 'Europe/Brussels' | 'Europe/Copenhagen' | 'Africa/Johannesburg' 16 | * | 'Africa/Cairo' | 'Europe/Luxembourg' | 'Europe/Madrid' | 'Europe/Malta' 17 | * | 'Europe/Oslo' | 'Europe/Paris' | 'Europe/Rome' | 'Europe/Stockholm' 18 | * | 'Europe/Warsaw' | 'Europe/Zurich' | 'Europe/Athens' | 'Asia/Bahrain' 19 | * | 'Europe/Helsinki' | 'Europe/Istanbul' | 'Asia/Jerusalem' | 'Asia/Kuwait' 20 | * | 'Europe/Moscow' | 'Asia/Qatar' | 'Europe/Riga' | 'Asia/Riyadh' 21 | * | 'Europe/Tallinn' | 'Europe/Vilnius' | 'Asia/Tehran' | 'Asia/Dubai' 22 | * | 'Asia/Muscat' | 'Asia/Ashkhabad' | 'Asia/Kolkata' | 'Asia/Almaty' 23 | * | 'Asia/Bangkok' | 'Asia/Jakarta' | 'Asia/Ho_Chi_Minh' | 'Asia/Chongqing' 24 | * | 'Asia/Hong_Kong' | 'Australia/Perth' | 'Asia/Shanghai' | 'Asia/Singapore' 25 | * | 'Asia/Taipei' | 'Asia/Seoul' | 'Asia/Tokyo' | 'Australia/Brisbane' 26 | * | 'Australia/Adelaide' | 'Australia/Sydney' | 'Pacific/Norfolk' 27 | * | 'Pacific/Auckland' | 'Pacific/Fakaofo' | 'Pacific/Chatham'} Timezone (Chart) timezone 28 | */ 29 | 30 | /** 31 | * @typedef {'1' | '3' | '5' | '15' | '30' 32 | * | '45' | '60' | '120' | '180' | '240' 33 | * | '1D' | '1W' | '1M' | 'D' | 'W' | 'M'} TimeFrame 34 | */ 35 | 36 | module.exports = {}; 37 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Generates a session id 4 | * @function genSessionID 5 | * @param {String} type Session type 6 | * @returns {string} 7 | */ 8 | genSessionID(type = 'xs') { 9 | let r = ''; 10 | const c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 11 | for (let i = 0; i < 12; i += 1) r += c.charAt(Math.floor(Math.random() * c.length)); 12 | return `${type}_${r}`; 13 | }, 14 | 15 | genAuthCookies(sessionId = '', signature = '') { 16 | if (!sessionId) return ''; 17 | if (!signature) return `sessionid=${sessionId}`; 18 | return `sessionid=${sessionId};sessionid_sign=${signature}`; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /tests/allErrors.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | 4 | const token = process.env.SESSION; 5 | const signature = process.env.SIGNATURE; 6 | 7 | describe('AllErrors', () => { 8 | const waitForError = (instance: any, next = () => {}) => new Promise((resolve) => { 9 | instance.onError((...error: string[]) => resolve(error)); 10 | next(); 11 | }); 12 | 13 | it.skip('throws an error when an invalid token is set', async () => { 14 | console.log('Testing "Credentials error" error:'); 15 | 16 | const client = new TradingView.Client({ 17 | token: 'FAKE_CREDENTIALS', // Set wrong credentials 18 | }); 19 | 20 | const error = await waitForError(client); 21 | console.log('=> Client error:', error); 22 | 23 | expect(error).toBeDefined(); 24 | expect(error[0]).toBe('Credentials error:'); 25 | expect(error[1]).toBe('Wrong or expired sessionid/signature'); 26 | expect(error.length).toBe(2); 27 | }); 28 | 29 | it('throws an error when an invalid symbol is set', async () => { 30 | console.log('Testing "invalid symbol" error:'); 31 | 32 | const client = new TradingView.Client(); 33 | const chart = new client.Session.Chart(); 34 | 35 | const error = await waitForError( 36 | chart, 37 | () => chart.setMarket('XXXXX'), 38 | ); 39 | 40 | console.log('=> Chart error:', error); 41 | 42 | expect(error).toBeDefined(); 43 | expect(error[0]).toBe('(ser_1) Symbol error:'); 44 | expect(error[1]).toBe('invalid symbol'); 45 | expect(error.length).toBe(2); 46 | }); 47 | 48 | it('throws an error when an invalid timezome is set', async () => { 49 | console.log('Testing "invalid timezone" error:'); 50 | 51 | const client = new TradingView.Client(); 52 | const chart = new client.Session.Chart(); 53 | chart.setMarket('BINANCE:BTCEUR'); 54 | 55 | const error = await waitForError( 56 | chart, 57 | // @ts-expect-error 58 | () => chart.setTimezone('Nowhere/Nowhere'), 59 | ); 60 | 61 | console.log('=> Chart error:', error); 62 | 63 | expect(error).toBeDefined(); 64 | expect(error[0]).toBe('Critical error:'); 65 | expect(error[1]).toBe('invalid timezone'); 66 | expect(error[2]).toBe('method: switch_timezone. args: "[Nowhere/Nowhere]"'); 67 | expect(error.length).toBe(3); 68 | }); 69 | 70 | it.skip('throws an error when a custom timeframe is set without premium', async () => { 71 | console.log('Testing "custom timeframe" error:'); 72 | 73 | const client = new TradingView.Client(); 74 | const chart = new client.Session.Chart(); 75 | 76 | const error = await waitForError( 77 | chart, 78 | () => chart.setMarket('BINANCE:BTCEUR', { // Set a market 79 | // @ts-expect-error 80 | timeframe: '20', // Set a custom timeframe 81 | /* 82 | Timeframe '20' isn't available because we are 83 | not logged in as a premium TradingView account 84 | */ 85 | }), 86 | ); 87 | 88 | console.log('=> Chart error:', error); 89 | 90 | expect(error).toBeDefined(); 91 | expect(error[0]).toBe('Series error:'); 92 | expect(error[1]).toBe('custom_resolution'); 93 | expect(error.length).toBe(2); 94 | }); 95 | 96 | it('throws an error when an invalid timeframe is set', async () => { 97 | console.log('Testing "Invalid timeframe" error:'); 98 | 99 | const client = new TradingView.Client(); 100 | const chart = new client.Session.Chart(); 101 | 102 | const error = await waitForError( 103 | chart, 104 | () => chart.setMarket('BINANCE:BTCEUR', { // Set a market 105 | // @ts-expect-error 106 | timeframe: 'XX', // Set an invalid timeframe 107 | }), 108 | ); 109 | 110 | console.log('=> Chart error:', error); 111 | 112 | expect(error).toBeDefined(); 113 | expect(error[0]).toBe('Critical error:'); 114 | expect(error[1]).toBe('invalid parameters'); 115 | expect(error[2]).toBe('method: create_series. args: "[$prices, s1, ser_1, XX, 100]"'); 116 | expect(error.length).toBe(3); 117 | }); 118 | 119 | it('throws an error when a premium chart type is set without premium', async () => { 120 | console.log('Testing "Study not auth" error:'); 121 | 122 | const client = new TradingView.Client(); 123 | const chart = new client.Session.Chart(); 124 | 125 | const error = await waitForError( 126 | chart, 127 | () => chart.setMarket('BINANCE:BTCEUR', { // Set a market 128 | timeframe: '15', 129 | type: 'Renko', 130 | }), 131 | ); 132 | 133 | console.log('=> Chart error:', error); 134 | 135 | expect(error).toBeDefined(); 136 | expect(error[0]).toBe('Series error:'); 137 | expect(error[1]).toMatch(/study_not_auth:BarSetRenko@tv-prostudies-\d+/); 138 | expect(error.length).toBe(2); 139 | }); 140 | 141 | it('throws an error when series is edited before market is set', async () => { 142 | console.log('Testing "Set the market before..." error:'); 143 | 144 | const client = new TradingView.Client(); 145 | const chart = new client.Session.Chart(); 146 | 147 | const error = await waitForError( 148 | chart, 149 | () => chart.setSeries('15'), 150 | ); 151 | console.log('=> Chart error:', error); 152 | 153 | expect(error).toBeDefined(); 154 | expect(error[0]).toBe('Please set the market before setting series'); 155 | expect(error.length).toBe(1); 156 | }); 157 | 158 | it('throws an error when getting a non-existent indicator', async () => { 159 | console.log('Testing "Inexistent indicator" error:'); 160 | 161 | expect( 162 | TradingView.getIndicator('STD;XXXXXXX'), 163 | ).rejects.toThrow('Inexistent or unsupported indicator: "undefined"'); 164 | }); 165 | 166 | it.skipIf( 167 | !token || !signature, 168 | )('throws an error when setting an invalid study option value', async () => { 169 | console.log('Testing "Invalid value" error:'); 170 | 171 | const client = new TradingView.Client({ token, signature }); 172 | const chart = new client.Session.Chart(); 173 | 174 | chart.setMarket('BINANCE:BTCEUR'); // Set a market 175 | 176 | const ST = await TradingView.getIndicator('STD;Supertrend'); 177 | ST.setOption('Factor', -1); // This will cause an error 178 | 179 | const Supertrend = new chart.Study(ST); 180 | 181 | const error = await waitForError(Supertrend); 182 | console.log('=> Study error:', error); 183 | 184 | expect(error).toEqual([ 185 | { 186 | ctx: { 187 | length: -1, 188 | nameInvalidValue: 'factor', 189 | bar_index: 0, 190 | operation: '>', 191 | funName: '\'supertrend\'', 192 | }, 193 | error: 'Error on bar {bar_index}: Invalid value of the \'{nameInvalidValue}\' argument ({length}) in the \'{funName}\' function. It must be {operation} 0.', 194 | stack_trace: [{ n: '#main', p: 7 }], 195 | }, 196 | 'undefined', 197 | ]); 198 | 199 | console.log('OK'); 200 | }); 201 | 202 | it.skipIf( 203 | !token || !signature, 204 | ).skip('throws an error when getting user data without signature', async () => { 205 | console.log('Testing "Wrong or expired sessionid/signature" error using getUser method:'); 206 | 207 | console.log('Trying with signaure'); 208 | const userInfo = await TradingView.getUser(token, signature); 209 | 210 | console.log('Result:', { 211 | id: userInfo.id, 212 | username: userInfo.username, 213 | firstName: userInfo.firstName, 214 | lastName: userInfo.lastName, 215 | following: userInfo.following, 216 | followers: userInfo.followers, 217 | notifications: userInfo.notifications, 218 | joinDate: userInfo.joinDate, 219 | }); 220 | 221 | expect(userInfo).toBeDefined(); 222 | expect(userInfo.id).toBeDefined(); 223 | 224 | console.log('Trying without signaure'); 225 | expect( 226 | TradingView.getUser(token), 227 | ).rejects.toThrow('Wrong or expired sessionid/signature'); 228 | 229 | console.log('OK'); 230 | }); 231 | 232 | it.skipIf( 233 | !token || !signature, 234 | ).skip('throws an error when creating an authenticated client without signature', async () => { 235 | console.log('Testing "Wrong or expired sessionid/signature" error using client:'); 236 | 237 | const client = new TradingView.Client({ token }); 238 | 239 | const error = await waitForError(client); 240 | console.log('=> Client error:', error); 241 | 242 | expect(error).toBeDefined(); 243 | expect(error[0]).toBe('Credentials error:'); 244 | expect(error[1]).toBe('Wrong or expired sessionid/signature'); 245 | expect(error.length).toBe(2); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /tests/authenticated.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | import utils from './utils'; 4 | 5 | const token = process.env.SESSION as string; 6 | const signature = process.env.SIGNATURE as string; 7 | 8 | describe.skipIf(!token || !signature)('Authenticated actions', () => { 9 | it('gets user info', async () => { 10 | console.log('Testing getUser method'); 11 | 12 | const userInfo = await TradingView.getUser(token, signature); 13 | 14 | console.log('User:', { 15 | id: userInfo.id, 16 | username: userInfo.username, 17 | firstName: userInfo.firstName, 18 | lastName: userInfo.lastName, 19 | following: userInfo.following, 20 | followers: userInfo.followers, 21 | notifications: userInfo.notifications, 22 | joinDate: userInfo.joinDate, 23 | }); 24 | 25 | expect(userInfo).toBeDefined(); 26 | expect(userInfo.id).toBeDefined(); 27 | expect(userInfo.username).toBeDefined(); 28 | expect(userInfo.following).toBeDefined(); 29 | expect(userInfo.followers).toBeDefined(); 30 | expect(userInfo.notifications).toBeDefined(); 31 | expect(userInfo.notifications.following).toBeDefined(); 32 | expect(userInfo.notifications.user).toBeDefined(); 33 | expect(userInfo.joinDate).toBeDefined(); 34 | 35 | expect(userInfo.session).toBe(token); 36 | expect(userInfo.signature).toBe(signature); 37 | }); 38 | 39 | const userIndicators: any[] = []; 40 | 41 | it('gets user indicators', async () => { 42 | console.log('Testing getPrivateIndicators method'); 43 | 44 | userIndicators.push(...await TradingView.getPrivateIndicators(token)); 45 | console.log('Indicators:', userIndicators.map((i) => i.name)); 46 | 47 | expect(userIndicators.length).toBeGreaterThan(0); 48 | }); 49 | 50 | it('creates a chart with all user indicators', async () => { 51 | console.log('Creating logged client'); 52 | const client = new TradingView.Client({ token, signature }); 53 | const chart = new client.Session.Chart(); 54 | 55 | console.log('Setting market to BINANCE:BTCEUR...'); 56 | chart.setMarket('BINANCE:BTCEUR', { timeframe: 'D' }); 57 | 58 | // Limit to 3 indicators for testing 59 | const testedIndicators = userIndicators.slice(0, 3); 60 | 61 | const checked = new Set(); 62 | async function check(item) { 63 | checked.add(item); 64 | console.log('Checked:', [...checked], `(${checked.size}/${testedIndicators.length + 1})`); 65 | } 66 | 67 | chart.onUpdate(async () => { 68 | console.log('Market data:', { 69 | name: chart.infos.pro_name, 70 | description: chart.infos.short_description, 71 | exchange: chart.infos.exchange, 72 | price: chart.periods[0].close, 73 | }); 74 | 75 | await check(Symbol.for('PRICE')); 76 | }); 77 | 78 | console.log('Loading indicators...'); 79 | for (const indic of testedIndicators) { 80 | const privateIndic = await indic.get(); 81 | console.log(`[${indic.name}] Loading indicator...`); 82 | 83 | const indicator = new chart.Study(privateIndic); 84 | 85 | indicator.onReady(() => { 86 | console.log(`[${indic.name}] Indicator loaded !`); 87 | }); 88 | 89 | indicator.onUpdate(async () => { 90 | console.log(`[${indic.name}] Last plot:`, indicator.periods[0]); 91 | await check(indic.id); 92 | }); 93 | } 94 | 95 | while (checked.size < testedIndicators.length + 1) await utils.wait(100); 96 | 97 | console.log('All indicators loaded !'); 98 | }, 10000); 99 | }); 100 | -------------------------------------------------------------------------------- /tests/builtInIndicator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | import utils from './utils'; 4 | 5 | describe('BuiltInIndicator', () => { 6 | let client: TradingView.Client; 7 | let chart: InstanceType; 8 | 9 | it('creates a client', async () => { 10 | client = new TradingView.Client(); 11 | expect(client).toBeDefined(); 12 | }); 13 | 14 | it('creates a chart', async () => { 15 | chart = new client.Session.Chart(); 16 | expect(chart).toBeDefined(); 17 | }); 18 | 19 | it('sets market', async () => { 20 | chart.setMarket('BINANCE:BTCEUR', { 21 | timeframe: '60', 22 | }); 23 | 24 | await new Promise((resolve) => { 25 | chart.onSymbolLoaded(() => { 26 | console.log('Chart loaded'); 27 | resolve(true); 28 | }); 29 | }); 30 | 31 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 32 | }); 33 | 34 | it('gets volume profile', async () => { 35 | const volumeProfile = new TradingView.BuiltInIndicator('VbPFixed@tv-basicstudies-241!'); 36 | volumeProfile.setOption('first_bar_time', Date.now() - 10 ** 8); 37 | 38 | const VOL = new chart.Study(volumeProfile); 39 | 40 | while (!VOL.graphic?.horizHists?.length) await utils.wait(100); 41 | expect(VOL.graphic?.horizHists).toBeDefined(); 42 | 43 | const hists = VOL.graphic.horizHists 44 | .filter((h) => h.lastBarTime === 0) // We only keep recent volume infos 45 | .sort((a, b) => b.priceHigh - a.priceHigh); 46 | 47 | expect(hists.length).toBeGreaterThan(15); 48 | 49 | for (const hist of hists) { 50 | console.log( 51 | `~ ${Math.round((hist.priceHigh + hist.priceLow) / 2)} € :`, 52 | `${'_'.repeat(hist.rate[0] / 3)}${'_'.repeat(hist.rate[1] / 3)}`, 53 | ); 54 | } 55 | }); 56 | 57 | it('removes chart', () => { 58 | console.log('Closing the chart...'); 59 | chart.delete(); 60 | }); 61 | 62 | it('removes client', async () => { 63 | console.log('Closing the client...'); 64 | await client.end(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/customChartTypes.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | import utils from './utils'; 4 | 5 | describe('CustomChartTypes', () => { 6 | let client: TradingView.Client; 7 | let chart: InstanceType; 8 | 9 | it('creates a client', async () => { 10 | client = new TradingView.Client(); 11 | expect(client).toBeDefined(); 12 | }); 13 | 14 | it('creates a chart', async () => { 15 | chart = new client.Session.Chart(); 16 | expect(chart).toBeDefined(); 17 | }); 18 | 19 | it('sets chart type to HeikinAshi', async () => { 20 | console.log('Setting chart type to: HeikinAshi'); 21 | 22 | chart.setMarket('BINANCE:BTCEUR', { 23 | type: 'HeikinAshi', 24 | timeframe: 'D', 25 | }); 26 | 27 | while ( 28 | chart.infos.full_name !== 'BINANCE:BTCEUR' 29 | || !chart.periods.length 30 | ) await utils.wait(100); 31 | 32 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 33 | expect(chart.periods.length).toBe(100); 34 | expect( 35 | utils.calculateTimeGap(chart.periods), 36 | ).toBe(86400); 37 | }); 38 | 39 | it('sets chart type to Renko', async () => { 40 | console.log('Setting chart type to: Renko'); 41 | 42 | chart.setMarket('BINANCE:ETHEUR', { 43 | type: 'Renko', 44 | timeframe: 'D', 45 | inputs: { 46 | source: 'close', 47 | sources: 'Close', 48 | boxSize: 3, 49 | style: 'ATR', 50 | atrLength: 14, 51 | wicks: true, 52 | }, 53 | }); 54 | 55 | while ( 56 | chart.infos.full_name !== 'BINANCE:ETHEUR' 57 | || !chart.periods.length 58 | ) await utils.wait(100); 59 | 60 | expect(chart.infos.full_name).toBe('BINANCE:ETHEUR'); 61 | }); 62 | 63 | it('sets chart type to LineBreak', async () => { 64 | console.log('Setting chart type to: LineBreak'); 65 | 66 | chart.setMarket('BINANCE:BTCEUR', { 67 | type: 'LineBreak', 68 | timeframe: 'D', 69 | inputs: { 70 | source: 'close', 71 | lb: 3, 72 | }, 73 | }); 74 | 75 | while ( 76 | chart.infos.full_name !== 'BINANCE:BTCEUR' 77 | || !chart.periods.length 78 | ) await utils.wait(100); 79 | 80 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 81 | expect(chart.periods.length).toBeGreaterThan(0); 82 | }); 83 | 84 | it('sets chart type to Kagi', async () => { 85 | console.log('Setting chart type to: Kagi'); 86 | 87 | chart.setMarket('BINANCE:ETHEUR', { 88 | type: 'Kagi', 89 | timeframe: 'D', 90 | inputs: { 91 | source: 'close', 92 | style: 'ATR', 93 | atrLength: 14, 94 | reversalAmount: 1, 95 | }, 96 | }); 97 | 98 | while ( 99 | chart.infos.full_name !== 'BINANCE:ETHEUR' 100 | || !chart.periods.length 101 | ) await utils.wait(100); 102 | 103 | expect(chart.infos.full_name).toBe('BINANCE:ETHEUR'); 104 | expect(chart.periods.length).toBeGreaterThan(0); 105 | }); 106 | 107 | it('sets chart type to PointAndFigure', async () => { 108 | console.log('Setting chart type to: PointAndFigure'); 109 | 110 | chart.setMarket('BINANCE:BTCEUR', { 111 | type: 'PointAndFigure', 112 | timeframe: 'D', 113 | inputs: { 114 | sources: 'Close', 115 | reversalAmount: 3, 116 | boxSize: 1, 117 | style: 'ATR', 118 | atrLength: 14, 119 | oneStepBackBuilding: false, 120 | }, 121 | }); 122 | 123 | while ( 124 | chart.infos.full_name !== 'BINANCE:BTCEUR' 125 | || !chart.periods.length 126 | ) await utils.wait(100); 127 | 128 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 129 | expect(chart.periods.length).toBeGreaterThan(0); 130 | }); 131 | 132 | it('sets chart type to Range', async () => { 133 | console.log('Setting chart type to: Range'); 134 | 135 | chart.setMarket('BINANCE:ETHEUR', { 136 | type: 'Range', 137 | timeframe: 'D', 138 | inputs: { 139 | range: 1, 140 | phantomBars: false, 141 | }, 142 | }); 143 | 144 | while ( 145 | chart.infos.full_name !== 'BINANCE:ETHEUR' 146 | || !chart.periods.length 147 | ) await utils.wait(100); 148 | 149 | expect(chart.infos.full_name).toBe('BINANCE:ETHEUR'); 150 | expect(chart.periods.length).toBeGreaterThanOrEqual(99); 151 | expect(chart.periods.length).toBeLessThanOrEqual(102); 152 | }); 153 | 154 | it('closes chart', async () => { 155 | console.log('Waiting 1 second...'); 156 | await utils.wait(1000); 157 | console.log('Closing the chart...'); 158 | chart.delete(); 159 | }); 160 | 161 | it('closes client', async () => { 162 | console.log('Waiting 1 second...'); 163 | await utils.wait(1000); 164 | console.log('Closing the client...'); 165 | await client.end(); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /tests/indicators.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | 4 | const token = process.env.SESSION; 5 | const signature = process.env.SIGNATURE; 6 | 7 | describe('Indicators', () => { 8 | const indicators: { [name: string]: TradingView.PineIndicator } = {}; 9 | 10 | it('gets Supertrend strategy', async () => { 11 | indicators.SuperTrend = await TradingView.getIndicator('STD;Supertrend%Strategy'); 12 | expect(indicators.SuperTrend).toBeDefined(); 13 | expect(indicators.SuperTrend.description).toBe('Supertrend Strategy'); 14 | 15 | indicators.SuperTrend.setOption('commission_type', 'percent'); 16 | indicators.SuperTrend.setOption('commission_value', 0); 17 | indicators.SuperTrend.setOption('initial_capital', 25000); 18 | indicators.SuperTrend.setOption('default_qty_value', 20); 19 | indicators.SuperTrend.setOption('default_qty_type', 'percent_of_equity'); 20 | indicators.SuperTrend.setOption('currency', 'EUR'); 21 | indicators.SuperTrend.setOption('pyramiding', 10); 22 | 23 | expect(indicators.SuperTrend).toBeDefined(); 24 | }); 25 | 26 | it('gets MarketCipher B study', async () => { 27 | indicators.CipherB = await TradingView.getIndicator('PUB;uA35GeckoTA2EfgI63SD2WCSmca4njxp'); 28 | expect(indicators.CipherB).toBeDefined(); 29 | expect(indicators.CipherB.description).toBe('VuManChu B Divergences'); 30 | 31 | indicators.CipherB.setOption('Show_WT_Hidden_Divergences', true); 32 | indicators.CipherB.setOption('Show_Stoch_Regular_Divergences', true); 33 | indicators.CipherB.setOption('Show_Stoch_Hidden_Divergences', true); 34 | 35 | expect(indicators.CipherB).toBeDefined(); 36 | }); 37 | 38 | let client: TradingView.Client; 39 | let chart: InstanceType; 40 | 41 | const noAuth = !token || !signature; 42 | 43 | it.skipIf(noAuth)('creates a client', async () => { 44 | client = new TradingView.Client({ token, signature }); 45 | expect(client).toBeDefined(); 46 | }); 47 | 48 | it.skipIf(noAuth)('creates a chart', async () => { 49 | chart = new client.Session.Chart(); 50 | expect(chart).toBeDefined(); 51 | }); 52 | 53 | it.skipIf(noAuth)('sets market', async () => { 54 | chart.setMarket('BINANCE:BTCEUR', { 55 | timeframe: '60', 56 | }); 57 | 58 | await new Promise((resolve) => { 59 | chart.onSymbolLoaded(() => { 60 | console.log('Chart loaded'); 61 | resolve(true); 62 | }); 63 | }); 64 | 65 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 66 | }); 67 | 68 | it.skipIf(noAuth).concurrent('gets performance data from SuperTrend strategy', async () => { 69 | const SuperTrend = new chart.Study(indicators.SuperTrend); 70 | 71 | let QTY = 10; 72 | const perfResult = await new Promise((resolve) => { 73 | SuperTrend.onUpdate(() => { 74 | // SuperTrend is a strategy so it sends a strategy report 75 | const perfReport = SuperTrend.strategyReport.performance; 76 | 77 | console.log('Performances:', { 78 | total: { 79 | trades: perfReport?.all?.totalTrades, 80 | perf: `${Math.round(( 81 | perfReport?.all?.netProfitPercent || 0 82 | ) * 10000) / 100} %`, 83 | }, 84 | buy: { 85 | trades: perfReport?.long?.totalTrades, 86 | perf: `${Math.round(( 87 | perfReport?.long?.netProfitPercent || 0 88 | ) * 10000) / 100} %`, 89 | }, 90 | sell: { 91 | trades: perfReport?.short?.totalTrades, 92 | perf: `${Math.round(( 93 | perfReport?.short?.netProfitPercent || 0 94 | ) * 10000) / 100} %`, 95 | }, 96 | }); 97 | 98 | if (QTY >= 50) { 99 | resolve(true); 100 | return; 101 | } 102 | 103 | QTY += 10; 104 | console.log('TRY WITH', QTY, '%'); 105 | setTimeout(() => { 106 | indicators.SuperTrend.setOption('default_qty_value', QTY); 107 | SuperTrend.setIndicator(indicators.SuperTrend); 108 | }, 1000); 109 | }); 110 | }); 111 | 112 | expect(perfResult).toBe(true); 113 | 114 | SuperTrend.remove(); 115 | }, 10000); 116 | 117 | it.skipIf(noAuth).concurrent('gets data from MarketCipher B study', async () => { 118 | const CipherB = new chart.Study(indicators.CipherB); 119 | 120 | const lastResult: any = await new Promise((resolve) => { 121 | CipherB.onUpdate(() => { 122 | resolve(CipherB.periods[0]); 123 | }); 124 | }); 125 | 126 | console.log('MarketCipher B last values:', { 127 | VWAP: Math.round(lastResult.VWAP * 1000) / 1000, 128 | moneyFlow: (lastResult.rsiMFI >= 0) ? 'POSITIVE' : 'NEGATIVE', 129 | buyCircle: lastResult.Buy_and_sell_circle && lastResult.VWAP > 0, 130 | sellCircle: lastResult.Buy_and_sell_circle && lastResult.VWAP < 0, 131 | }); 132 | 133 | expect(lastResult.VWAP).toBeTypeOf('number'); 134 | expect(lastResult.rsiMFI).toBeTypeOf('number'); 135 | expect(lastResult.Buy_and_sell_circle).toBeTypeOf('number'); 136 | 137 | CipherB.remove(); 138 | }); 139 | 140 | it.skipIf(noAuth)('removes chart', () => { 141 | console.log('Closing the chart...'); 142 | chart.delete(); 143 | }); 144 | 145 | it.skipIf(noAuth)('removes client', async () => { 146 | console.log('Closing the client...'); 147 | await client.end(); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /tests/quoteSession.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | 4 | describe('Quote session', () => { 5 | let client: TradingView.Client; 6 | let quoteSession: any; 7 | let BTC: any; 8 | 9 | it('creates client', () => { 10 | client = new TradingView.Client(); 11 | expect(client).toBeDefined(); 12 | }); 13 | 14 | it('creates quote session', () => { 15 | quoteSession = new client.Session.Quote({ fields: 'all' }); 16 | expect(quoteSession).toBeDefined(); 17 | }); 18 | 19 | it('asks for market BTCEUR', () => { 20 | BTC = new quoteSession.Market('BTCEUR'); 21 | expect(BTC).toBeDefined(); 22 | 23 | expect(new Promise((resolve) => { 24 | BTC.onLoaded(() => { 25 | console.log('BTCEUR quote loaded'); 26 | resolve(true); 27 | }); 28 | })).resolves.toBe(true); 29 | }); 30 | 31 | it('data has all properties', () => { 32 | expect(new Promise((resolve) => { 33 | BTC.onData((data) => { 34 | const rsKeys = Object.keys(data); 35 | console.log('BTCEUR data received'); 36 | if (rsKeys.length <= 2) return; 37 | resolve(rsKeys.sort()); 38 | }); 39 | })).resolves.toEqual( 40 | [ 41 | 'ask', 'bid', 'format', 42 | 'volume', 'update_mode', 'type', 'timezone', 43 | 'short_name', 'rtc_time', 'rtc', 'rchp', 'ch', 44 | 'rch', 'provider_id', 'pro_name', 'pricescale', 45 | 'prev_close_price', 'original_name', 'lp', 46 | 'open_price', 'minmove2', 'minmov', 'lp_time', 47 | 'low_price', 'is_tradable', 'high_price', 48 | 'fractional', 'exchange', 'description', 49 | 'current_session', 'currency_code', 'chp', 50 | 'currency-logoid', 'base-currency-logoid', 51 | ].sort(), 52 | ); 53 | }); 54 | 55 | it('closes quote session', () => { 56 | BTC.close(); 57 | }); 58 | 59 | it('closes client', () => { 60 | client.end(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/replayMode.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | import utils from './utils'; 4 | 5 | describe('ReplayMode', () => { 6 | let client: TradingView.Client; 7 | let chart: InstanceType; 8 | 9 | it('creates a client', async () => { 10 | client = new TradingView.Client(); 11 | expect(client).toBeDefined(); 12 | }); 13 | 14 | it('creates a chart', async () => { 15 | chart = new client.Session.Chart(); 16 | expect(chart).toBeDefined(); 17 | }); 18 | 19 | it('sets market', async () => { 20 | chart.setMarket('BINANCE:BTCEUR', { 21 | timeframe: 'D', 22 | replay: Math.round(Date.now() / 1000) - 86400 * 10, 23 | range: 1, 24 | }); 25 | 26 | await new Promise((resolve) => { 27 | chart.onSymbolLoaded(() => { 28 | console.log('Chart loaded'); 29 | resolve(true); 30 | }); 31 | }); 32 | 33 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 34 | }); 35 | 36 | it('steps forward manually', async () => { 37 | let finished = false; 38 | 39 | async function step() { 40 | if (finished) return; 41 | await chart.replayStep(1); 42 | await utils.wait(100); 43 | console.log('Replay step'); 44 | step(); 45 | } 46 | step(); 47 | 48 | chart.onReplayPoint((p: number) => { 49 | console.log('Last point ->', p); 50 | }); 51 | 52 | chart.onReplayEnd(async () => { 53 | console.log('Replay end'); 54 | finished = true; 55 | }); 56 | 57 | await new Promise((resolve) => { 58 | chart.onReplayEnd(() => { 59 | finished = true; 60 | resolve(true); 61 | }); 62 | }); 63 | 64 | expect( 65 | utils.calculateTimeGap(chart.periods), 66 | ).toBe(24 * 60 * 60); 67 | 68 | expect(chart.periods.length).toBeGreaterThanOrEqual(6); 69 | expect(chart.periods.length).toBeLessThanOrEqual(11); 70 | }); 71 | 72 | it('sets market', async () => { 73 | chart.setMarket('BINANCE:BTCEUR', { 74 | timeframe: 'D', 75 | replay: Math.round(Date.now() / 1000) - 86400 * 10, 76 | range: 1, 77 | }); 78 | 79 | await new Promise((r) => { 80 | chart.onSymbolLoaded(() => r(true)); 81 | }); 82 | console.log('Chart loaded'); 83 | 84 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 85 | }); 86 | 87 | it.skip('steps forward automatically', async () => { 88 | console.log('Play replay mode'); 89 | await chart.replayStart(200); 90 | 91 | chart.onUpdate(() => { 92 | console.log('Point ->', chart.periods[0].time); 93 | }); 94 | 95 | const msg = await Promise.race([ 96 | new Promise((r) => { 97 | chart.onReplayEnd(() => r('Replay end')); 98 | }), 99 | new Promise((r) => { 100 | setTimeout(() => r('Timeout'), 9000); 101 | }), 102 | ]); 103 | 104 | console.log(msg); 105 | 106 | expect( 107 | utils.calculateTimeGap(chart.periods), 108 | ).toBe(24 * 60 * 60); 109 | 110 | expect(chart.periods.length).toBeGreaterThanOrEqual(7); 111 | expect(chart.periods.length).toBeLessThanOrEqual(11); 112 | }); 113 | 114 | it('closes chart', async () => { 115 | console.log('Waiting 1 second...'); 116 | await utils.wait(1000); 117 | console.log('Closing the chart...'); 118 | chart.delete(); 119 | }); 120 | 121 | it('closes client', async () => { 122 | console.log('Waiting 1 second...'); 123 | await utils.wait(1000); 124 | console.log('Closing the client...'); 125 | await client.end(); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/search.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { searchMarket, searchIndicator, searchMarketV3 } from '../main'; 3 | 4 | describe('Search functions', () => { 5 | it('market search (old): "BINANCE:" has results', async () => { 6 | console.log('Searching market: "BINANCE:"'); 7 | const markets = await searchMarket('BINANCE:'); 8 | console.log('Found', markets.length, 'markets'); 9 | 10 | expect(markets.length).toBeGreaterThan(10); 11 | }); 12 | 13 | it('market search: "BINANCE:" has results', async () => { 14 | console.log('Searching market: "BINANCE:"'); 15 | const markets = await searchMarketV3('BINANCE:'); 16 | console.log('Found', markets.length, 'markets'); 17 | 18 | expect(markets.length).toBeGreaterThan(10); 19 | }); 20 | 21 | it('indicator search: "RSI" has results', async () => { 22 | console.log('Searching indicator: "RSI"'); 23 | const indicators = await searchIndicator('RSI'); 24 | console.log('Found', indicators.length, 'indicators'); 25 | 26 | expect(indicators.length).toBeGreaterThan(10); 27 | }); 28 | }); 29 | 30 | describe('Technical Analysis', () => { 31 | const SEARCHES = { 32 | // search text: expected first result 33 | 'binance:BTCUSD': 'BINANCE:BTCUSD', 34 | 'nasdaq apple': 'NASDAQ:AAPL', 35 | }; 36 | 37 | for (const marketName in SEARCHES) { 38 | it(`gets TA for '${marketName}'`, async () => { 39 | const foundMarkets = await searchMarketV3(marketName); 40 | const firstResult = foundMarkets[0]; 41 | 42 | console.log(`Market search first result for '${marketName}':`, firstResult); 43 | 44 | expect(firstResult).toBeDefined(); 45 | expect(firstResult.id).toBe(SEARCHES[marketName]); 46 | 47 | const ta = await firstResult.getTA(); 48 | expect(ta).toBeDefined(); 49 | 50 | for (const period of ['1', '5', '15', '60', '240', '1D', '1W', '1M']) { 51 | expect(ta[period]).toBeDefined(); 52 | expect(ta[period].Other).toBeDefined(); 53 | expect(ta[period].All).toBeDefined(); 54 | expect(ta[period].MA).toBeDefined(); 55 | } 56 | }); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /tests/simpleChart.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import TradingView from '../main'; 3 | import utils from './utils'; 4 | 5 | describe('Simple chart session', async () => { 6 | let client: TradingView.Client; 7 | let chart: InstanceType; 8 | 9 | it('creates a client', () => { 10 | client = new TradingView.Client(); 11 | expect(client).toBeDefined(); 12 | }); 13 | 14 | it('creates a chart session', () => { 15 | chart = new client.Session.Chart(); 16 | expect(chart).toBeDefined(); 17 | }); 18 | 19 | it('sets market', async () => { 20 | chart.setMarket('BINANCE:BTCEUR', { 21 | timeframe: 'D', 22 | }); 23 | 24 | while ( 25 | chart.infos.full_name !== 'BINANCE:BTCEUR' 26 | || chart.periods.length < 10 27 | ) await utils.wait(100); 28 | 29 | expect(chart.infos.full_name).toBe('BINANCE:BTCEUR'); 30 | expect( 31 | utils.calculateTimeGap(chart.periods), 32 | ).toBe(24 * 60 * 60); 33 | }); 34 | 35 | it('sets timeframe', async () => { 36 | console.log('Waiting 1 second...'); 37 | await utils.wait(1000); 38 | 39 | console.log('Setting timeframe to 15 minutes...'); 40 | chart.setSeries('15'); 41 | 42 | while (chart.periods.length < 10) await utils.wait(100); 43 | console.log('Chart timeframe set'); 44 | 45 | expect( 46 | utils.calculateTimeGap(chart.periods), 47 | ).toBe(15 * 60); 48 | }); 49 | 50 | it('sets chart type', async () => { 51 | console.log('Waiting 1 second...'); 52 | await utils.wait(1000); 53 | 54 | console.log('Setting the chart type to "Heikin Ashi"...'); 55 | chart.setMarket('BINANCE:ETHEUR', { 56 | timeframe: 'D', 57 | type: 'HeikinAshi', 58 | }); 59 | 60 | while (chart.infos.full_name !== 'BINANCE:ETHEUR') await utils.wait(100); 61 | 62 | console.log('Chart type set'); 63 | expect(chart.infos.full_name).toBe('BINANCE:ETHEUR'); 64 | }); 65 | 66 | it('closes chart', async () => { 67 | console.log('Waiting 1 second...'); 68 | await utils.wait(1000); 69 | console.log('Closing the chart...'); 70 | chart.delete(); 71 | }); 72 | 73 | it('closes client', async () => { 74 | console.log('Waiting 1 second...'); 75 | await utils.wait(1000); 76 | console.log('Closing the client...'); 77 | await client.end(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | export const wait = (ms: number) => ( 2 | new Promise((resolve) => { setTimeout(resolve, ms); }) 3 | ); 4 | 5 | export function calculateTimeGap(periods: { time: number }[]) { 6 | let minTimeGap = Infinity; 7 | 8 | for (let i = 1; i < periods.length; i += 1) { 9 | minTimeGap = Math.min( 10 | minTimeGap, 11 | periods[i - 1].time - periods[i].time, 12 | ); 13 | } 14 | 15 | return minTimeGap; 16 | } 17 | 18 | export default { 19 | wait, 20 | calculateTimeGap, 21 | }; 22 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: { 3 | testTimeout: 10000, 4 | retry: 3, 5 | setupFiles: 'dotenv/config', 6 | }, 7 | }; 8 | --------------------------------------------------------------------------------