diff --git a/README.md b/README.md index 604bad3..76a7611 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Here is the Telegram group of the library: https://t.me/tradingview_api - [x] Get TradingView's technical analysis - [x] Replay mode + Fake Replay mode (for free plan) - [x] Get values from a specific date range +- [x] Deep Backtesting (Beta) (Premium only) - [ ] TradingView socket server emulation - [ ] Interract with public chats - [ ] Get Screener top values diff --git a/package.json b/package.json index 8abd01f..8a2854e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mathieuc/tradingview", - "version": "3.4.1", + "version": "3.5.0", "description": "Tradingview instant stocks API, indicator alerts, trading bot, and more !", "main": "main.js", "scripts": { diff --git a/src/chart/history.js b/src/chart/history.js new file mode 100644 index 0000000..9f3f027 --- /dev/null +++ b/src/chart/history.js @@ -0,0 +1,184 @@ +const { genSessionID } = require('../utils'); +const { parseCompressed } = require('../protocol'); +const { getInputs, parseTrades } = require('./study'); + +/** + * @param {import('../client').ClientBridge} client + */ +module.exports = (client) => class HistorySession { + #historySessionID = genSessionID('hs'); + + /** Parent client */ + #client = client; + + #callbacks = { + historyLoaded: [], + + event: [], + error: [], + }; + + /** @type {StrategyReport} */ + #strategyReport = { + trades: [], + history: {}, + performance: {}, + }; + + /** @return {StrategyReport} Get the strategy report if available */ + get strategyReport() { + return this.#strategyReport; + } + + /** + * @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.#historySessionID] = { + type: 'history', + onData: async (packet) => { + if (global.TW_DEBUG) console.log('§90§30§106 HISTORY SESSION §0 DATA', packet); + + if (packet.type === 'request_data') { + const data = packet.data[2]; + if (data.ns && data.ns.d) { + const parsed = JSON.parse(data.ns.d); + const changes = await this.updateReport(parsed); + this.#handleEvent('historyLoaded', changes); + } + } + + if (['request_error', 'critical_error'].includes(packet.type)) { + const [, name, description] = packet.data; + this.#handleError('Critical error:', name, description); + } + }, + }; + + this.#client.send('history_create_session', [this.#historySessionID]); + } + + async updateReport(parsed) { + const changes = []; + const updateStrategyReport = (report) => { + if (report.currency) { + this.#strategyReport.currency = report.currency; + changes.push('report.currency'); + } + + if (report.settings) { + this.#strategyReport.settings = report.settings; + changes.push('report.settings'); + } + + if (report.performance) { + this.#strategyReport.performance = report.performance; + changes.push('report.perf'); + } + + if (report.trades) { + this.#strategyReport.trades = parseTrades(report.trades); + changes.push('report.trades'); + } + + if (report.equity) { + this.#strategyReport.history = { + buyHold: report.buyHold, + buyHoldPercent: report.buyHoldPercent, + drawDown: report.drawDown, + drawDownPercent: report.drawDownPercent, + equity: report.equity, + equityPercent: report.equityPercent, + }; + changes.push('report.history'); + } + }; + + if (parsed.dataCompressed) { + updateStrategyReport((await parseCompressed(parsed.dataCompressed)).report); + } + + if (parsed.data && parsed.data.report) updateStrategyReport(parsed.data.report); + + return changes; + } + + /** + * Sets the history market + * @param {string} symbol Market symbol + * @param {number} from Deep backtest starting point (Timestamp) + * @param {number} to Deep backtest ending point (Timestamp) + * @param {PineIndicator} indicator PineIndicator with options set + * @param {Object} options Chart options + * @param {import('../types').TimeFrame} [options.timeframe] Chart period timeframe (Default is 5) + * @param {number} [options.from] First available timestamp (Default is 2010-01-01) + * @param {number} [options.to] Last candle timestamp (Default is now) + * @param {'splits' | 'dividends'} [options.adjustment] Market adjustment + * @param {'regular' | 'extended'} [options.session] Chart session + * @param {'EUR' | 'USD' | string} [options.currency] Chart currency + */ + requestHistoryData(symbol, indicator, options) { + const symbolInit = { + symbol: symbol || 'BTCEUR', + adjustment: options.adjustment || 'splits', + }; + + if (options.session) symbolInit.session = options.session; + if (options.currency) symbolInit['currency-id'] = options.currency; + const from = options.from || Math.floor(new Date(2010, 1, 1) / 1000); + const to = options.to || Math.floor(Date.now() / 1000); + + this.#client.send('request_history_data', [ + this.#historySessionID, + 0, // what is this? + `=${JSON.stringify(symbolInit)}`, + options.timeframe || '5', + 0, // what is this? + { from_to: { from, to } }, + indicator.type, + getInputs(indicator), + [], // what is this? + ]); + } + + /** + * When a deep backtest history is loaded + * @param {() => void} cb + * @event + */ + onHistoryLoaded(cb) { + this.#callbacks.historyLoaded.push(cb); + } + + /** + * When deep backtest history error happens + * @param {(...any) => void} cb Callback + * @event + */ + onError(cb) { + this.#callbacks.error.push(cb); + } + + /** @type {HistorySessionBridge} */ + #historySession = { + sessionID: this.#historySessionID, + send: (t, p) => this.#client.send(t, p), + }; + + /** Delete the chart session */ + delete() { + this.#client.send('history_delete_session', [this.#historySessionID]); + delete this.#client.sessions[this.#historySessionID]; + } +}; diff --git a/src/chart/session.js b/src/chart/session.js index 619f149..3be5c72 100644 --- a/src/chart/session.js +++ b/src/chart/session.js @@ -1,6 +1,6 @@ const { genSessionID } = require('../utils'); -const studyConstructor = require('./study'); +const { studyConstructor } = require('./study'); /** * @typedef {'HeikinAshi' | 'Renko' | 'LineBreak' | 'Kagi' | 'PointAndFigure' diff --git a/src/chart/study.js b/src/chart/study.js index 4735d81..a292817 100644 --- a/src/chart/study.js +++ b/src/chart/study.js @@ -145,7 +145,7 @@ const parseTrades = (trades) => trades.reverse().map((t) => ({ /** * @param {import('./session').ChartSessionBridge} chartSession */ -module.exports = (chartSession) => class ChartStudy { +const studyConstructor = (chartSession) => class ChartStudy { #studID = genSessionID('st'); #studyListeners = chartSession.studyListeners; @@ -433,3 +433,9 @@ module.exports = (chartSession) => class ChartStudy { delete this.#studyListeners[this.#studID]; } }; + +module.exports = { + getInputs, + parseTrades, + studyConstructor, +}; diff --git a/src/client.js b/src/client.js index 8a07c4d..48154f8 100644 --- a/src/client.js +++ b/src/client.js @@ -5,6 +5,7 @@ const protocol = require('./protocol'); const quoteSessionGenerator = require('./quote/session'); const chartSessionGenerator = require('./chart/session'); +const historySessionGenerator = require('./chart/history'); /** * @typedef {Object} Session @@ -216,7 +217,7 @@ module.exports = class Client { * @prop {string} [token] User auth token (in 'sessionid' cookie) * @prop {string} [signature] User auth token signature (in 'sessionid_sign' cookie) * @prop {boolean} [DEBUG] Enable debug mode - * @prop {'data' | 'prodata' | 'widgetdata'} [server] Server type + * @prop {'data' | 'prodata' | 'widgetdata' | 'history-data'} [server] Server type */ /** Client object @@ -276,6 +277,7 @@ module.exports = class Client { Session = { Quote: quoteSessionGenerator(this.#clientBridge), Chart: chartSessionGenerator(this.#clientBridge), + History: historySessionGenerator(this.#clientBridge), }; /** diff --git a/tests/11. DeepBacktest.js b/tests/11. DeepBacktest.js new file mode 100644 index 0000000..820eaf3 --- /dev/null +++ b/tests/11. DeepBacktest.js @@ -0,0 +1,53 @@ +const TradingView = require('../main'); + +const wait = (ms) => new Promise((cb) => { setTimeout(cb, ms); }); + +module.exports = async (log, success, warn, err, cb) => { + if (!process.env.SESSION || !process.env.SIGNATURE) { + warn('No sessionid/signature was provided'); + cb(); + return; + } + + log('Creating logged history client'); + const client = new TradingView.Client({ + // requires a Premium plan + token: process.env.SESSION, + signature: process.env.SIGNATURE, + // needs a new server type + server: 'history-data', + }); + + client.onError((...error) => { + err('Client error', error); + }); + + const history = new client.Session.History(); + + history.onError((...error) => { + err('History error', error); + history.delete(); + client.end(); + cb(); + }); + + await wait(1000); + log('Loading built-in strategy....'); + const indicator = await TradingView.getIndicator('STD;MACD%1Strategy'); + + const from = Math.floor(new Date(2010, 1, 1) / 1000); + const to = Math.floor(Date.now() / 1000); + + log('Running deep backtest....'); + history.requestHistoryData('AMEX:SPY', indicator, { timeframe: '5', from, to }); + history.onHistoryLoaded(async () => { + success('Deep backtest traded from', new Date(history.strategyReport.settings.dateRange.trade.from)); + log('Closing client...'); + history.delete(); + await client.end(); + success('Client closed'); + cb(); + }); + + log('Strategy loaded !'); +};