UNPKG

@mathieuc/tradingview

Version:

Tradingview instant stocks API, indicator alerts, trading bot, and more !

554 lines (470 loc) 17 kB
const { genSessionID } = require('../utils'); const studyConstructor = require('./study'); /** * @typedef {'HeikinAshi' | 'Renko' | 'LineBreak' | 'Kagi' | 'PointAndFigure' * | 'Range'} ChartType Custom chart type */ const ChartTypes = { HeikinAshi: 'BarSetHeikenAshi@tv-basicstudies-60!', Renko: 'BarSetRenko@tv-prostudies-40!', LineBreak: 'BarSetPriceBreak@tv-prostudies-34!', Kagi: 'BarSetKagi@tv-prostudies-34!', PointAndFigure: 'BarSetPnF@tv-prostudies-34!', Range: 'BarSetRange@tv-basicstudies-72!', }; /** * @typedef {Object} ChartInputs Custom chart type * @prop {number} [atrLength] Renko/Kagi/PointAndFigure ATR length * @prop {'open' | 'high' | 'low' | 'close' | 'hl2' * | 'hlc3' | 'ohlc4'} [source] Renko/LineBreak/Kagi source * @prop {'ATR' | string} [style] Renko/Kagi/PointAndFigure style * @prop {number} [boxSize] Renko/PointAndFigure box size * @prop {number} [reversalAmount] Kagi/PointAndFigure reversal amount * @prop {'Close'} [sources] Renko/PointAndFigure sources * @prop {boolean} [wicks] Renko wicks * @prop {number} [lb] LineBreak Line break * @prop {boolean} [oneStepBackBuilding] PointAndFigure oneStepBackBuilding * @prop {boolean} [phantomBars] Range phantom bars * @prop {number} [range] Range range */ /** @typedef {Object<string, Function[]>} StudyListeners */ /** * @typedef {Object} ChartSessionBridge * @prop {string} sessionID * @prop {StudyListeners} studyListeners * @prop {Object<number, number>} indexes * @prop {import('../client').SendPacket} send */ /** * @typedef {'seriesLoaded' | 'symbolLoaded' | 'update' | 'error'} ChartEvent */ /** * @typedef {Object} PricePeriod * @prop {number} time Period timestamp * @prop {number} open Period open value * @prop {number} close Period close value * @prop {number} max Period max value * @prop {number} min Period min value * @prop {number} volume Period volume value */ /** * @typedef {Object} Subsession * @prop {string} id Subsession ID (ex: 'regular') * @prop {string} description Subsession description (ex: 'Regular') * @prop {boolean} private If private * @prop {string} session Session (ex: '24x7') * @prop {string} session-correction Session correction * @prop {string} session-display Session display (ex: '24x7') */ /** * @typedef {Object} MarketInfos * @prop {string} series_id Used series (ex: 'ser_1') * @prop {string} base_currency Base currency (ex: 'BTC') * @prop {string} base_currency_id Base currency ID (ex: 'XTVCBTC') * @prop {string} name Market short name (ex: 'BTCEUR') * @prop {string} full_name Market full name (ex: 'COINBASE:BTCEUR') * @prop {string} pro_name Market pro name (ex: 'COINBASE:BTCEUR') * @prop {string} description Market symbol description (ex: 'BTC/EUR') * @prop {string} short_description Market symbol short description (ex: 'BTC/EUR') * @prop {string} exchange Market exchange (ex: 'COINBASE') * @prop {string} listed_exchange Market exchange (ex: 'COINBASE') * @prop {string} provider_id Values provider ID (ex: 'coinbase') * @prop {string} currency_id Used currency ID (ex: 'EUR') * @prop {string} currency_code Used currency code (ex: 'EUR') * @prop {string} variable_tick_size Variable tick size * @prop {number} pricescale Price scale * @prop {number} pointvalue Point value * @prop {string} session Session (ex: '24x7') * @prop {string} session_display Session display (ex: '24x7') * @prop {string} type Market type (ex: 'crypto') * @prop {boolean} has_intraday If intraday values are available * @prop {boolean} fractional If market is fractional * @prop {boolean} is_tradable If the market is curently tradable * @prop {number} minmov Minimum move value * @prop {number} minmove2 Minimum move value 2 * @prop {string} timezone Used timezone * @prop {boolean} is_replayable If the replay mode is available * @prop {boolean} has_adjustment If the adjustment mode is enabled * @prop {boolean} has_extended_hours Has extended hours * @prop {string} bar_source Bar source * @prop {string} bar_transform Bar transform * @prop {boolean} bar_fillgaps Bar fill gaps * @prop {string} allowed_adjustment Allowed adjustment (ex: 'none') * @prop {string} subsession_id Subsession ID (ex: 'regular') * @prop {string} pro_perm Pro permission (ex: '') * @prop {[]} base_name Base name (ex: ['COINBASE:BTCEUR']) * @prop {[]} legs Legs (ex: ['COINBASE:BTCEUR']) * @prop {Subsession[]} subsessions Sub sessions * @prop {[]} typespecs Typespecs (ex: []) * @prop {[]} resolutions Resolutions (ex: []) * @prop {[]} aliases Aliases (ex: []) * @prop {[]} alternatives Alternatives (ex: []) */ /** * @param {import('../client').ClientBridge} client */ module.exports = (client) => class ChartSession { #chartSessionID = genSessionID('cs'); #replaySessionID = genSessionID('rs'); #replayMode = false; /** @type {Object<string, (): any>} */ #replayOKCB = {}; /** Parent client */ #client = client; /** @type {StudyListeners} */ #studyListeners = {}; /** * Table of periods values indexed by timestamp * @type {Object<number, PricePeriod[]>} */ #periods = {}; /** @return {PricePeriod[]} List of periods values */ get periods() { return Object.values(this.#periods).sort((a, b) => b.time - a.time); } /** * Current market infos * @type {MarketInfos} */ #infos = {}; /** @return {MarketInfos} Current market infos */ get infos() { return this.#infos; } #callbacks = { seriesLoaded: [], symbolLoaded: [], update: [], replayLoaded: [], replayPoint: [], replayResolution: [], replayEnd: [], event: [], error: [], }; /** * @param {ChartEvent} ev Client event * @param {...{}} data Packet data */ #handleEvent(ev, ...data) { this.#callbacks[ev].forEach((e) => e(...data)); this.#callbacks.event.forEach((e) => e(ev, ...data)); } #handleError(...msgs) { if (this.#callbacks.error.length === 0) console.error(...msgs); else this.#handleEvent('error', ...msgs); } constructor() { this.#client.sessions[this.#chartSessionID] = { type: 'chart', onData: (packet) => { if (global.TW_DEBUG) console.log('§90§30§106 CHART SESSION §0 DATA', packet); if (typeof packet.data[1] === 'string' && this.#studyListeners[packet.data[1]]) { this.#studyListeners[packet.data[1]](packet); return; } if (packet.type === 'symbol_resolved') { this.#infos = { series_id: packet.data[1], ...packet.data[2], }; this.#handleEvent('symbolLoaded'); return; } if (['timescale_update', 'du'].includes(packet.type)) { const changes = []; Object.keys(packet.data[1]).forEach((k) => { changes.push(k); if (k === '$prices') { const periods = packet.data[1].$prices; if (!periods || !periods.s) return; periods.s.forEach((p) => { [this.#chartSession.indexes[p.i]] = p.v; this.#periods[p.v[0]] = { time: p.v[0], open: p.v[1], close: p.v[4], max: p.v[2], min: p.v[3], volume: Math.round(p.v[5] * 100) / 100, }; }); return; } if (this.#studyListeners[k]) this.#studyListeners[k](packet); }); this.#handleEvent('update', changes); return; } if (packet.type === 'symbol_error') { this.#handleError(`(${packet.data[1]}) Symbol error:`, packet.data[2]); return; } if (packet.type === 'series_error') { this.#handleError('Series error:', packet.data[3]); return; } if (packet.type === 'critical_error') { const [, name, description] = packet.data; this.#handleError('Critical error:', name, description); } }, }; this.#client.sessions[this.#replaySessionID] = { type: 'replay', onData: (packet) => { if (global.TW_DEBUG) console.log('§90§30§106 REPLAY SESSION §0 DATA', packet); if (packet.type === 'replay_ok') { if (this.#replayOKCB[packet.data[1]]) { this.#replayOKCB[packet.data[1]](); delete this.#replayOKCB[packet.data[1]]; } return; } if (packet.type === 'replay_instance_id') { this.#handleEvent('replayLoaded', packet.data[1]); return; } if (packet.type === 'replay_point') { this.#handleEvent('replayPoint', packet.data[1]); return; } if (packet.type === 'replay_resolutions') { this.#handleEvent('replayResolution', packet.data[1], packet.data[2]); return; } if (packet.type === 'replay_data_end') { this.#handleEvent('replayEnd'); return; } if (packet.type === 'critical_error') { const [, name, description] = packet.data; this.#handleError('Critical error:', name, description); } }, }; this.#client.send('chart_create_session', [this.#chartSessionID]); } #seriesCreated = false; #currentSeries = 0; /** * @param {import('../types').TimeFrame} timeframe Chart period timeframe * @param {number} [range] Number of loaded periods/candles (Default: 100) * @param {number} [reference] Reference candle timestamp (Default is now) */ setSeries(timeframe = '240', range = 100, reference = null) { if (!this.#currentSeries) { this.#handleError('Please set the market before setting series'); return; } const calcRange = !reference ? range : ['bar_count', reference, range]; this.#periods = {}; this.#client.send(`${this.#seriesCreated ? 'modify' : 'create'}_series`, [ this.#chartSessionID, '$prices', 's1', `ser_${this.#currentSeries}`, timeframe, this.#seriesCreated ? '' : calcRange, ]); this.#seriesCreated = true; } /** * Set the chart market * @param {string} symbol Market symbol * @param {Object} [options] Chart options * @param {import('../types').TimeFrame} [options.timeframe] Chart period timeframe * @param {number} [options.range] Number of loaded periods/candles (Default: 100) * @param {number} [options.to] Last candle timestamp (Default is now) * @param {'splits' | 'dividends'} [options.adjustment] Market adjustment * @param {boolean} [options.backadjustment] Market backadjustment of futures contracts * @param {'regular' | 'extended'} [options.session] Chart session * @param {'EUR' | 'USD' | string} [options.currency] Chart currency * @param {ChartType} [options.type] Chart custom type * @param {ChartInputs} [options.inputs] Chart custom inputs * @param {number} [options.replay] Replay mode starting point (Timestamp) */ setMarket(symbol, options = {}) { this.#periods = {}; if (this.#replayMode) { this.#replayMode = false; this.#client.send('replay_delete_session', [this.#replaySessionID]); } const symbolInit = { symbol: symbol || 'BTCEUR', adjustment: options.adjustment || 'splits', }; if (options.backadjustment) symbolInit.backadjustment = 'default'; if (options.session) symbolInit.session = options.session; if (options.currency) symbolInit['currency-id'] = options.currency; if (options.replay) { if (!this.#replayMode) { this.#replayMode = true; this.#client.send('replay_create_session', [this.#replaySessionID]); } this.#client.send('replay_add_series', [ this.#replaySessionID, 'req_replay_addseries', `=${JSON.stringify(symbolInit)}`, options.timeframe, ]); this.#client.send('replay_reset', [ this.#replaySessionID, 'req_replay_reset', options.replay, ]); } const complex = options.type || options.replay; const chartInit = complex ? {} : symbolInit; if (complex) { if (options.replay) chartInit.replay = this.#replaySessionID; chartInit.symbol = symbolInit; chartInit.type = ChartTypes[options.type]; if (options.type) chartInit.inputs = { ...options.inputs }; } this.#currentSeries += 1; this.#client.send('resolve_symbol', [ this.#chartSessionID, `ser_${this.#currentSeries}`, `=${JSON.stringify(chartInit)}`, ]); this.setSeries(options.timeframe, options.range, options.to); } /** * Set the chart timezone * @param {import('../types').Timezone} timezone New timezone */ setTimezone(timezone) { this.#periods = {}; this.#client.send('switch_timezone', [this.#chartSessionID, timezone]); } /** * Fetch x additional previous periods/candles values * @param {number} number Number of additional periods/candles you want to fetch */ fetchMore(number = 1) { this.#client.send('request_more_data', [this.#chartSessionID, '$prices', number]); } /** * Fetch x additional previous periods/candles values * @param {number} number Number of additional periods/candles you want to fetch * @returns {Promise} Raise when the data has been fetched */ replayStep(number = 1) { return new Promise((cb) => { if (!this.#replayMode) { this.#handleError('No replay session'); return; } const reqID = genSessionID('rsq_step'); this.#client.send('replay_step', [this.#replaySessionID, reqID, number]); this.#replayOKCB[reqID] = () => { cb(); }; }); } /** * Start fetching a new period/candle every x ms * @param {number} interval Number of additional periods/candles you want to fetch * @returns {Promise} Raise when the replay mode starts */ replayStart(interval = 1000) { return new Promise((cb) => { if (!this.#replayMode) { this.#handleError('No replay session'); return; } const reqID = genSessionID('rsq_start'); this.#client.send('replay_start', [this.#replaySessionID, reqID, interval]); this.#replayOKCB[reqID] = () => { cb(); }; }); } /** * Stop fetching a new period/candle every x ms * @returns {Promise} Raise when the replay mode stops */ replayStop() { return new Promise((cb) => { if (!this.#replayMode) { this.#handleError('No replay session'); return; } const reqID = genSessionID('rsq_stop'); this.#client.send('replay_stop', [this.#replaySessionID, reqID]); this.#replayOKCB[reqID] = () => { cb(); }; }); } /** * When a symbol is loaded * @param {() => void} cb * @event */ onSymbolLoaded(cb) { this.#callbacks.symbolLoaded.push(cb); } /** * When a chart update happens * @param {(changes: ('$prices' | string)[]) => void} cb * @event */ onUpdate(cb) { this.#callbacks.update.push(cb); } /** * When the replay session is ready * @param {() => void} cb * @event */ onReplayLoaded(cb) { this.#callbacks.replayLoaded.push(cb); } /** * When the replay session has new resolution * @param {( * timeframe: import('../types').TimeFrame, * index: number, * ) => void} cb * @event */ onReplayResolution(cb) { this.#callbacks.replayResolution.push(cb); } /** * When the replay session ends * @param {() => void} cb * @event */ onReplayEnd(cb) { this.#callbacks.replayEnd.push(cb); } /** * When the replay session cursor has moved * @param {(index: number) => void} cb * @event */ onReplayPoint(cb) { this.#callbacks.replayPoint.push(cb); } /** * When chart error happens * @param {(...any) => void} cb Callback * @event */ onError(cb) { this.#callbacks.error.push(cb); } /** @type {ChartSessionBridge} */ #chartSession = { sessionID: this.#chartSessionID, studyListeners: this.#studyListeners, indexes: {}, send: (t, p) => this.#client.send(t, p), }; Study = studyConstructor(this.#chartSession); /** Delete the chart session */ delete() { if (this.#replayMode) this.#client.send('replay_delete_session', [this.#replaySessionID]); this.#client.send('chart_delete_session', [this.#chartSessionID]); delete this.#client.sessions[this.#chartSessionID]; delete this.#client.sessions[this.#replaySessionID]; this.#replayMode = false; } };