diff --git a/.gitignore b/.gitignore index 286297e..c8391ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -dist node_modules .log \ No newline at end of file diff --git a/README.MD b/README.MD index 8724138..c99fc8c 100644 --- a/README.MD +++ b/README.MD @@ -7,6 +7,9 @@ Unofficial library to interact with websockets on Tradingview. - Fetching candlesticks for any symbol with any available timeframe ## Example + +### Getting candles +here is an example when you want to get a candle with a one-time function this function returns a candle that matches the symbol you provide ```ts import { connect, getCandles } from 'tradingview-ws' @@ -24,6 +27,33 @@ import { connect, getCandles } from 'tradingview-ws' }()); ``` +### Subscribe With Symbol +The following is an example of subscribing to some of the symbols that you follow. You have to send a callback to find out which symbols you are subscribing for + +```ts +import { connect, connectAndSubscribe } from 'tradingview-ws' + +(async function () { + try { + for (const iterator of ["SOLUSDT", "BTCUSDT"]) { + const connection = await connect() + connectAndSubscribe({ + connection, + symbols: [iterator], + amount: 1, + timeframe: 1, + callback: (event) => { + console.log(event) + } + }) + } + } catch (error) { + console.error(error) + } +}()); +``` + + ## API ### `connect(options: ConnectionOptions = {}): Promise` diff --git a/dist/TradingViewWebsocket.d.ts b/dist/TradingViewWebsocket.d.ts new file mode 100644 index 0000000..6f9c085 --- /dev/null +++ b/dist/TradingViewWebsocket.d.ts @@ -0,0 +1,4 @@ +declare class TradingViewWebsocker { + constructor(); +} +//# sourceMappingURL=TradingViewWebsocket.d.ts.map \ No newline at end of file diff --git a/dist/TradingViewWebsocket.d.ts.map b/dist/TradingViewWebsocket.d.ts.map new file mode 100644 index 0000000..2faa6e4 --- /dev/null +++ b/dist/TradingViewWebsocket.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"TradingViewWebsocket.d.ts","sourceRoot":"","sources":["../src/TradingViewWebsocket.ts"],"names":[],"mappings":"AAAA,cAAM,oBAAoB;;CASzB"} \ No newline at end of file diff --git a/dist/TradingViewWebsocket.js b/dist/TradingViewWebsocket.js new file mode 100644 index 0000000..17c9354 --- /dev/null +++ b/dist/TradingViewWebsocket.js @@ -0,0 +1,6 @@ +"use strict"; +class TradingViewWebsocker { + constructor() { + } +} +//# sourceMappingURL=TradingViewWebsocket.js.map \ No newline at end of file diff --git a/dist/TradingViewWebsocket.js.map b/dist/TradingViewWebsocket.js.map new file mode 100644 index 0000000..c27e700 --- /dev/null +++ b/dist/TradingViewWebsocket.js.map @@ -0,0 +1 @@ +{"version":3,"file":"TradingViewWebsocket.js","sourceRoot":"","sources":["../src/TradingViewWebsocket.ts"],"names":[],"mappings":";AAAA,MAAM,oBAAoB;IAExB;IAEA,CAAC;CAKF"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..f154dcb --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,14 @@ +import { Candle, ConnectionOptions, GetCandlesParams, TradingviewConnection } from './types'; +export declare const EVENT_NAMES: { + TIMESCALE_UPDATE: string; + SERIES_COMPLETED: string; + SYMBOL_ERROR: string; + RESOLVE_SYMBOL: string; + CREATE_SERIES: string; + REQUEST_MORE_DATA: string; +}; +export declare function connect(options?: ConnectionOptions): Promise; +export declare function getCandlesV2({ connection, symbols, amount, timeframe }: GetCandlesParams): Promise; +export declare function getCandles({ connection, symbols, amount, timeframe }: GetCandlesParams): Promise; +export declare function connectAndSubscribe({ connection, symbols, timeframe }: GetCandlesParams): Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map new file mode 100644 index 0000000..521015d --- /dev/null +++ b/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,gBAAgB,EAAyD,qBAAqB,EAAgB,MAAM,SAAS,CAAA;AAEjK,eAAO,MAAM,WAAW;;;;;;;CAOvB,CAAA;AAmBD,wBAAsB,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAI,OAAO,CAAC,qBAAqB,CAAC,CAkE9F;AAED,wBAAsB,YAAY,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,MAAa,EAAE,SAAgB,EAAE,EAAE,gBAAgB,sBAwB5G;AAED,wBAAsB,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,SAAc,EAAE,EAAE,gBAAgB,uBAoFjG;AAID,wBAAsB,mBAAmB,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,SAAa,EAAE,EAAE,gBAAgB,gCA8BjG"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..bca1e09 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,253 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.connectAndSubscribe = exports.getCandles = exports.getCandlesV2 = exports.connect = exports.EVENT_NAMES = void 0; +const axios_1 = __importDefault(require("axios")); +const ws_1 = __importDefault(require("ws")); +const randomstring_1 = __importDefault(require("randomstring")); +const types_1 = require("./types"); +exports.EVENT_NAMES = { + TIMESCALE_UPDATE: 'timescale_update', + SERIES_COMPLETED: 'series_completed', + SYMBOL_ERROR: 'symbol_error', + RESOLVE_SYMBOL: 'resolve_symbol', + CREATE_SERIES: 'create_series', + REQUEST_MORE_DATA: 'request_more_data', +}; +function parseMessage(message) { + if (message.length === 0) + return []; + const events = message.toString().split(/~m~\d+~m~/).slice(1); + const s = events.map(event => { + if (event.substring(0, 3) === "~h~") { + return { type: 'ping', data: `~m~${event.length}~m~${event}` }; + } + const parsed = JSON.parse(event); + if (parsed['session_id']) { + return { type: 'session', data: parsed }; + } + return { type: 'event', data: parsed }; + }); + return s; +} +function connect(options = {}) { + return __awaiter(this, void 0, void 0, function* () { + let token = 'unauthorized_user_token'; + if (options.sessionId) { + const resp = yield (0, axios_1.default)({ + method: 'get', + url: 'https://www.tradingview.com/disclaimer/', + headers: { "Cookie": `sessionid=${options.sessionId}` } + }); + token = resp.data.match(/"auth_token":"(.+?)"/)[1]; + } + const connection = new ws_1.default("wss://prodata.tradingview.com/socket.io/websocket", { + origin: "https://prodata.tradingview.com" + }).setMaxListeners(400); + const subscribers = new Set(); + function subscribe(handler) { + subscribers.add(handler); + return () => { + subscribers.delete(handler); + }; + } + function send(name, params) { + const data = JSON.stringify({ m: name, p: params }); + const message = "~m~" + data.length + "~m~" + data; + connection.send(message); + } + function close() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + connection.on('close', resolve); + connection.on('error', reject); + connection.close(); + }); + }); + } + return new Promise((resolve, reject) => { + connection.on('error', error => reject(error)); + connection.on('message', message => { + const payloads = parseMessage(message.toString()); + for (const payload of payloads) { + switch (payload.type) { + case 'ping': + connection.send(payload.data); + break; + case 'session': + send('set_auth_token', [token]); + resolve({ subscribe, send, close }); + break; + case 'event': + const event = { + name: payload.data.m, + params: payload.data.p, + }; + subscribers.forEach(handler => handler(event)); + break; + default: + throw new Error(`unknown payload: ${payload}`); + } + } + }); + }); + }); +} +exports.connect = connect; +function getCandlesV2({ connection, symbols, amount = 1000, timeframe = '1D' }) { + return __awaiter(this, void 0, void 0, function* () { + if (symbols.length === 0) + return []; // at most make 10 requests every second, but evenly spaced. + const d = symbols.map(symbol => new Promise((resolve, reject) => { + const chartSession = "cs_" + randomstring_1.default.generate(12); + const batchSize = amount && amount < types_1.MAX_BATCH_SIZE ? amount : types_1.MAX_BATCH_SIZE; + connection.send('chart_create_session', [chartSession, '']); + connection.send('resolve_symbol', [ + chartSession, + `sds_sym_0`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]); + connection.send('create_series', [ + chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), batchSize, '' + ]); + connection.subscribe(({ name, params }) => { + var _a, _b; + if (name === "timescale_update") { + resolve((_b = (_a = params[1]) === null || _a === void 0 ? void 0 : _a.sds_1) === null || _b === void 0 ? void 0 : _b.s); + } + if (name === "symbol_error") { + resolve([]); + } + }); + })); + return yield Promise.all(d); + }); +} +exports.getCandlesV2 = getCandlesV2; +function getCandles({ connection, symbols, amount, timeframe = 60 }) { + return __awaiter(this, void 0, void 0, function* () { + if (symbols.length === 0) + return []; + const chartSession = "cs_" + randomstring_1.default.generate(12); + const batchSize = amount && amount < types_1.MAX_BATCH_SIZE ? amount : types_1.MAX_BATCH_SIZE; + return new Promise(resolve => { + const allCandles = []; + let currentSymCandles = []; + let currentSymIndex = 0; + let symbol = symbols[currentSymIndex]; + connection.send('chart_create_session', [chartSession, '']); + connection.send('resolve_symbol', [ + chartSession, + `sds_sym_0`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]); + connection.send('create_series', [ + chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), batchSize, '' + ]); + const unsubscribe = connection.subscribe(event => { + // received new candles + if (event.name === 'timescale_update') { + let newCandles = event.params[1]['sds_1']['s']; + if (newCandles.length > batchSize) { + // sometimes tradingview sends already received candles + newCandles = newCandles.slice(0, -currentSymCandles.length); + } + currentSymCandles = newCandles.concat(currentSymCandles); + return; + } + // loaded all requested candles + if (['series_completed', 'symbol_error'].includes(event.name)) { + const loadedCount = currentSymCandles.length; + if (loadedCount > 0 && loadedCount % batchSize === 0 && (!amount || loadedCount < amount)) { + connection.send('request_more_data', [chartSession, 'sds_1', batchSize]); + return; + } + // loaded all candles for current symbol + if (amount) + currentSymCandles = currentSymCandles.slice(0, amount); + const candles = currentSymCandles.map(c => ({ + timestamp: c.v[0], + open: c.v[1], + high: c.v[2], + low: c.v[3], + close: c.v[4], + volume: c.v[5] + })); + allCandles.push(candles); + // next symbol + if (symbols.length - 1 > currentSymIndex) { + currentSymCandles = []; + currentSymIndex += 1; + symbol = symbols[currentSymIndex]; + connection.send('resolve_symbol', [ + chartSession, + `sds_sym_${currentSymIndex}`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]); + connection.send('modify_series', [ + chartSession, + 'sds_1', + `s${currentSymIndex}`, + `sds_sym_${currentSymIndex}`, + timeframe.toString(), + '' + ]); + return; + } + // all symbols loaded + unsubscribe(); + resolve(allCandles); + } + }); + }); + }); +} +exports.getCandles = getCandles; +function connectAndSubscribe({ connection, symbols, timeframe = 1 }) { + return __awaiter(this, void 0, void 0, function* () { + if (symbols.length === 0) + return []; + const chartSession = "cs_" + randomstring_1.default.generate(12); + const currentSymIndex = 0; + const symbol = symbols[currentSymIndex]; + connection.send('chart_create_session', [chartSession, '']); + connection.send(exports.EVENT_NAMES.RESOLVE_SYMBOL, [ + chartSession, + `sds_sym_0`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]); + connection.send(exports.EVENT_NAMES.CREATE_SERIES, [ + chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), 1, '' + ]); + connection.subscribe((event) => { + var _a; + if (event.name = "event") { + if (((_a = event.params.p[1].sds_1) === null || _a === void 0 ? void 0 : _a.s) !== undefined) { + const candles = { + timestamp: event.params.p[1].sds_1.s[0].v[0], + open: event.params.p[1].sds_1.s[0].v[1], + high: event.params.p[1].sds_1.s[0].v[2], + low: event.params.p[1].sds_1.s[0].v[3], + close: event.params.p[1].sds_1.s[0].v[4], + volume: event.params.p[1].sds_1.s[0].v[5], + symbol + }; + // callback(candles) + } + } + }); + }); +} +exports.connectAndSubscribe = connectAndSubscribe; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..045f950 --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/types.d.ts b/dist/types.d.ts new file mode 100644 index 0000000..7884294 --- /dev/null +++ b/dist/types.d.ts @@ -0,0 +1,40 @@ +export declare const MAX_BATCH_SIZE = 5000; +export declare type Subscriber = (event: TradingviewEvent) => void; +export declare type Unsubscriber = () => void; +export declare type MessageType = 'ping' | 'session' | 'event'; +export interface RawCandle { + i: number; + v: number[]; +} +export interface GetCandlesParams { + connection: TradingviewConnection; + symbols: string[]; + amount?: number; + timeframe?: TradingviewTimeframe; +} +export interface Candle { + timestamp: number; + high: number; + low: number; + open: number; + close: number; + volume: number; +} +export interface MessagePayload { + type: MessageType; + data: any; +} +export interface TradingviewConnection { + subscribe: (handler: Subscriber) => Unsubscriber; + send: (name: string, params: any[]) => void; + close: () => Promise; +} +export interface ConnectionOptions { + sessionId?: string; +} +export interface TradingviewEvent { + name: string; + params: any[]; +} +export declare type TradingviewTimeframe = number | '1D' | '1W' | '1M'; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/dist/types.d.ts.map b/dist/types.d.ts.map new file mode 100644 index 0000000..7161514 --- /dev/null +++ b/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,oBAAY,UAAU,GAAG,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAC1D,oBAAY,YAAY,GAAG,MAAM,IAAI,CAAA;AACrC,oBAAY,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEtD,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,EAAE,CAAA;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,qBAAqB,CAAC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,oBAAoB,CAAA;CAEjC;AACD,MAAM,WAAW,MAAM;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,GAAG,CAAA;CACV;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,YAAY,CAAA;IAChD,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC3C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,GAAG,EAAE,CAAC;CACf;AAED,oBAAY,oBAAoB,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA"} \ No newline at end of file diff --git a/dist/types.js b/dist/types.js new file mode 100644 index 0000000..093f5a6 --- /dev/null +++ b/dist/types.js @@ -0,0 +1,5 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MAX_BATCH_SIZE = void 0; +exports.MAX_BATCH_SIZE = 5000; // found experimentally +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/dist/types.js.map b/dist/types.js.map new file mode 100644 index 0000000..65ff75e --- /dev/null +++ b/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAAa,QAAA,cAAc,GAAG,IAAI,CAAA,CAAC,uBAAuB"} \ No newline at end of file diff --git a/example.ts b/example.ts new file mode 100644 index 0000000..b51e5d9 --- /dev/null +++ b/example.ts @@ -0,0 +1,28 @@ +import { connect, getCandles, getCandlesV2, } from './src/index' + + +(async function () { + + const data = ["TLKM","UNVR","ADRO","ICBP","ANTM","INDF","PGAS","PTBA","KLBF","WIKA","ADHI","JPFA","ITMG","MYOR","AALI","MNCN","UNTR","PTPP","CTRA","SMGR","BRIS","CPIN","PWON","BSDE","INTP","SIDO","HRUM","LPPF","SMRA","INCO","SCMA","LSIP","EXCL","KAEF","TINS","ERAA","FREN","ACES","INKP","ELSA","MDKA","TKIM","AKRA","BRPT","KIJA","ISAT","BTPS","BMTR","EMTK","ULTJ","BRMS","ASRI","INAF","DMAS","SMDR","MIKA","MAPI","GJTL","ENRG","WTON","PTRO","MPMX","RALS","TPIA","WEGE","APLN","MLPL","IRRA","SMBR","BANK","CLEO","MTDL","AGII","HOKI","AUTO","POWR","LPKR","FILM","MPPA","PNBS","DMMX","WOOD","BKSL","RAJA","BEST","ISSP","SIMP","GOOD","MAIN","WIRG","IPTV","SOCI","HEXA","NIKL","BIRD","SILO","SAME","BSSR","BOSS","EKAD","MBAP","IPCC","LINK","MBSS","SMMT","WMUU","MTEL","KPIG","DKFT","GZCO","TOTL","AISA","FPNI","FIRE","KBLI","TMAS","TSPC","ACST","CAMP","PALM","PSAB","KINO","MSIN","SMSM","LPCK","DEWA","PTSN","SSIA","JAST","DSNG","YELO","ROTI","DILD","PSSI","TAPG","KREN","JRPT","PURA","SGRO","TGRA","PRDA","JAYA","ARNA","LUCK","MERK","PEHA","MARK","DGNS","MLPT","KIOS","SLIS","MCAS","KKGI","TOYS","HEAL","ABMM","INDR","NICL","BUDI","CEKA","PPRE","BYAN","ADES","IPCM","BISI","KBAG","MMLP","REAL","CCSI","TOTO","META","TRUK","SGER","CAKK","ASGR","CSRA","COAL","MYOH","KOBX","TCPI","BEBS","ADMG","PZZA","DGIK","MAPA","FAST","MLIA","IKAN","GEMS","ESIP","WINS","TRIN","PBID","MRAT","DVLA","AYLS","SPMA","JKON","BMHS","TNCA","SOHO","BCIP","LAND","RANC","PKPK","FOOD","GDST","UCID","NELY","RBMS","NRCA","SMCB","WEHA","TECH","LTLS","MITI","TPMA","HDIT","PTDU","NFCX","IPPE","BAPA","INDX","ASHA","RIGS","PANI","MDKI","MPOW","MSKY","KOTA","HAIS","CSIS","BMSR","ZYRX","SHIP","DSFI","KEEN","AVIA","CLPI","SPTO","GPRA","HOPE","ANJT","KEJU","WIFI","PRIM","PSKT","PAMG","RODA","ITMA","HRME","KAYU","DMND","ALDO","TRUE","STTP","SAMF","CMNP","IGAR","DIVA","UNIQ","TRJA","KBLM","ARCI","OPMS","ADCP","RMKE","IPOL","APEX","ZBRA","EDGE","EPMT","BELL","BTON","DADA","EAST","BAUT","TFAS","CMRY","TOPS","COCO","ANDI","BESS","PJAA","LPIN","IKAI","INCI","ASLC","MTPS","MAPB","HERO","ARII","ICON","DRMA","CASS","UNIC","GDYR","WINR","SMDM","SICO","MCOL","DIGI","KICI","DSSA","MORA","SRAJ","TEBE","MTLA","KARW","CBMF","HATM","SCCO","BOGA","SEMA","SBMA","GWSA","BUKK","KDSI","TAMU","LABA","TCID","STAA","OILS","AMFG","GGRP","KRYA","INDS","CITA","GTSI","PCAR","MASA","IMPC","EPAC","KJEN","DWGL","ENZO","NPGF","TRIS","OBMD","AKSI","TBMS","IFII","MFMI","AXIO","TGKA","SMKL","AKPI","IDPR","MINA","BLUE","IIKP","NASI","DUTI","GOLD","VOKS","GLVA","JTPE","ATAP","TAMA","MICE","PBSA","UFOE","KIAS","CSAP","PMJS","LION","BAPI","WAPO","BBSS","NTBK","MEDS","CANI","ESTI","DPNS","LRNA","VICI","SAPX","GHON","WMPP","SKBM","MBTO","PLIN","ASPI","NZIA","IFSH","MIDI","CINT","APII","URBN","LMPI","BKDP","HITS","DEWI","AIMS","KOPI","BIPP","FITT","TMPO","BATA","CITY","BRAM","SURE","SIPD","INTD","BOLT","ALKA","TARA","SKLT","KUAS","PORT","GPSO","CRAB","YPAS","FISH","SDPC","MTMH","LMSH","SOSS","APLI","TOOL","PTPW","RISE","KONI","SHID","PGLI","MTFN","IKBI","ZONE","AMIN","BIKE","CPRI","JIHD","TRST","JECC","GAMA","MTSM","TFCO","SCNP","BLTA","SINI","BBRM","BIKA","MKPI","SCPI","KOIN","ENAK","IBST","ALMI","RDTX","SNLK","MIRA","BRNA","CTBN","SOTS","MKNT","CHEM","TURI","BAYU","ECII","FMII","OMRE","PGUN","DEPO","KMDS","DAYA","KKES","PURI","BLTZ","HOMI","BOBA","SWID","GMTD","LCKM","SSTM","PUDP","PTSP","AMAN","GEMA","JSPT","RSGK","CSMI","EMDE","AGAR","PNSE","JGLE","BINO","MGNA","TAYS","RAFI","ELPI","BUAH","GULA","JMAS"] + try { + + const start = new Date().getTime(); + const connection = await connect(); + const candles = await getCandlesV2({ + connection, + symbols: data.map(res => `IDX:${res}`), + amount: 2, + timeframe: "1D", + }) + console.log(candles) + const end = new Date().getTime(); + const time = end - start; + console.log('Execution time: ' + time); + + connection.close() + + } catch (error) { + console.error(error) + } +}()); + diff --git a/package-lock.json b/package-lock.json index d3c4493..1cb0ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tradingview-ws", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/index.ts b/src/index.ts index 27fa792..233b2f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,73 +1,38 @@ import axios from 'axios' -import WebSocket from 'ws' +import WebSocket, { EventEmitter } from 'ws' import randomstring from "randomstring" - -const MAX_BATCH_SIZE = 5000 // found experimentally - -type Subscriber = (event: TradingviewEvent) => void -type Unsubscriber = () => void - -type MessageType = 'ping' | 'session' | 'event' - -interface RawCandle { - i: number - v: number[] +import { Candle, ConnectionOptions, GetCandlesParams, MAX_BATCH_SIZE, MessagePayload, RawCandle, Subscriber, TradingviewConnection, Unsubscriber } from './types' + +export const EVENT_NAMES = { + TIMESCALE_UPDATE: 'timescale_update', + SERIES_COMPLETED: 'series_completed', + SYMBOL_ERROR: 'symbol_error', + RESOLVE_SYMBOL: 'resolve_symbol', + CREATE_SERIES: 'create_series', + REQUEST_MORE_DATA: 'request_more_data', } -export interface Candle { - timestamp: number - high: number - low: number - open: number - close: number - volume: number -} - -interface MessagePayload { - type: MessageType - data: any -} - -interface TradingviewConnection { - subscribe: (handler: Subscriber) => Unsubscriber - send: (name: string, params: any[]) => void - close: () => Promise -} - -interface ConnectionOptions { - sessionId?: string -} - -interface TradingviewEvent { - name: string, - params: any[] -} - -type TradingviewTimeframe = number | '1D' | '1W' | '1M' - function parseMessage(message: string): MessagePayload[] { if (message.length === 0) return [] - const events = message.toString().split(/~m~\d+~m~/).slice(1) - - return events.map(event => { + const s: MessagePayload[] = events.map(event => { if (event.substring(0, 3) === "~h~") { return { type: 'ping', data: `~m~${event.length}~m~${event}` } } - const parsed = JSON.parse(event) - if (parsed['session_id']) { return { type: 'session', data: parsed } } - return { type: 'event', data: parsed } }) + + return s } -export async function connect(options: ConnectionOptions = {}): Promise { +export async function connect(options: ConnectionOptions = {},): Promise { let token = 'unauthorized_user_token' + if (options.sessionId) { const resp = await axios({ method: 'get', @@ -79,7 +44,7 @@ export async function connect(options: ConnectionOptions = {}): Promise = new Set() @@ -106,10 +71,8 @@ export async function connect(options: ConnectionOptions = {}): Promise((resolve, reject) => { connection.on('error', error => reject(error)) - connection.on('message', message => { const payloads = parseMessage(message.toString()) - for (const payload of payloads) { switch (payload.type) { case 'ping': @@ -122,7 +85,7 @@ export async function connect(options: ConnectionOptions = {}): Promise handler(event)) break; @@ -134,11 +97,30 @@ export async function connect(options: ConnectionOptions = {}): Promise new Promise((resolve, reject) => { + const chartSession = "cs_" + randomstring.generate(12) + const batchSize = amount && amount < MAX_BATCH_SIZE ? amount : MAX_BATCH_SIZE + connection.send('chart_create_session', [chartSession, '']) + connection.send('resolve_symbol', [ + chartSession, + `sds_sym_0`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]) + connection.send('create_series', [ + chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), batchSize, '' + ]) + connection.subscribe(({ name, params }) => { + if (name === "timescale_update") { + resolve(params[1]?.sds_1?.s) + } + if (name === "symbol_error") { + resolve([]) + } + }) + })) + return await Promise.all(d) } export async function getCandles({ connection, symbols, amount, timeframe = 60 }: GetCandlesParams) { @@ -149,9 +131,19 @@ export async function getCandles({ connection, symbols, amount, timeframe = 60 } return new Promise(resolve => { const allCandles: Candle[][] = [] + let currentSymCandles: RawCandle[] = [] let currentSymIndex = 0 let symbol = symbols[currentSymIndex] - let currentSymCandles: RawCandle[] = [] + connection.send('chart_create_session', [chartSession, '']) + connection.send('resolve_symbol', [ + chartSession, + `sds_sym_0`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]) + + connection.send('create_series', [ + chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), batchSize, '' + ]) const unsubscribe = connection.subscribe(event => { // received new candles @@ -214,15 +206,39 @@ export async function getCandles({ connection, symbols, amount, timeframe = 60 } resolve(allCandles) } }) + }) +} - connection.send('chart_create_session', [chartSession, '']) - connection.send('resolve_symbol', [ - chartSession, - `sds_sym_0`, - '=' + JSON.stringify({ symbol, adjustment: 'splits' }) - ]) - connection.send('create_series', [ - chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), batchSize, '' - ]) + + +export async function connectAndSubscribe({ connection, symbols, timeframe = 1 }: GetCandlesParams) { + if (symbols.length === 0) return [] + const chartSession = "cs_" + randomstring.generate(12) + const currentSymIndex = 0 + const symbol = symbols[currentSymIndex] + connection.send('chart_create_session', [chartSession, '']) + connection.send(EVENT_NAMES.RESOLVE_SYMBOL, [ + chartSession, + `sds_sym_0`, + '=' + JSON.stringify({ symbol, adjustment: 'splits' }) + ]) + connection.send(EVENT_NAMES.CREATE_SERIES, [ + chartSession, 'sds_1', 's0', 'sds_sym_0', timeframe.toString(), 1, '' + ]) + connection.subscribe((event: any) => { + if (event.name = "event") { + if (event.params.p[1].sds_1?.s !== undefined) { + const candles = { + timestamp: event.params.p[1].sds_1.s[0].v[0], + open: event.params.p[1].sds_1.s[0].v[1], + high: event.params.p[1].sds_1.s[0].v[2], + low: event.params.p[1].sds_1.s[0].v[3], + close: event.params.p[1].sds_1.s[0].v[4], + volume: event.params.p[1].sds_1.s[0].v[5], + symbol + } + // callback(candles) + } + } }) } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9077394 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,48 @@ +export const MAX_BATCH_SIZE = 5000 // found experimentally + +export type Subscriber = (event: TradingviewEvent) => void +export type Unsubscriber = () => void +export type MessageType = 'ping' | 'session' | 'event' + +export interface RawCandle { + i: number + v: number[] +} + +export interface GetCandlesParams { + connection: TradingviewConnection, + symbols: string[], + amount?: number + timeframe?: TradingviewTimeframe + // callback: (event: any) => void +} +export interface Candle { + timestamp: number + high: number + low: number + open: number + close: number + volume: number +} + +export interface MessagePayload { + type: MessageType + data: any +} + +export interface TradingviewConnection { + subscribe: (handler: Subscriber) => Unsubscriber + send: (name: string, params: any[]) => void + close: () => Promise +} + +export interface ConnectionOptions { + sessionId?: string +} + +export interface TradingviewEvent { + name: string, + params: any[], +} + +export type TradingviewTimeframe = number | '1D' | '1W' | '1M' \ No newline at end of file