diff --git a/README.md b/README.md index 66610001..8846ee44 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Join the Telegram group of the TradingView-API Community: [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/examples/DeepBacktest.js b/examples/DeepBacktest.js new file mode 100644 index 00000000..d576a1f1 --- /dev/null +++ b/examples/DeepBacktest.js @@ -0,0 +1,52 @@ +const TradingView = require('../main'); + +/** + * This example runs a deep backtest using TradingView's + * history-data server (requires a Premium plan). + */ + +if (!process.env.SESSION || !process.env.SIGNATURE) { + throw Error('Please set your sessionid and signature cookies'); +} + +console.log('----- Testing DeepBacktest: -----'); + +const client = new TradingView.Client({ + token: process.env.SESSION, + signature: process.env.SIGNATURE, + server: 'history-data', +}); + +client.onError((...error) => { + console.error('Client error:', ...error); +}); + +const history = new client.Session.History(); + +history.onError(async (...error) => { + console.error('History error:', ...error); + history.delete(); + await client.end(); +}); + +(async () => { + console.log('Loading built-in strategy...'); + const indicator = await TradingView.getIndicator('STD;MACD%1Strategy'); + + const from = Math.floor(new Date(2010, 1, 1).getTime() / 1000); + const to = Math.floor(Date.now() / 1000); + + console.log('Running deep backtest...'); + history.requestHistoryData('AMEX:SPY', indicator, { timeframe: '5', from, to }); + + history.onHistoryLoaded(async () => { + console.log( + 'Deep backtest traded from', + new Date(history.strategyReport.settings.dateRange.trade.from), + ); + console.log('Closing client...'); + history.delete(); + await client.end(); + console.log('Done !'); + }); +})(); diff --git a/src/chart/history.js b/src/chart/history.js new file mode 100644 index 00000000..9f3f0271 --- /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 1d2ee209..b1f44bed 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 4735d818..a292817f 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 718d15c7..7542337f 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 * @prop {string} [location] Auth page location (For france: https://fr.tradingview.com/) * @prop {Object} [headers] Custom WebSocket headers */ @@ -293,6 +294,7 @@ module.exports = class Client { Session = { Quote: quoteSessionGenerator(this.#clientBridge), Chart: chartSessionGenerator(this.#clientBridge), + History: historySessionGenerator(this.#clientBridge), }; /**