UNPKG

trading-vue3-js

Version:

Customizable charting lib for traders. Based on https://github.com/C451/trading-vue-js by C451.

1,614 lines (1,449 loc) 46.6 kB
// Script std-lib (built-in functions) import se from './script_engine.js' import linreg from '../stuff/linreg.js' import * as u from './script_utils.js' import Sampler from './sampler.js' import { Sym, ARR, TSS, NUM } from './symbol.js' const BUF_INC = 5 export default class ScriptStd { constructor(env) { this.env = env this.se = se this.SWMA = [1/6, 2/6, 2/6, 1/6] this.STDEV_EPS = 1e-10 this.STDEV_Z = 1e-4 this._index_tracking() } // Wrap every index with index-tracking function // That way we will know exact index ranges _index_tracking() { let proto = Object.getPrototypeOf(this) let std = `` for (var k of Object.getOwnPropertyNames(proto)) { switch(k) { case 'constructor': case 'ts': case 'tstf': case 'sample': case '_index_tracking': case '_tsid': case '_i': case '_v': case '_add_i': case 'chart': case 'onchart': case 'offchart': case 'sym': continue } let f = this._add_i(k, this[k].toString()) if (f) this[k] = f } } // Add index tracking to the function _add_i(name, src) { let args = u.f_args(src) src = u.f_body(src) let src2 = u.wrap_idxs(src, 'this.') if (src2 !== src) { return new Function (...args, src2) } return null } // Generate the next timeseries id _tsid(prev, next) { // TODO: prev presence check return `${prev}<-${next}` } // Index-tracker _i(i, x) { // If an object is actually a timeseries if (x != undefined && x === x && x.__id__) { // Increase TS buff length if (!x.__len__ || i >= x.__len__) { x.__len__ = i + BUF_INC } } return i } // Index-tracker (object-based) _v(x, i) { // If an object is actually a timeseries if (x != undefined && x === x && x.__id__) { // Increase TS buff length if (!x.__len__ || i >= x.__len__) { x.__len__ = i + BUF_INC } } return x } /** * Creates a new time-series & records each x. * Returns an array. Id is auto-genrated * @param {*} x - A variable to sample from * @return {TS} - New time-series */ ts(x, _id, _tf) { if (_tf) return this.tstf(x, _tf, _id) let ts = this.env.tss[_id] if (!ts) { ts = this.env.tss[_id] = [x] ts.__id__ = _id } else { ts[0] = x } return ts } /** * Creates a new time-series & records each x. * Uses Sampler to aggregate the values * Return the an array. Id is auto-genrated * @param {*} x - A variable to sample from * @param {(number|string)} tf - Timeframe in ms or as a string * @return {TS} - New time-series */ tstf(x, tf, _id) { let ts = this.env.tss[_id] if (!ts) { ts = this.env.tss[_id] = [x] ts.__id__ = _id ts.__tf__ = u.tf_from_str(tf) ts.__fn__ = Sampler('close').bind(ts) } else { ts.__fn__(x) } return ts } /** * Creates a new custom sampler. * Return the an array. Id is auto-genrated * @param {*} x - A variable to sample from * @param {string} type - Sampler type * @param {(number|string)} tf - Timeframe in ms or as a string * @return {TS} - New time-series */ sample(x, type, tf, _id) { let ts = this.env.tss[_id] if (!ts) { ts = this.env.tss[_id] = [x] ts.__id__ = _id ts.__tf__ = u.tf_from_str(tf) ts.__fn__ = Sampler(type).bind(ts) } else { ts.__fn__(x) } return ts } /** * Replaces the variable if it's NaN * @param {*} x - The variable * @param {*} [v] - A value to replace with * @return {*} - New value */ nz(x, v) { if (x == undefined || x !== x) { return v || 0 } return x } /** * Is the variable NaN ? * @param {*} x - The variable * @return {boolean} - New value */ na(x) { return x == undefined || x !== x } /** Replaces the var with NaN if Infinite * @param {*} x - The variable * @param {*} [v] - A value to replace with * @return {*} - New value */ nf(x, v) { if (!isFinite(x)) { return v !== undefined ? v : NaN } return x } // Math operators on t-series and numbers /** Adds values / time-series * @param {(TS|*)} x - First input * @param {(TS|*)} y - Second input * @return {TS} - New time-series */ add(x, y, _id) { // __id__ means this is a time-series let id = this._tsid(_id, `add`) let x0 = this.na(x) ? NaN : (x.__id__ ? x[0] : x) let y0 = this.na(y) ? NaN : (y.__id__ ? y[0] : y) return this.ts(x0 + y0, id, x.__tf__) } /** Subtracts values / time-series * @param {(TS|*)} x - First input * @param {(TS|*)} y - Second input * @return {TS} - New time-series */ sub(x, y, _id) { let id = this._tsid(_id, `sub`) let x0 = this.na(x) ? NaN : (x.__id__ ? x[0] : x) let y0 = this.na(y)? NaN : (y.__id__ ? y[0] : y) return this.ts(x0 - y0, id, x.__tf__) } /** Multiplies values / time-series * @param {(TS|*)} x - First input * @param {(TS|*)} y - Second input * @return {TS} - New time-series */ mult(x, y, _id) { let id = this._tsid(_id, `mult`) let x0 = this.na(x) ? NaN : (x.__id__ ? x[0] : x) let y0 = this.na(y)? NaN : (y.__id__ ? y[0] : y) return this.ts(x0 * y0, id, x.__tf__) } /** Divides values / time-series * @param {(TS|*)} x - First input * @param {(TS|*)} y - Second input * @return {TS} - New time-series */ div(x, y, _id) { let id = this._tsid(_id, `div`) let x0 = this.na(x) ? NaN : (x.__id__ ? x[0] : x) let y0 = this.na(y)? NaN : (y.__id__ ? y[0] : y) return this.ts(x0 / y0, id, x.__tf__) } /** Returns a negative value / time-series * @param {(TS|*)} x - Input * @return {TS} - New time-series */ neg(x, _id) { let id = this._tsid(_id, `neg`) let x0 = this.na(x) ? NaN : (x.__id__ ? x[0] : x) return this.ts(-x0, id, x.__tf__) } /** Absolute value * @param {number} x - Input * @return {number} - Absolute value */ abs(x) { return Math.abs(x) } /** Arccosine function * @param {number} x - Input * @return {number} - Arccosine of x */ acos(x) { return Math.acos(x) } /** Emits an event to DataCube * @param {string} type - Signal type * @param {*} data - Signal data */ signal(type, data = {}) { if (this.se.shared.event !== 'update') return this.se.send('script-signal', { type, data }) } /** Emits an event if cond === true * @param {(boolean|TS)} cond - The condition * @param {string} type - Signal type * @param {*} data - Signal data */ signalif(cond, type, data = {}) { if (this.se.shared.event !== 'update') return if (cond && cond.__id__) cond = cond[0] if (cond) { this.se.send('script-signal', { type, data }) } } /** Arnaud Legoux Moving Average * @param {TS} src - Input * @param {number} len - Length * @param {number} offset - Offset * @param {number} sigma - Sigma * @return {TS} - New time-series */ alma(src, len, offset, sigma, _id) { let id = this._tsid(_id, `alma(${len},${offset},${sigma})`) let m = Math.floor(offset * (len - 1)) let s = len / sigma let norm = 0 let sum = 0 for (var i = 0; i < len; i++) { let w = Math.exp(-1 * Math.pow(i - m, 2) / (2 * Math.pow(s, 2))) norm = norm + w sum = sum + src[len - i - 1] * w } return this.ts(sum / norm, id, src.__tf__) } /** Arcsine function * @param {number} x - Input * @return {number} - Arcsine of x */ asin(x) { return Math.asin(x) } /** Arctangent function * @param {number} x - Input * @return {number} - Arctangent of x */ atan(x) { return Math.atan(x) } /** Average True Range * @param {number} len - Length * @return {TS} - New time-series */ atr(len, _id, _tf) { let tfs = _tf || '' let id = this._tsid(_id, `atr(${len})`) let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let close = this.env.shared[`close${tfs}`] let tr = this.ts(0, id, _tf) tr[0] = this.na(high[1]) ? high[0] - low[0] : Math.max( Math.max( high[0] - low[0], Math.abs(high[0] - close[1]) ), Math.abs(low[0] - close[1]) ) return this.rma(tr, len, id) } /** Average of arguments * @param {...number} args - Numeric values * @return {number} */ avg(...args) { args.pop() // Remove _id let sum = 0 for (var i = 0; i < args.length; i++) { sum += args[i] } return sum / args.length } /** Candles since the event occured (cond === true) * @param {(boolean|TS)} cond - the condition */ since(cond, _id) { let id = this._tsid(_id, `since()`) if (cond && cond.__id__) cond = cond[0] let s = this.ts(undefined, id) s[0] = cond ? 0 : s[1] + 1 return s } /** Bollinger Bands * @param {TS} src - Input * @param {number} len - Length * @param {number} mult - Multiplier * @return {TS[]} - Array of new time-series (3 bands) */ bb(src, len, mult, _id) { let id = this._tsid(_id, `bb(${len},${mult})`) let basis = this.sma(src, len, id) let dev = this.stdev(src, len, id)[0] * mult return [ basis, this.ts(basis[0] + dev, id + '1', src.__tf__), this.ts(basis[0] - dev, id + '2', src.__tf__) ] } /** Bollinger Bands Width * @param {TS} src - Input * @param {number} len - Length * @param {number} mult - Multiplier * @return {TS} - New time-series */ bbw(src, len, mult, _id) { let id = this._tsid(_id, `bbw(${len},${mult})`) let basis = this.sma(src, len, id)[0] let dev = this.stdev(src, len, id)[0] * mult return this.ts(2 * dev / basis, id, src.__tf__) } /** Converts the variable to Boolean * @param {number} x The variable * @return {number} */ bool(x) { return !!x } /** Commodity Channel Index * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ cci(src, len, _id) { // TODO: Not exactly precise, but pretty damn close let id = this._tsid(_id, `cci(${len})`) let ma = this.sma(src, len, id) let dev = this.dev(src, len, id) let cci = (src[0] - ma[0]) / (0.015 * dev[0]) return this.ts(cci, id, src.__tf__) } /** Shortcut for Math.ceil() * @param {number} x The variable * @return {number} */ ceil(x) { return Math.ceil(x) } /** Change: x[0] - x[len] * @param {TS} src - Input * @param {number} [len] - Length * @return {TS} - New time-series */ change(src, len = 1, _id) { let id = this._tsid(_id, `change(${len})`) return this.ts(src[0] - src[len], id, src.__tf__) } /** Chande Momentum Oscillator * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ cmo(src, len, _id) { let id = this._tsid(_id, `cmo(${len})`) let mom = this.change(src, 1, id) let g = this.ts(mom[0] >= 0 ? mom[0] : 0.0, id+"g", src.__tf__) let l = this.ts(mom[0] >= 0 ? 0.0 : -mom[0], id+"l", src.__tf__) let sm1 = this.sum(g, len, id+'1')[0] let sm2 = this.sum(l, len, id+'2')[0] return this.ts(100 * (sm1 - sm2) / (sm1 + sm2), id, src.__tf__) } /** Center of Gravity * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ cog(src, len, _id) { let id = this._tsid(_id, `cmo(${len})`) let sum = this.sum(src, len, id)[0] let num = 0 for (var i = 0; i < len; i++) { num += src[i] * (i + 1) } return this.ts(-num / sum, id, src.__tf__) } // Correlation corr() { // TODO: this } /** Cosine function * @param {number} x - Input * @return {number} - Cosine of x */ cos(x) { return Math.cos(x) } /** When one time-series crosses another * @param {TS} src1 - TS1 * @param {TS} src2 - TS2 * @return {TS} - New time-series */ cross(src1, src2, _id) { let id = this._tsid(_id, `cross`) let x = (src1[0] > src2[0]) !== (src1[1] > src2[1]) return this.ts(x, id, src1.__tf__) } /** When one time-series goes over another one * @param {TS} src1 - TS1 * @param {TS} src2 - TS2 * @return {TS} - New time-series */ crossover(src1, src2, _id) { let id = this._tsid(_id, `crossover`) let x = (src1[0] > src2[0]) && (src1[1] <= src2[1]) return this.ts(x, id, src1.__tf__) } /** When one time-series goes under another one * @param {TS} src1 - TS1 * @param {TS} src2 - TS2 * @return {TS} - New time-series */ crossunder(src1, src2, _id) { let id = this._tsid(_id, `crossunder`) let x = (src1[0] < src2[0]) && (src1[1] >= src2[1]) return this.ts(x, id, src1.__tf__) } /** Sum of all elements of src * @param {TS} src1 - Input * @return {TS} - New time-series */ cum(src, _id) { let id = this._tsid(_id, `cum`) let res = this.ts(0, id, src.__tf__) res[0] = this.nz(src[0]) + this.nz(res[1]) return res } /** Day of month, literally * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Day */ dayofmonth(time) { return new Date(time || se.t).getUTCDate() } /** Day of week, literally * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Day */ dayofweek(time) { return new Date(time || se.t).getUTCDay() + 1 } /** Deviation from SMA * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ dev(src, len, _id) { let id = this._tsid(_id, `dev(${len})`) let mean = this.sma(src, len, id)[0] let sum = 0 for (var i = 0; i < len; i++) { sum += Math.abs(src[i] - mean) } return this.ts(sum / len, id, src.__tf__) } /** Directional Movement Index ADX, +DI, -DI * @param {number} len - Length * @param {number} smooth - Smoothness * @return {TS} - New time-series */ dmi(len, smooth, _id, _tf) { let id = this._tsid(_id, `dmi(${len},${smooth})`) let tfs = _tf || '' let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let up = this.change(high, 1, id+'1')[0] let down = this.neg(this.change(low, 1, id+'2'), id)[0] let plusDM = this.ts(100 * ( this.na(up) ? NaN : (up > down && up > 0 ? up : 0)), id+'3', _tf ) let minusDM = this.ts(100 * ( this.na(down) ? NaN : (down > up && down > 0 ? down : 0)), id+'4', _tf ) let trur = this.rma(this.tr(false, id, _tf), len, id+'5') let plus = this.div( this.rma(plusDM, len, id+'6'), trur, id+'8') let minus = this.div( this.rma(minusDM, len, id+'7'), trur, id+'9') let sum = this.add(plus, minus, id+'10')[0] let adx = this.rma( this.ts(100 * Math.abs(plus[0] - minus[0]) / (sum === 0 ? 1 : sum), id+'11', _tf), smooth, id+'12' ) return [adx, plus, minus] } /** Exponential Moving Average with alpha = 2 / (y + 1) * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ ema(src, len, _id) { let id = this._tsid(_id, `ema(${len})`) let a = 2 / (len + 1) let ema = this.ts(0, id, src.__tf__) ema[0] = this.na(ema[1]) ? this.sma(src, len, id)[0] : a * src[0] + (1 - a) * this.nz(ema[1]) return ema } /** Shortcut for Math.exp() * @param {number} x The variable * @return {number} */ exp(x) { return Math.exp(x) } /** Test if "src" TS is falling for "len" candles * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ falling(src, len, _id) { let id = this._tsid(_id, `falling(${len})`) let bot = src[0] for (var i = 1; i < len + 1; i++) { if (bot >= src[i]) { return this.ts(false, id, src.__tf__) } } return this.ts(true, id, src.__tf__) } /** For a given series replaces NaN values with * previous nearest non-NaN value * @param {TS} src - Input time-series * @return {TS} */ fixnan(src) { if (this.na(src[0])) { for (var i = 1; i < src.length; i++) { if (!this.na(src[i])) { src[0] = src[i] break } } } return src } /* TODO: think skipnan(x, _id) { let id = this._tsid(_id, `skipnan()`) return this.ts(true, id, src.__tf__) }*/ /** Shortcut for Math.floor() * @param {number} x The variable * @return {number} */ floor(x) { Math.floor(x) } /** Highest value for a given number of candles back * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ highest(src, len, _id) { let id = this._tsid(_id, `highest(${len})`) let high = -Infinity for (var i = 0; i < len; i++) { if (src[i] > high) high = src[i] } return this.ts(high, id, src.__tf__) } /** Highest value offset for a given number of bars back * @param {TS} src - Input * @param {number} len - Length */ highestbars(src, len, _id) { let id = this._tsid(_id, `highestbars(${len})`) let high = -Infinity let hi = 0 for (var i = 0; i < len; i++) { if (src[i] > high) { high = src[i], hi = i } } return this.ts(-hi, id, src.__tf__) } /** Hull Moving Average * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ hma(src, len, _id) { let id = this._tsid(_id, `hma(${len})`) let len2 = Math.floor(len/2) let len3 = Math.round(Math.sqrt(len)) let a = this.mult(this.wma(src, len2, id+'1'), 2, id) let b = this.wma(src, len, id+'2') let delt = this.sub(a, b, id+'3') return this.wma(delt, len3, id+'4') } /** Returns hours of a given timestamp * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Hour */ hour(time) { return new Date(time || se.t).getUTCHours() } /** Returns x or y depending on the condition * @param {(boolean|TS)} cond - Condition * @param {*} x - Frist value * @param {*} y - Second value * @return {*} */ iff(cond, x, y) { if (cond && cond.__id__) cond = cond[0] return cond ? x : y } /** Keltner Channels * @param {TS} src - Input * @param {number} len - Length * @param {number} mult - Multiplier * @param {boolean} [use_tr] - Use true range * @return {TS[]} - Array of new time-series (3 bands) */ kc(src, len, mult, use_tr = true, _id, _tf) { let id = this._tsid(_id, `kc(${len},${mult},${use_tr})`) let tfs = _tf || '' let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let basis = this.ema(src, len, id+'1') let range = use_tr ? this.tr(false, id+'2', _tf) : this.ts(high[0] - low[0], id+'3', src.__tf__) let ema = this.ema(range, len, id+'4') return [ basis, this.ts(basis[0] + ema[0] * mult, id+'5', src.__tf__), this.ts(basis[0] - ema[0] * mult, id+'6', src.__tf__) ] } /** Keltner Channels Width * @param {TS} src - Input * @param {number} len - Length * @param {number} mult - Multiplier * @param {boolean} [use_tr] - Use true range * @return {TS} - New time-series */ kcw(src, len, mult, use_tr = true, _id, _tf) { let id = this._tsid(_id, `kcw(${len},${mult},${use_tr})`) let kc = this.kc(src, len, mult, use_tr, `kcw`, _tf) return this.ts((kc[1][0] - kc[2][0]) / kc[0][0], id, src.__tf__) } /** Linear Regression * @param {TS} src - Input * @param {number} len - Length * @param {number} offset - Offset * @return {TS} - New time-series */ linreg(src, len, offset = 0, _id) { let id = this._tsid(_id, `linreg(${len})`) src.__len__ = Math.max(src.__len__ || 0, len) let lr = linreg(src, len, offset) return this.ts(lr, id, src.__tf__) } /** Shortcut for Math.log() * @param {number} x The variable * @return {number} */ log(x) { return Math.log(x) } /** Shortcut for Math.log10() * @param {number} x The variable * @return {number} */ log10(x) { return Math.log10(x) } /** Lowest value for a given number of candles back * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ lowest(src, len, _id) { let id = this._tsid(_id, `lowest(${len})`) let low = Infinity for (var i = 0; i < len; i++) { if (src[i] < low) low = src[i] } return this.ts(low, id, src.__tf__) } /** Lowest value offset for a given number of bars back * @param {TS} src - Input * @param {number} len - Length */ lowestbars(src, len, _id) { let id = this._tsid(_id, `lowestbars(${len})`) let low = Infinity let li = 0 for (var i = 0; i < len; i++) { if (src[i] < low) { low = src[i], li = i } } return this.ts(-li, id, src.__tf__) } /** Moving Average Convergence/Divergence * @param {TS} src - Input * @param {number} fast - Fast EMA * @param {number} slow - Slow EMA * @param {number} sig - Signal * @return {TS[]} - [macd, signal, hist] */ macd(src, fast, slow, sig, _id) { let id = this._tsid(_id, `macd(${fast}${slow}${sig})`) let fast_ma = this.ema(src, fast, id+'1') let slow_ma = this.ema(src, slow, id+'2') let macd = this.sub(fast_ma, slow_ma, id+'3') let signal = this.ema(macd, sig, id+'4') let hist = this.sub(macd, signal, id+'5') return [macd, signal, hist] } /** Max of arguments * @param {...number} args - Numeric values * @return {number} */ max(...args) { args.pop() // Remove _id return Math.max(...args) } /** Sends update to some overlay / main chart * @param {string} id - Overlay id * @param {Object} fields - Fields to be overwritten */ modify(id, fields) { se.send('modify-overlay', { uuid:id, fields }) } /** Sets the reverse buffer size for a given * time-series (default = 5, grows on demand) * @param {TS} src - Input * @param {number} len - New length */ buffsize(src, len) { src.__len__ = len } /** Money Flow Index * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ mfi(src, len, _id) { let id = this._tsid(_id, `mfi(${len})`) let vol = this.env.shared.vol let ch = this.change(src, 1, id+'1')[0] let ts1 = this.mult(vol, ch <= 0.0 ? 0.0 : src[0], id+'2') let ts2 = this.mult(vol, ch >= 0.0 ? 0.0 : src[0], id+'3') let upper = this.sum(ts1, len, id+'4') let lower = this.sum(ts2, len, id+'5') let res = undefined if (!this.na(lower)) { res = this.rsi(upper, lower, id+'6')[0] } return this.ts(res, id, src.__tf__) } /** Min of arguments * @param {...number} args - Numeric values * @return {number} */ min(...args) { args.pop() // Remove _id return Math.min(...args) } /** Returns minutes of a given timestamp * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Hour */ minute(time) { return new Date(time || se.t).getUTCMinutes() } /** Momentum * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ mom(src, len, _id) { let id = this._tsid(_id, `mom(${len})`) return this.ts(src[0] - src[len], id, src.__tf__) } /** Month * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Day */ month(time) { return new Date(time || se.t).getUTCMonth() } // Display data point as the main chart chart() { // TODO: this } // TODO: optionally enable scripts for $synth ovs // TODO: add indexBased option /** Display data point onchart * (create a new overlay in DataCube) * @param {(TS|TS[]|*)} x - Data point / TS / array of TS * @param {string} [name] - Overlay name * @param {Object} [sett] - Object with settings & OV type */ onchart(x, name, sett = {}, _id) { let off = 0 name = name || u.get_fn_id('Onchart', _id) if (x && x.__id__) { off = x.__offset__ || 0 x = x[0] } if (Array.isArray(x) && x[0] && x[0].__id__) { off = x[0].__offset__ || 0 x = x.map(x => x[0]) } if (!this.env.onchart[name]) { let type = sett.type delete sett.type sett.$synth = true sett.skipNaN = true let post = Array.isArray(x) ? 's': '' this.env.onchart[name] = { name: name, type: type || 'Spline' + post, data: [], settings: sett, scripts: false, grid: sett.grid || {} } } off *= se.tf let v = Array.isArray(x) ? [se.t + off, ...x] : [se.t + off, x] u.update(this.env.onchart[name].data, v) } /** Display data point offchart * (create a new overlay in DataCube) * @param {(TS|TS[]|*)} x - Data point / TS / array of TS * @param {string} [name] - Overlay name * @param {Object} [sett] - Object with settings & OV type */ offchart(x, name, sett = {}, _id) { name = name || u.get_fn_id('Offchart', _id) let off = 0 if (x && x.__id__) { off = x.__offset__ || 0 x = x[0] } if (Array.isArray(x) && x[0] && x[0].__id__) { off = x[0].__offset__ || 0 x = x.map(x => x[0]) } if (!this.env.offchart[name]) { let type = sett.type delete sett.type sett.$synth = true sett.skipNaN = true let post = Array.isArray(x) ? 's': '' this.env.offchart[name] = { name: name, type: type || 'Spline' + post, data: [], settings: sett, scripts: false, grid: sett.grid || {} } } off *= se.tf let v = Array.isArray(x) ? [se.t + off, ...x] : [se.t + off, x] u.update(this.env.offchart[name].data, v) } /** Returns true when the candle(<tf>) is being closed * (create a new overlay in DataCube) * @param {(number|string)} tf - Timeframe in ms or as a string * @return {boolean} */ onclose(tf) { if (!this.env.shared.onclose) return false if (!tf) tf = se.tf return (se.t + se.tf) % u.tf_from_str(tf) === 0 } /** Sends settings update * (can be called from init(), update() or post()) * @param {Object} upd - Settings update (object to merge) */ settings(upd) { this.env.send_modify({ settings: upd }) Object.assign(this.env.src.sett, upd) } /** Shifts TS left or right by "num" candles * @param {number} num - Offset measured in candles * @return {TS} - New / existing time-series */ offset(src, num, _id) { if (src.__id__) { src.__offset__ = num return src } let id = this._tsid(_id, `offset(${num})`) let out = ts(src, id) out.__offset__ = num return out } // percentile_linear_interpolation linearint() { // TODO: this } // percentile_nearest_rank nearestrank() { // TODO: this } /** The current time * @return {number} - timestamp */ now() { return new Date().getTime() } percentrank() { // TODO: this } /** Returns price of the pivot high point * Tip: works best with `offset` function * @param {TS} src - Input * @param {number} left - left threshold, candles * @param {number} right - right threshold, candles * @return {TS} - New time-series */ pivothigh(src, left, right, _id) { let id = this._tsid(_id, `pivothigh(${left},${right})`) let len = left + right + 1 let top = src[right] for (var i = 0; i < len; i++) { if (top <= src[i] && i !== right) { return this.ts(NaN, id, src.__tf__) } } return this.ts(top, id, src.__tf__) } /** Returns price of the pivot low point * Tip: works best with `offset` function * @param {TS} src - Input * @param {number} left - left threshold, candles * @param {number} right - right threshold, candles * @return {TS} - New time-series */ pivotlow(src, left, right, _id) { let id = this._tsid(_id, `pivotlow(${left},${right})`) let len = left + right + 1 let bot = src[right] for (var i = 0; i < len; i++) { if (bot >= src[i] && i !== right) { return this.ts(NaN, id, src.__tf__) } } return this.ts(bot, id, src.__tf__) } /** Shortcut for Math.pow() * @param {number} x The variable * @return {number} */ pow(x) { return Math.pow(x) } /** Test if "src" TS is rising for "len" candles * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ rising(src, len, _id) { let id = this._tsid(_id, `rising(${len})`) let top = src[0] for (var i = 1; i < len + 1; i++) { if (top <= src[i]) { return this.ts(false, id, src.__tf__) } } return this.ts(true, id, src.__tf__) } /** Exponentially MA with alpha = 1 / length * Used in RSI * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ rma(src, len, _id) { let id = this._tsid(_id, `rma(${len})`) let a = len let sum = this.ts(0, id, src.__tf__) sum[0] = this.na(sum[1]) ? this.sma(src, len, id)[0] : (src[0] + (a - 1) * this.nz(sum[1])) / a return sum } /** Rate of Change * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ roc(src, len, _id) { let id = this._tsid(_id, `roc(${len})`) return this.ts( 100 * (src[0] - src[len]) / src[len], id, src.__tf__ ) } /** Shortcut for Math.round() * @param {number} x The variable * @return {number} */ round(x) { return Math.round(x) } /** Relative Strength Index * @param {TS} x - First Input * @param {number|TS} y - Second Input * @return {TS} - New time-series */ rsi(x, y, _id) { // Check if y is a timeseries if (!this.na(y) && y.__id__) { var id = this._tsid(_id, `rsi(x,y)`) var rsi = 100 - 100 / (1 + this.div(x, y, id)[0]) } else { var id = this._tsid(_id, `rsi(${y})`) let ch = this.change(x, 1, _id)[0] let pc = this.ts(Math.max(ch, 0), id+'1', x.__tf__) let nc = this.ts(-Math.min(ch, 0), id+'2', x.__tf__) let up = this.rma(pc, y, id+'3')[0] let down = this.rma(nc, y, id+'4')[0] var rsi = down === 0 ? 100 : ( up === 0 ? 0 : (100 - (100 / (1 + up / down))) ) } return this.ts(rsi, id+'5', x.__tf__) } /** Parabolic SAR * @param {number} start - Start * @param {number} inc - Increment * @param {number} max - Maximum * @return {TS} - New time-series */ sar(start, inc, max, _id, _tf) { // Source: Parabolic SAR by imuradyan // TODO: simplify the code // TODO: fix the custom TF mode let id = this._tsid(_id, `sar(${start},${inc},${max})`) let tfs = _tf || '' let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let close = this.env.shared[`close${tfs}`] let minTick = 0 //1e-7 let out = this.ts(undefined, id+'1', _tf) let pos = this.ts(undefined, id+'2', _tf) let maxMin = this.ts(undefined, id+'3', _tf) let acc = this.ts(undefined, id+'4', _tf) let n = _tf ? out.__len__ - 1 : se.iter let prev let outSet = false if (n >= 1) { prev = out[1] if (n === 1) { if (close[0] > close[1]) { pos[0] = 1 maxMin[0] = Math.max(high[0], high[1]) prev = Math.min(low[0], low[1]) } else { pos[0] = -1 maxMin[0] = Math.min(low[0], low[1]) prev = Math.max(high[0], high[1]) } acc[0] = start } else { pos[0] = pos[1] acc[0] = acc[1] maxMin[0] = maxMin[1] } if (pos[0] === 1) { if (high[0] > maxMin[0]) { maxMin[0] = high[0] acc[0] = Math.min(acc[0] + inc, max) } if (low[0] <= prev) { pos[0] = -1 out[0] = maxMin[0] maxMin[0] = low[0] acc[0] = start outSet = true } } else { if (low[0] < maxMin[0]) { maxMin[0] = low[0] acc[0] = Math.min(acc[0] + inc, max) } if (high[0] >= prev) { pos[0] = 1 out[0] = maxMin[0] maxMin[0] = high[0] acc[0] = start outSet = true } } if (!outSet) { out[0] = prev + acc[0] * (maxMin[0] - prev) if (pos[0] === 1) if (out[0] >= low[0]) out[0] = low[0] - minTick if (pos[0] === -1) if (out[0] <= high[0]) out[0] = high[0] + minTick } } return out } /** Returns seconds of a given timestamp * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Hour */ second(time) { return new Date(time || se.t).getUTCSeconds() } /** Shortcut for Math.sing() * @param {number} x The variable * @return {number} */ sign(x) { return Math.sign(x) } /** Sine function * @param {number} x The variable * @return {number} */ sin(x) { return Math.sin(x) } /** Simple Moving Average * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ sma(src, len, _id) { let id = this._tsid(_id, `sma(${len})`) let sum = 0 for (var i = 0; i < len; i++) { sum = sum + src[i] } return this.ts(sum / len, id, src.__tf__) } /** Shortcut for Math.sqrt() * @param {number} x The variable * @return {number} */ sqrt(x) { return Math.sqrt(x) } /** Standard deviation * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ stdev(src, len, _id) { let sumf = (x, y) => { let res = x + y return res } let id = this._tsid(_id, `stdev(${len})`) let avg = this.sma(src, len, id) let sqd = 0 for (var i = 0; i < len; i++) { let sum = sumf(src[i], -avg[0]) sqd += sum * sum } return this.ts(Math.sqrt(sqd / len), id, src.__tf__) } /** Stochastic * @param {TS} src - Input * @param {TS} high - TS of high * @param {TS} low - TS of low * @param {number} len - Length * @return {TS} - New time-series */ stoch(src, high, low, len, _id) { let id = this._tsid(_id, `sum(${len})`) let x = 100 * (src[0] - this.lowest(low, len)[0]) let y = this.highest(high, len)[0] - this.lowest(low, len)[0] return this.ts(x / y, id, src.__tf__) } /** Returns the sliding sum of last "len" values of the source * @param {TS} src - Input * @param {number} len - Length * @return {TS} - New time-series */ sum(src, len, _id) { let id = this._tsid(_id, `sum(${len})`) let sum = 0 for (var i = 0; i < len; i++) { sum = sum + src[i] } return this.ts(sum, id, src.__tf__) } /** Supertrend Indicator * @param {number} factor - ATR multiplier * @param {number} atrlen - Length of ATR * @return {TS[]} - Supertrend line and direction of trend */ supertrend(factor, atrlen, _id, _tf) { let id = this._tsid(_id, `supertrend(${factor},${atrlen})`) let tfs = _tf || '' let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let close = this.env.shared[`close${tfs}`] let hl2 = (high[0] + low[0]) * 0.5 let atr = factor * this.atr(atrlen, id+'1', _tf)[0] let ls = this.ts(hl2 - atr, id+'2', _tf) let ls1 = this.nz(ls[1], ls[0]) ls[0] = close[1] > ls1 ? Math.max(ls[0], ls1) : ls[0] let ss = this.ts(hl2 + atr, id+'3', _tf) let ss1 = this.nz(ss[1], ss) ss[0] = close[1] < ss1 ? Math.min(ss[0], ss1) : ss[0] let dir = this.ts(1, id+'4', _tf) dir[0] = this.nz(dir[1], dir[0]) dir[0] = dir[0] === -1 && close[0] > ss1 ? 1 : (dir[0] === 1 && close[0] < ls1 ? -1 : dir[0]) let plot = this.ts(dir[0] === 1 ? ls[0] : ss[0], id+'5', _tf) return [plot, this.neg(dir, id+'6')] } /** Symmetrically Weighted Moving Average * @param {TS} src - Input * @return {TS} - New time-series */ swma(src, _id) { let id = this._tsid(_id, `swma`) let sum = src[3] * this.SWMA[0] + src[2] * this.SWMA[1] + src[1] * this.SWMA[2] + src[0] * this.SWMA[3] return this.ts(sum, id, src.__tf__) } /** Creates a new Symbol. * @param {*} x - Something, depends on arg variation * @param {*} y - Something, depends on arg variation * @return {Sym} * Argument variations: * <data>(Array), [<params>(Object)] * <ts>(TS), [<params>(Object)] * <point>(Number), [<params>(Object)] * <tf>(String) 1m, 5m, 1H, etc. (uses main OHLCV) * Params object: { * id: <String>, * tf: <String|Number>, * aggtype: <String> (TODO: Type of aggregation) * format: <String> (Data format, e.g. "time:price:vol") * window: <String|Number> (Aggregation window) * main <true|false> (Use as the main chart) * } */ sym(x, y = {}, _id) { let id = y.id || this._tsid(_id, `sym`) y.id = id if (this.env.syms[id]) { this.env.syms[id].update(x) return this.env.syms[id] } switch(typeof x) { case 'object': var sym = new Sym(x, y) this.env.syms[id] = sym if (x.__id__) { sym.data_type = TSS } else { sym.data_type = ARR } break case 'number': sym = new Sym(null, y) sym.data_type = NUM break case 'string': y.tf = x sym = new Sym(se.data.ohlcv.data, y) sym.data_type = ARR break } this.env.syms[id] = sym return sym } /** Tangent function * @param {number} x The variable * @return {number} */ tan(x) { return Math.tan(x) } time(res, sesh) { // TODO: this } timestamp() { // TODO: this } /** True Range * @param {TS} fixnan - Fix NaN values * @return {TS} - New time-series */ tr(fixnan = false, _id, _tf) { let id = this._tsid(_id, `tr(${fixnan})`) let tfs = _tf || '' let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let close = this.env.shared[`close${tfs}`] let res = 0 if (this.na(close[1]) && fixnan) { res = high[0] - low[0] } else { res = Math.max( high[0] - low[0], Math.abs(high[0] - close[1]), Math.abs(low[0] - close[1]) ) } return this.ts(res, id, _tf) } /** True strength index * @param {TS} src - Input * @param {number} short - Short length * @param {number} long - Long length * @return {TS} - New time-series */ tsi(src, short, long, _id) { let id = this._tsid(_id, `tsi(${short},${long})`) let m = this.change(src, 1, id+'0') let m_abs = this.ts(Math.abs(m[0]), id+'1', src.__tf__) let tsi = ( this.ema(this.ema(m, long, id+'1'), short, id+'2')[0] / this.ema(this.ema(m_abs, long, id+'3'), short, id+'4')[0] ) return this.ts(tsi, id, src.__tf__) } variance(src, len) { // TODO: this } vwap(src) { // TODO: this } /** Volume Weighted Moving Average * @param {TS} src - Input * @param {number} len - length * @return {TS} - New time-series */ vwma(src, len, _id) { let id = this._tsid(_id, `vwma(${len})`) let vol = this.env.shared.vol let sxv = this.ts(src[0] * vol[0], id+'1', src.__tf__) let res = this.sma(sxv, len, id+'2')[0] / this.sma(vol, len, id+'3')[0] return this.ts(res, id+'4', src.__tf__) } /** Week of year, literally * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Week */ weekofyear(time) { let date = new Date(time || se.t) date.setUTCHours(0, 0, 0, 0) date.setDate(date.getUTCDate() + 3 - (date.getUTCDay() + 6) % 7) let week1 = new Date(date.getUTCFullYear(), 0, 4) return 1 + Math.round( ((date - week1) / 86400000 - 3 + (week1.getUTCDay() + 6) % 7) / 7 ) } /** Weighted moving average * @param {TS} src - Input * @param {number} len - length * @return {TS} - New time-series */ wma(src, len, _id) { let id = this._tsid(_id, `wma(${len})`) let norm = 0 let sum = 0 for (var i = 0; i < len; i++) { let w = (len - i) * len norm += w sum += src[i] * w } return this.ts(sum / norm, id, src.__tf__) } /** Williams %R * @param {number} len - length * @return {TS} - New time-series */ wpr(len, _id, _tf) { let id = this._tsid(_id, `wpr(${len})`) let tfs = _tf || '' let high = this.env.shared[`high${tfs}`] let low = this.env.shared[`low${tfs}`] let close = this.env.shared[`close${tfs}`] let hh = this.highest(high, len, id) let ll = this.lowest(low, len, id) let res = (hh[0] - close[0]) / (hh[0] - ll[0]) return this.ts(-res * 100 , id, _tf) } /** Year * @param {number} [time] - Time in ms (current t, if not defined) * @return {number} - Year */ year(time) { return new Date(time || se.t).getUTCFullYear() } }