UNPKG

toptrader-vue-js

Version:
1,605 lines (1,451 loc) 43.7 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(); } }