@mathieuc/tradingview
Version:
Tradingview instant stocks API, indicator alerts, trading bot, and more !
436 lines (370 loc) • 13.8 kB
JavaScript
const { genSessionID } = require('../utils');
const { parseCompressed } = require('../protocol');
const graphicParser = require('./graphicParser');
const PineIndicator = require('../classes/PineIndicator');
const BuiltInIndicator = require('../classes/BuiltInIndicator');
/**
* Get pine inputs
* @param {PineIndicator | BuiltInIndicator} options
*/
function getInputs(options) {
if (options instanceof PineIndicator) {
const pineInputs = { text: options.script };
if (options.pineId) pineInputs.pineId = options.pineId;
if (options.pineVersion) pineInputs.pineVersion = options.pineVersion;
Object.keys(options.inputs).forEach((inputID, n) => {
const input = options.inputs[inputID];
pineInputs[inputID] = {
v: (input.type !== 'color') ? input.value : n,
f: input.isFake,
t: input.type,
};
});
return pineInputs;
}
return options.options;
}
const parseTrades = (trades) => trades.reverse().map((t) => ({
entry: {
name: t.e.c,
type: (t.e.tp[0] === 's' ? 'short' : 'long'),
value: t.e.p,
time: t.e.tm,
},
exit: {
name: t.x.c,
value: t.x.p,
time: t.x.tm,
},
quantity: t.q,
profit: t.tp,
cumulative: t.cp,
runup: t.rn,
drawdown: t.dd,
}));
// const historyParser = (history) => history.reverse().map((h) => ({
/**
* @typedef {Object} TradeReport Trade report
* @prop {Object} entry Trade entry
* @prop {string} entry.name Trade name
* @prop {'long' | 'short'} entry.type Entry type (long/short)
* @prop {number} entry.value Entry price value
* @prop {number} entry.time Entry timestamp
* @prop {Object} exit Trade exit
* @prop {'' | string} exit.name Trade name ('' if false exit)
* @prop {number} exit.value Exit price value
* @prop {number} exit.time Exit timestamp
* @prop {number} quantity Trade quantity
* @prop {RelAbsValue} profit Trade profit
* @prop {RelAbsValue} cumulative Trade cummulative profit
* @prop {RelAbsValue} runup Trade run-up
* @prop {RelAbsValue} drawdown Trade drawdown
*/
/**
* @typedef {Object} PerfReport
* @prop {number} avgBarsInTrade Average bars in trade
* @prop {number} avgBarsInWinTrade Average bars in winning trade
* @prop {number} avgBarsInLossTrade Average bars in losing trade
* @prop {number} avgTrade Average trade gain
* @prop {number} avgTradePercent Average trade performace
* @prop {number} avgLosTrade Average losing trade gain
* @prop {number} avgLosTradePercent Average losing trade performace
* @prop {number} avgWinTrade Average winning trade gain
* @prop {number} avgWinTradePercent Average winning trade performace
* @prop {number} commissionPaid Commission paid
* @prop {number} grossLoss Gross loss value
* @prop {number} grossLossPercent Gross loss percent
* @prop {number} grossProfit Gross profit
* @prop {number} grossProfitPercent Gross profit percent
* @prop {number} largestLosTrade Largest losing trade gain
* @prop {number} largestLosTradePercent Largent losing trade performance (percentage)
* @prop {number} largestWinTrade Largest winning trade gain
* @prop {number} largestWinTradePercent Largest winning trade performance (percentage)
* @prop {number} marginCalls Margin calls
* @prop {number} maxContractsHeld Max Contracts Held
* @prop {number} netProfit Net profit
* @prop {number} netProfitPercent Net performance (percentage)
* @prop {number} numberOfLosingTrades Number of losing trades
* @prop {number} numberOfWiningTrades Number of winning trades
* @prop {number} percentProfitable Strategy winrate
* @prop {number} profitFactor Profit factor
* @prop {number} ratioAvgWinAvgLoss Ratio Average Win / Average Loss
* @prop {number} totalOpenTrades Total open trades
* @prop {number} totalTrades Total trades
*/
/**
* @typedef {Object} FromTo
* @prop {number} from From timestamp
* @prop {number} to To timestamp
*/
/**
* @typedef {Object} StrategyReport
* @prop {'EUR' | 'USD' | 'JPY' | '' | 'CHF'} [currency] Selected currency
* @prop {Object} [settings] Backtester settings
* @prop {Object} [settings.dateRange] Backtester date range
* @prop {FromTo} [settings.dateRange.backtest] Date range for backtest
* @prop {FromTo} [settings.dateRange.trade] Date range for trade
* @prop {TradeReport[]} trades Trade list starting by the last
* @prop {Object} history History Chart value
* @prop {number[]} [history.buyHold] Buy hold values
* @prop {number[]} [history.buyHoldPercent] Buy hold percent values
* @prop {number[]} [history.drawDown] Drawdown values
* @prop {number[]} [history.drawDownPercent] Drawdown percent values
* @prop {number[]} [history.equity] Equity values
* @prop {number[]} [history.equityPercent] Equity percent values
* @prop {Object} performance Strategy performance
* @prop {PerfReport} [performance.all] Strategy long/short performances
* @prop {PerfReport} [performance.long] Strategy long performances
* @prop {PerfReport} [performance.short] Strategy short performances
* @prop {number} [performance.buyHoldReturn] Strategy Buy & Hold Return
* @prop {number} [performance.buyHoldReturnPercent] Strategy Buy & Hold Return percent
* @prop {number} [performance.maxDrawDown] Strategy max drawdown
* @prop {number} [performance.maxDrawDownPercent] Strategy max drawdown percent
* @prop {number} [performance.openPL] Strategy Open P&L (Profit And Loss)
* @prop {number} [performance.openPLPercent] Strategy Open P&L (Profit And Loss) percent
* @prop {number} [performance.sharpeRatio] Strategy Sharpe Ratio
* @prop {number} [performance.sortinoRatio] Strategy Sortino Ratio
*/
/**
* @param {import('./session').ChartSessionBridge} chartSession
*/
module.exports = (chartSession) => class ChartStudy {
#studID = genSessionID('st');
#studyListeners = chartSession.studyListeners;
/**
* Table of periods values indexed by timestamp
* @type {Object<number, {}[]>}
*/
#periods = {};
/** @return {{}[]} List of periods values */
get periods() {
return Object.values(this.#periods).sort((a, b) => b.$time - a.$time);
}
/**
* List of graphic xPos indexes
* @type {number[]}
*/
#indexes = [];
/**
* Table of graphic drawings indexed by type and ID
* @type {Object<string, Object<number, {}>>}
*/
#graphic = {};
/**
* Table of graphic drawings indexed by type
* @return {import('./graphicParser').GraphicData}
*/
get graphic() {
const translator = {};
Object.keys(chartSession.indexes)
.sort((a, b) => chartSession.indexes[b] - chartSession.indexes[a])
.forEach((r, n) => { translator[r] = n; });
const indexes = this.#indexes.map((i) => translator[i]);
return graphicParser(this.#graphic, indexes);
}
/** @type {StrategyReport} */
#strategyReport = {
trades: [],
history: {},
performance: {},
};
/** @return {StrategyReport} Get the strategy report if available */
get strategyReport() {
return this.#strategyReport;
}
#callbacks = {
studyCompleted: [],
update: [],
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);
}
/**
* @param {PineIndicator | BuiltInIndicator} indicator Indicator object instance
*/
constructor(indicator) {
if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) {
throw new Error(`Indicator argument must be an instance of PineIndicator or BuiltInIndicator.
Please use 'TradingView.getIndicator(...)' function.`);
}
/** @type {PineIndicator | BuiltInIndicator} Indicator instance */
this.instance = indicator;
this.#studyListeners[this.#studID] = async (packet) => {
if (global.TW_DEBUG) console.log('§90§30§105 STUDY §0 DATA', packet);
if (packet.type === 'study_completed') {
this.#handleEvent('studyCompleted');
return;
}
if (['timescale_update', 'du'].includes(packet.type)) {
const changes = [];
const data = packet.data[1][this.#studID];
if (data && data.st && data.st[0]) {
data.st.forEach((p) => {
const period = {};
p.v.forEach((plot, i) => {
if (!this.instance.plots) {
period[i === 0 ? '$time' : `plot_${i - 1}`] = plot;
return;
}
const plotName = (i === 0 ? '$time' : this.instance.plots[`plot_${i - 1}`]);
if (plotName && !period[plotName]) period[plotName] = plot;
else period[`plot_${i - 1}`] = plot;
});
this.#periods[p.v[0]] = period;
});
changes.push('plots');
}
if (data.ns && data.ns.d) {
const parsed = JSON.parse(data.ns.d);
if (parsed.graphicsCmds) {
if (parsed.graphicsCmds.erase) {
parsed.graphicsCmds.erase.forEach((instruction) => {
// console.log('Erase', instruction);
if (instruction.action === 'all') {
if (!instruction.type) {
Object.keys(this.#graphic).forEach((drawType) => {
this.#graphic[drawType] = {};
});
} else delete this.#graphic[instruction.type];
return;
}
if (instruction.action === 'one') {
delete this.#graphic[instruction.type][instruction.id];
}
// Can an 'instruction' contains other things ?
});
}
if (parsed.graphicsCmds.create) {
Object.keys(parsed.graphicsCmds.create).forEach((drawType) => {
if (!this.#graphic[drawType]) this.#graphic[drawType] = {};
parsed.graphicsCmds.create[drawType].forEach((group) => {
group.data.forEach((item) => {
this.#graphic[drawType][item.id] = item;
});
});
});
}
// console.log('graphicsCmds', Object.keys(parsed.graphicsCmds));
// Can 'graphicsCmds' contains other things ?
changes.push('graphic');
}
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);
}
if (data.ns.indexes && typeof data.ns.indexes === 'object') {
this.#indexes = data.ns.indexes;
}
this.#handleEvent('update', changes);
return;
}
if (packet.type === 'study_error') {
this.#handleError(packet.data[3], packet.data[4]);
}
};
chartSession.send('create_study', [
chartSession.sessionID,
`${this.#studID}`,
'st1',
'$prices',
this.instance.type,
getInputs(this.instance),
]);
}
/**
* @param {PineIndicator | BuiltInIndicator} indicator Indicator instance
*/
setIndicator(indicator) {
if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) {
throw new Error(`Indicator argument must be an instance of PineIndicator or BuiltInIndicator.
Please use 'TradingView.getIndicator(...)' function.`);
}
this.instance = indicator;
chartSession.send('modify_study', [
chartSession.sessionID,
`${this.#studID}`,
'st1',
getInputs(this.instance),
]);
}
/**
* When the indicator is ready
* @param {() => void} cb
* @event
*/
onReady(cb) {
this.#callbacks.studyCompleted.push(cb);
}
/**
* @typedef {'plots' | 'report.currency'
* | 'report.settings' | 'report.perf'
* | 'report.trades' | 'report.history'
* | 'graphic'
* } UpdateChangeType
*/
/**
* When an indicator update happens
* @param {(changes: UpdateChangeType[]) => void} cb
* @event
*/
onUpdate(cb) {
this.#callbacks.update.push(cb);
}
/**
* When indicator error happens
* @param {(...any) => void} cb Callback
* @event
*/
onError(cb) {
this.#callbacks.error.push(cb);
}
/** Remove the study */
remove() {
chartSession.send('remove_study', [
chartSession.sessionID,
this.#studID,
]);
delete this.#studyListeners[this.#studID];
}
};