UNPKG

klinecharts

Version:

Lightweight k-line chart built with html5 canvas

2,070 lines 351 kB
/** * @license * KLineChart v10.0.0-beta2 * Copyright (c) 2019 lihu. * Licensed under Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0 */ var DEV = process.env.NODE_ENV === "development"; function log(templateText, tagStyle, messageStyle, api, invalidParam, append) { if (DEV) { const apiStr = api !== "" ? `Call api \`${api}\`${invalidParam !== "" || append !== "" ? ", " : "."}` : ""; const invalidParamStr = invalidParam !== "" ? `invalid parameter \`${invalidParam}\`${append !== "" ? ", " : "."}` : ""; console.log(templateText, tagStyle, messageStyle, apiStr, invalidParamStr, append !== "" ? append : ""); } } function logWarn(api, invalidParam, append) { log("%c😑 klinecharts warning%c %s%s%s", "padding:3px 4px;border-radius:2px;color:#ffffff;background-color:#FF9600", "color:#FF9600", api, invalidParam, append ?? ""); } function logError(api, invalidParam, append) { log("%c😟 klinecharts error%c %s%s%s", "padding:3px 4px;border-radius:2px;color:#ffffff;background-color:#F92855;", "color:#F92855;", api, invalidParam, append ?? ""); } function logTag() { log("%c❤️ Welcome to klinecharts. Version is 10.0.0-beta2", "border-radius:4px;border:dashed 1px #1677FF;line-height:70px;padding:0 20px;margin:16px 0;font-size:14px;color:#1677FF;", "", "", "", ""); } function merge(target, source) { if (!isObject(target) && !isObject(source)) return; for (const key in source) if (Object.prototype.hasOwnProperty.call(source, key)) { const targetProp = target[key]; const sourceProp = source[key]; if (isObject(sourceProp) && isObject(targetProp)) merge(targetProp, sourceProp); else target[key] = clone(sourceProp); } } function clone(target) { if (!isObject(target)) return target; let copy = null; if (isArray(target)) copy = []; else copy = {}; for (const key in target) if (Object.prototype.hasOwnProperty.call(target, key)) { const v = target[key]; if (isObject(v)) copy[key] = clone(v); else copy[key] = v; } return copy; } function isArray(value) { return Object.prototype.toString.call(value) === "[object Array]"; } function isFunction(value) { return typeof value === "function"; } function isObject(value) { return typeof value === "object" && isValid(value); } function isNumber(value) { return typeof value === "number" && Number.isFinite(value); } function isValid(value) { return value !== null && value !== void 0; } function isBoolean(value) { return typeof value === "boolean"; } function isString(value) { return typeof value === "string"; } var reEscapeChar = /\\(\\)?/g; var rePropName = RegExp("[^.[\\]]+|\\[(?:([^\"'][^[]*)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))", "g"); function formatValue(data, key, defaultValue) { if (isValid(data)) { const path = []; key.replace(rePropName, (subString, ...args) => { let k = subString; if (isValid(args[1])) k = args[2].replace(reEscapeChar, "$1"); else if (isValid(args[0])) k = args[0].trim(); path.push(k); return ""; }); let value = data; let index = 0; const length = path.length; while (isValid(value) && index < length) value = value?.[path[index++]]; return isValid(value) ? value : defaultValue ?? "--"; } return defaultValue ?? "--"; } function formatTimestampToDateTime(dateTimeFormat, timestamp) { const date = {}; dateTimeFormat.formatToParts(new Date(timestamp)).forEach(({ type, value }) => { switch (type) { case "year": date.YYYY = value; break; case "month": date.MM = value; break; case "day": date.DD = value; break; case "hour": date.HH = value === "24" ? "00" : value; break; case "minute": date.mm = value; break; case "second": date.ss = value; break; default: break; } }); return date; } function formatTimestampByTemplate(dateTimeFormat, timestamp, template) { const date = formatTimestampToDateTime(dateTimeFormat, timestamp); return template.replace(/YYYY|MM|DD|HH|mm|ss/g, (key) => date[key]); } function formatPrecision(value, precision) { const v = +value; if (isNumber(v)) return v.toFixed(precision ?? 2); return `${value}`; } function formatBigNumber(value) { const v = +value; if (isNumber(v)) { if (v > 1e9) return `${+(v / 1e9).toFixed(3)}B`; if (v > 1e6) return `${+(v / 1e6).toFixed(3)}M`; if (v > 1e3) return `${+(v / 1e3).toFixed(3)}K`; } return `${value}`; } function formatThousands(value, sign) { const vl = `${value}`; if (sign.length === 0) return vl; if (vl.includes(".")) { const arr = vl.split("."); return `${arr[0].replace(/(\d)(?=(\d{3})+$)/g, ($1) => `${$1}${sign}`)}.${arr[1]}`; } return vl.replace(/(\d)(?=(\d{3})+$)/g, ($1) => `${$1}${sign}`); } function formatFoldDecimal(value, threshold) { const vl = `${value}`; if (new RegExp("\\.0{" + threshold + ",}[1-9][0-9]*$").test(vl)) { const result = vl.split("."); const lastIndex = result.length - 1; const v = result[lastIndex]; const match = /0*/.exec(v); if (isValid(match)) { const count = match[0].length; result[lastIndex] = v.replace(/0*/, `0{${count}}`); return result.join("."); } } return vl; } function formatTemplateString(template, params) { return template.replace(/\{(\w+)\}/g, (_, key) => { const value = params[key]; if (isValid(value)) return value; return `{${key}}`; }); } var measureCtx = null; function getPixelRatio(canvas) { return canvas.ownerDocument.defaultView?.devicePixelRatio ?? 1; } function createFont(size, weight, family) { return `${weight ?? "normal"} ${size ?? 12}px ${family ?? "Helvetica Neue"}`; } function calcTextWidth(text, size, weight, family) { if (!isValid(measureCtx)) { const canvas = document.createElement("canvas"); const pixelRatio = getPixelRatio(canvas); measureCtx = canvas.getContext("2d"); measureCtx.scale(pixelRatio, pixelRatio); } measureCtx.font = createFont(size, weight, family); return Math.round(measureCtx.measureText(text).width); } function createDefaultBounding(bounding) { const defaultBounding = { width: 0, height: 0, left: 0, right: 0, top: 0, bottom: 0 }; if (isValid(bounding)) merge(defaultBounding, bounding); return defaultBounding; } var UpdateLevel = function(UpdateLevel) { UpdateLevel[UpdateLevel["Main"] = 0] = "Main"; UpdateLevel[UpdateLevel["Overlay"] = 1] = "Overlay"; UpdateLevel[UpdateLevel["Separator"] = 2] = "Separator"; UpdateLevel[UpdateLevel["Drawer"] = 3] = "Drawer"; UpdateLevel[UpdateLevel["All"] = 4] = "All"; return UpdateLevel; }({}); function requestAnimationFrame(fn) { if (isFunction(window.requestAnimationFrame)) return window.requestAnimationFrame(fn); return window.setTimeout(fn, 20); } function cancelAnimationFrame(id) { if (isFunction(window.cancelAnimationFrame)) window.cancelAnimationFrame(id); else window.clearTimeout(id); } var Animation = class { constructor(options) { this._options = { duration: 500, iterationCount: 1 }; this._currentIterationCount = 0; this._running = false; this._time = 0; merge(this._options, options); } _loop() { this._running = true; const step = () => { if (this._running) { const diffTime = (/* @__PURE__ */ new Date()).getTime() - this._time; if (diffTime < this._options.duration) { this._doFrameCallback?.(diffTime); requestAnimationFrame(step); } else { this.stop(); this._currentIterationCount++; if (this._currentIterationCount < this._options.iterationCount) this.start(); } } }; requestAnimationFrame(step); } doFrame(callback) { this._doFrameCallback = callback; return this; } setDuration(duration) { this._options.duration = duration; return this; } setIterationCount(iterationCount) { this._options.iterationCount = iterationCount; return this; } start() { if (!this._running) { this._time = (/* @__PURE__ */ new Date()).getTime(); this._loop(); } } stop() { if (this._running) this._doFrameCallback?.(this._options.duration); this._running = false; } }; var baseId = 1; var prevIdTimestamp = (/* @__PURE__ */ new Date()).getTime(); function createId(prefix) { const timestamp = (/* @__PURE__ */ new Date()).getTime(); if (timestamp === prevIdTimestamp) ++baseId; else baseId = 1; prevIdTimestamp = timestamp; return `${prefix ?? ""}${timestamp}_${baseId}`; } function createDom(tagName, styles) { const dom = document.createElement(tagName); const s = styles ?? {}; for (const key in s) dom.style[key] = s[key] ?? ""; return dom; } function binarySearchNearest(dataList, valueKey, targetValue) { let left = 0; let right = 0; for (right = dataList.length - 1; left !== right;) { const midIndex = Math.floor((right + left) / 2); const mid = right - left; const midValue = dataList[midIndex][valueKey]; if (targetValue === dataList[left][valueKey]) return left; if (targetValue === dataList[right][valueKey]) return right; if (targetValue === midValue) return midIndex; if (targetValue > midValue) left = midIndex; else right = midIndex; if (mid <= 2) break; } return left; } function nice(value) { const exponent = Math.floor(log10(value)); const exp10 = index10(exponent); const f = value / exp10; let nf = 0; if (f < 1.5) nf = 1; else if (f < 2.5) nf = 2; else if (f < 3.5) nf = 3; else if (f < 4.5) nf = 4; else if (f < 5.5) nf = 5; else if (f < 6.5) nf = 6; else nf = 8; value = nf * exp10; return +value.toFixed(Math.abs(exponent)); } function round(value, precision) { precision = Math.max(0, precision ?? 0); const pow = Math.pow(10, precision); return Math.round(value * pow) / pow; } function getPrecision(value) { const str = value.toString(); const eIndex = str.indexOf("e"); if (eIndex > 0) { const precision = +str.slice(eIndex + 1); return precision < 0 ? -precision : 0; } const dotIndex = str.indexOf("."); return dotIndex < 0 ? 0 : str.length - 1 - dotIndex; } function getMaxMin(dataList, maxKey, minKey) { const maxMin = [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; const dataLength = dataList.length; let index = 0; while (index < dataLength) { const data = dataList[index]; maxMin[0] = Math.max(data[maxKey] ?? Number.MIN_SAFE_INTEGER, maxMin[0]); maxMin[1] = Math.min(data[minKey] ?? Number.MAX_SAFE_INTEGER, maxMin[1]); ++index; } return maxMin; } function log10(value) { if (value === 0) return 0; return Math.log10(value); } function index10(value) { return Math.pow(10, value); } function getDefaultVisibleRange() { return { from: 0, to: 0, realFrom: 0, realTo: 0 }; } var TaskScheduler = class { constructor(callback) { this._holdingTasks = null; this._running = false; this._callback = callback; } add(tasks) { if (!this._running) this._runTask(tasks); else if (isValid(this._holdingTasks)) this._holdingTasks = { ...this._holdingTasks, ...tasks }; else this._holdingTasks = tasks; } async _runTask(tasks) { this._running = true; try { await Promise.all(Object.values(tasks)); } finally { this._running = false; this._callback?.(); if (isValid(this._holdingTasks)) { const next = this._holdingTasks; this._runTask(next); this._holdingTasks = null; } } } clear() { this._holdingTasks = null; } }; var SymbolDefaultPrecisionConstants = { PRICE: 2, VOLUME: 0 }; var Action = class { constructor() { this._callbacks = []; } subscribe(callback) { if (this._callbacks.indexOf(callback) < 0) this._callbacks.push(callback); } unsubscribe(callback) { if (isFunction(callback)) { const index = this._callbacks.indexOf(callback); if (index > -1) this._callbacks.splice(index, 1); } else this._callbacks = []; } execute(data) { this._callbacks.forEach((callback) => { callback(data); }); } isEmpty() { return this._callbacks.length === 0; } }; function isTransparent(color) { return color === "transparent" || color === "none" || /^[rR][gG][Bb][Aa]\(([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\s]*,){3}[\s]*0[\s]*\)$/.test(color) || /^[hH][Ss][Ll][Aa]\(([\s]*(360|3[0-5][0-9]|[012]?[0-9][0-9]?)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*,){2}([\s]*0[\s]*)\)$/.test(color); } function hexToRgb(hex, alpha) { const h = hex.replace(/^#/, ""); const i = parseInt(h, 16); return `rgba(${i >> 16 & 255}, ${i >> 8 & 255}, ${i & 255}, ${alpha ?? 1})`; } var Color = { RED: "#F92855", GREEN: "#2DC08E", WHITE: "#FFFFFF", GREY: "#76808F", BLUE: "#1677FF" }; function getDefaultGridStyle() { return { show: true, horizontal: { show: true, size: 1, color: "#EDEDED", style: "dashed", dashedValue: [2, 2] }, vertical: { show: true, size: 1, color: "#EDEDED", style: "dashed", dashedValue: [2, 2] } }; } function getDefaultCandleStyle() { const highLow = { show: true, color: Color.GREY, textOffset: 5, textSize: 10, textFamily: "Helvetica Neue", textWeight: "normal" }; return { type: "candle_solid", bar: { compareRule: "current_open", upColor: Color.GREEN, downColor: Color.RED, noChangeColor: Color.GREY, upBorderColor: Color.GREEN, downBorderColor: Color.RED, noChangeBorderColor: Color.GREY, upWickColor: Color.GREEN, downWickColor: Color.RED, noChangeWickColor: Color.GREY }, area: { lineSize: 2, lineColor: Color.BLUE, smooth: false, value: "close", backgroundColor: [{ offset: 0, color: hexToRgb(Color.BLUE, .01) }, { offset: 1, color: hexToRgb(Color.BLUE, .2) }], point: { show: true, color: Color.BLUE, radius: 4, rippleColor: hexToRgb(Color.BLUE, .3), rippleRadius: 8, animation: true, animationDuration: 1e3 } }, priceMark: { show: true, high: { ...highLow }, low: { ...highLow }, last: { show: true, compareRule: "current_open", upColor: Color.GREEN, downColor: Color.RED, noChangeColor: Color.GREY, line: { show: true, style: "dashed", dashedValue: [4, 4], size: 1 }, text: { show: true, style: "fill", size: 12, paddingLeft: 4, paddingTop: 4, paddingRight: 4, paddingBottom: 4, borderColor: "transparent", borderStyle: "solid", borderSize: 0, borderDashedValue: [2, 2], color: Color.WHITE, family: "Helvetica Neue", weight: "normal", borderRadius: 2 }, extendTexts: [] } }, tooltip: { offsetLeft: 4, offsetTop: 6, offsetRight: 4, offsetBottom: 6, showRule: "always", showType: "standard", rect: { position: "fixed", paddingLeft: 4, paddingRight: 4, paddingTop: 4, paddingBottom: 4, offsetLeft: 4, offsetTop: 4, offsetRight: 4, offsetBottom: 4, borderRadius: 4, borderSize: 1, borderColor: "#F2F3F5", color: "#FEFEFE" }, title: { show: true, size: 14, family: "Helvetica Neue", weight: "normal", color: Color.GREY, marginLeft: 8, marginTop: 4, marginRight: 8, marginBottom: 4, template: "{ticker} · {period}" }, legend: { size: 12, family: "Helvetica Neue", weight: "normal", color: Color.GREY, marginLeft: 8, marginTop: 4, marginRight: 8, marginBottom: 4, defaultValue: "n/a", template: [ { title: "time", value: "{time}" }, { title: "open", value: "{open}" }, { title: "high", value: "{high}" }, { title: "low", value: "{low}" }, { title: "close", value: "{close}" }, { title: "volume", value: "{volume}" } ] }, features: [] } }; } function getDefaultIndicatorStyle() { const alphaGreen = hexToRgb(Color.GREEN, .7); const alphaRed = hexToRgb(Color.RED, .7); return { ohlc: { compareRule: "current_open", upColor: alphaGreen, downColor: alphaRed, noChangeColor: Color.GREY }, bars: [{ style: "fill", borderStyle: "solid", borderSize: 1, borderDashedValue: [2, 2], upColor: alphaGreen, downColor: alphaRed, noChangeColor: Color.GREY }], lines: [ "#FF9600", "#935EBD", Color.BLUE, "#E11D74", "#01C5C4" ].map((color) => ({ style: "solid", smooth: false, size: 1, dashedValue: [2, 2], color })), circles: [{ style: "fill", borderStyle: "solid", borderSize: 1, borderDashedValue: [2, 2], upColor: alphaGreen, downColor: alphaRed, noChangeColor: Color.GREY }], texts: [{ paddingLeft: 0, paddingTop: 0, paddingRight: 0, paddingBottom: 0, style: "fill", size: 12, color: Color.BLUE, family: "Helvetica Neue", weight: "normal", borderStyle: "solid", borderDashedValue: [2, 2], borderSize: 0, borderColor: "transparent", borderRadius: 0, backgroundColor: "transparent" }], lastValueMark: { show: false, text: { show: false, style: "fill", color: Color.WHITE, size: 12, family: "Helvetica Neue", weight: "normal", borderStyle: "solid", borderColor: "transparent", borderSize: 0, borderDashedValue: [2, 2], paddingLeft: 4, paddingTop: 4, paddingRight: 4, paddingBottom: 4, borderRadius: 2 } }, tooltip: { offsetLeft: 4, offsetTop: 6, offsetRight: 4, offsetBottom: 6, showRule: "always", showType: "standard", title: { show: true, showName: true, showParams: true, size: 12, family: "Helvetica Neue", weight: "normal", color: Color.GREY, marginLeft: 8, marginTop: 4, marginRight: 8, marginBottom: 4 }, legend: { size: 12, family: "Helvetica Neue", weight: "normal", color: Color.GREY, marginLeft: 8, marginTop: 4, marginRight: 8, marginBottom: 4, defaultValue: "n/a" }, features: [] } }; } function getDefaultAxisStyle() { return { show: true, size: "auto", axisLine: { show: true, color: "#DDDDDD", size: 1 }, tickText: { show: true, color: Color.GREY, size: 12, family: "Helvetica Neue", weight: "normal", marginStart: 4, marginEnd: 6 }, tickLine: { show: true, size: 1, length: 3, color: "#DDDDDD" } }; } function getDefaultCrosshairStyle() { return { show: true, horizontal: { show: true, line: { show: true, style: "dashed", dashedValue: [4, 2], size: 1, color: Color.GREY }, text: { show: true, style: "fill", color: Color.WHITE, size: 12, family: "Helvetica Neue", weight: "normal", borderStyle: "solid", borderDashedValue: [2, 2], borderSize: 1, borderColor: Color.GREY, borderRadius: 2, paddingLeft: 4, paddingRight: 4, paddingTop: 4, paddingBottom: 4, backgroundColor: Color.GREY }, features: [] }, vertical: { show: true, line: { show: true, style: "dashed", dashedValue: [4, 2], size: 1, color: Color.GREY }, text: { show: true, style: "fill", color: Color.WHITE, size: 12, family: "Helvetica Neue", weight: "normal", borderStyle: "solid", borderDashedValue: [2, 2], borderSize: 1, borderColor: Color.GREY, borderRadius: 2, paddingLeft: 4, paddingRight: 4, paddingTop: 4, paddingBottom: 4, backgroundColor: Color.GREY } } }; } function getDefaultOverlayStyle() { const pointBorderColor = hexToRgb(Color.BLUE, .35); const alphaBg = hexToRgb(Color.BLUE, .25); function text() { return { style: "fill", color: Color.WHITE, size: 12, family: "Helvetica Neue", weight: "normal", borderStyle: "solid", borderDashedValue: [2, 2], borderSize: 1, borderRadius: 2, borderColor: Color.BLUE, paddingLeft: 4, paddingRight: 4, paddingTop: 4, paddingBottom: 4, backgroundColor: Color.BLUE }; } return { point: { color: Color.BLUE, borderColor: pointBorderColor, borderSize: 1, radius: 5, activeColor: Color.BLUE, activeBorderColor: pointBorderColor, activeBorderSize: 3, activeRadius: 5 }, line: { style: "solid", smooth: false, color: Color.BLUE, size: 1, dashedValue: [2, 2] }, rect: { style: "fill", color: alphaBg, borderColor: Color.BLUE, borderSize: 1, borderRadius: 0, borderStyle: "solid", borderDashedValue: [2, 2] }, polygon: { style: "fill", color: Color.BLUE, borderColor: Color.BLUE, borderSize: 1, borderStyle: "solid", borderDashedValue: [2, 2] }, circle: { style: "fill", color: alphaBg, borderColor: Color.BLUE, borderSize: 1, borderStyle: "solid", borderDashedValue: [2, 2] }, arc: { style: "solid", color: Color.BLUE, size: 1, dashedValue: [2, 2] }, text: text() }; } function getDefaultSeparatorStyle() { return { size: 1, color: "#DDDDDD", fill: true, activeBackgroundColor: hexToRgb(Color.BLUE, .08) }; } function getDefaultStyles() { return { grid: getDefaultGridStyle(), candle: getDefaultCandleStyle(), indicator: getDefaultIndicatorStyle(), xAxis: getDefaultAxisStyle(), yAxis: getDefaultAxisStyle(), separator: getDefaultSeparatorStyle(), crosshair: getDefaultCrosshairStyle(), overlay: getDefaultOverlayStyle() }; } var DEFAULT_AXIS_ID = "default"; function getDefaultAxisRange() { return { from: 0, to: 0, range: 0, realFrom: 0, realTo: 0, realRange: 0, displayFrom: 0, displayTo: 0, displayRange: 0 }; } var AxisImp = class { constructor(parent) { this.scrollZoomEnabled = true; this._range = getDefaultAxisRange(); this._prevRange = getDefaultAxisRange(); this._ticks = []; this._autoCalcTickFlag = true; this._parent = parent; } getParent() { return this._parent; } buildTicks(force) { if (this._autoCalcTickFlag) this._range = this.createRangeImp(); if (this._prevRange.from !== this._range.from || this._prevRange.to !== this._range.to || force) { this._prevRange = this._range; this._ticks = this.createTicksImp(); return true; } return false; } getTicks() { return this._ticks; } setRange(range) { this._autoCalcTickFlag = false; this._range = range; } getRange() { return this._range; } setAutoCalcTickFlag(flag) { this._autoCalcTickFlag = flag; } getAutoCalcTickFlag() { return this._autoCalcTickFlag; } }; function eachFigures(indicator, dataIndex, barSpace, defaultStyles, eachFigureCallback) { const result = indicator.result; const figures = indicator.figures; const styles = indicator.styles; const textStyles = formatValue(styles, "texts", defaultStyles.texts); const textStyleCount = textStyles.length; const circleStyles = formatValue(styles, "circles", defaultStyles.circles); const circleStyleCount = circleStyles.length; const barStyles = formatValue(styles, "bars", defaultStyles.bars); const barStyleCount = barStyles.length; const lineStyles = formatValue(styles, "lines", defaultStyles.lines); const lineStyleCount = lineStyles.length; let textCount = 0; let circleCount = 0; let barCount = 0; let lineCount = 0; let defaultFigureStyles; let figureIndex = 0; figures.forEach((figure) => { switch (figure.type) { case "text": figureIndex = textCount; defaultFigureStyles = textStyles[textCount % textStyleCount]; textCount++; break; case "circle": { figureIndex = circleCount; const styles = circleStyles[circleCount % circleStyleCount]; defaultFigureStyles = { ...styles, color: styles.noChangeColor }; circleCount++; break; } case "bar": { figureIndex = barCount; const styles = barStyles[barCount % barStyleCount]; defaultFigureStyles = { ...styles, color: styles.noChangeColor }; barCount++; break; } case "line": figureIndex = lineCount; defaultFigureStyles = lineStyles[lineCount % lineStyleCount]; lineCount++; break; default: break; } if (isValid(figure.type)) { const ss = figure.styles?.({ data: { prev: result[dataIndex - 1], current: result[dataIndex], next: result[dataIndex + 1] }, indicator, barSpace, defaultStyles }); eachFigureCallback(figure, { ...defaultFigureStyles, ...ss }, figureIndex); } }); } var IndicatorImp = class { constructor(indicator) { this.yAxisId = DEFAULT_AXIS_ID; this.precision = 4; this.calcParams = []; this.shouldOhlc = false; this.shouldFormatBigNumber = false; this.visible = true; this.zLevel = 0; this.series = "normal"; this.figures = []; this.minValue = null; this.maxValue = null; this.styles = null; this.shouldUpdate = (prev, current) => { const calc = JSON.stringify(prev.calcParams) !== JSON.stringify(current.calcParams) || prev.figures !== current.figures || prev.calc !== current.calc; return { calc, draw: calc || prev.shortName !== current.shortName || prev.paneId !== current.paneId || prev.yAxisId !== current.yAxisId || prev.series !== current.series || prev.minValue !== current.minValue || prev.maxValue !== current.maxValue || prev.precision !== current.precision || prev.shouldOhlc !== current.shouldOhlc || prev.shouldFormatBigNumber !== current.shouldFormatBigNumber || prev.visible !== current.visible || prev.zLevel !== current.zLevel || prev.extendData !== current.extendData || prev.regenerateFigures !== current.regenerateFigures || prev.createTooltipDataSource !== current.createTooltipDataSource || prev.draw !== current.draw }; }; this.calc = () => []; this.regenerateFigures = null; this.createTooltipDataSource = null; this.draw = null; this.result = []; this._lockSeriesPrecision = false; this.override(indicator); this._lockSeriesPrecision = false; } override(indicator) { const { result, ...currentOthers } = this; this._prevIndicator = { ...clone(currentOthers), result }; const { id, name, shortName, precision, styles, figures, calcParams, ...others } = indicator; if (!isString(this.id) && isString(id)) this.id = id; if (!isString(this.name)) this.name = name ?? ""; this.shortName = shortName ?? this.shortName ?? this.name; if (isNumber(precision)) { this.precision = precision; this._lockSeriesPrecision = true; } if (isValid(styles)) { this.styles ??= {}; merge(this.styles, styles); } merge(this, others); if (isValid(calcParams)) { this.calcParams = calcParams; if (isFunction(this.regenerateFigures)) this.figures = this.regenerateFigures(this.calcParams); } this.figures = figures ?? this.figures; } setSeriesPrecision(precision) { if (!this._lockSeriesPrecision) this.precision = precision; } shouldUpdateImp() { const sort = this._prevIndicator.zLevel !== this.zLevel; const result = this.shouldUpdate(this._prevIndicator, this); if (isBoolean(result)) return { calc: result, draw: result, sort }; return { ...result, sort }; } async calcImp(dataList) { try { const result = await this.calc(dataList, this); this.result = result; return true; } catch (e) { return false; } } static extend(template) { class Custom extends IndicatorImp { constructor() { super(template); } } return Custom; } }; var averagePrice = { name: "AVP", shortName: "AVP", series: "price", precision: 2, figures: [{ key: "avp", title: "AVP: ", type: "line" }], calc: (dataList) => { let totalTurnover = 0; let totalVolume = 0; return dataList.map((kLineData) => { const avp = {}; const turnover = kLineData.turnover ?? 0; const volume = kLineData.volume ?? 0; totalTurnover += turnover; totalVolume += volume; if (totalVolume !== 0) avp.avp = totalTurnover / totalVolume; return avp; }); } }; var awesomeOscillator = { name: "AO", shortName: "AO", calcParams: [5, 34], figures: [{ key: "ao", title: "AO: ", type: "bar", baseValue: 0, styles: ({ data, indicator, defaultStyles }) => { const { prev, current } = data; const prevAo = prev?.ao ?? Number.MIN_SAFE_INTEGER; const currentAo = current?.ao ?? Number.MIN_SAFE_INTEGER; let color = ""; if (currentAo > prevAo) color = formatValue(indicator.styles, "bars[0].upColor", defaultStyles.bars[0].upColor); else color = formatValue(indicator.styles, "bars[0].downColor", defaultStyles.bars[0].downColor); return { color, style: currentAo > prevAo ? "stroke" : "fill", borderColor: color }; } }], calc: (dataList, indicator) => { const params = indicator.calcParams; const maxPeriod = Math.max(params[0], params[1]); let shortSum = 0; let longSum = 0; let short = 0; let long = 0; return dataList.map((kLineData, i) => { const ao = {}; const middle = (kLineData.low + kLineData.high) / 2; shortSum += middle; longSum += middle; if (i >= params[0] - 1) { short = shortSum / params[0]; const agoKLineData = dataList[i - (params[0] - 1)]; shortSum -= (agoKLineData.low + agoKLineData.high) / 2; } if (i >= params[1] - 1) { long = longSum / params[1]; const agoKLineData = dataList[i - (params[1] - 1)]; longSum -= (agoKLineData.low + agoKLineData.high) / 2; } if (i >= maxPeriod - 1) ao.ao = short - long; return ao; }); } }; var bias = { name: "BIAS", shortName: "BIAS", calcParams: [ 6, 12, 24 ], figures: [ { key: "bias1", title: "BIAS6: ", type: "line" }, { key: "bias2", title: "BIAS12: ", type: "line" }, { key: "bias3", title: "BIAS24: ", type: "line" } ], regenerateFigures: (params) => params.map((p, i) => ({ key: `bias${i + 1}`, title: `BIAS${p}: `, type: "line" })), calc: (dataList, indicator) => { const { calcParams: params, figures } = indicator; const closeSums = []; return dataList.map((kLineData, i) => { const bias = {}; const close = kLineData.close; params.forEach((p, index) => { closeSums[index] = (closeSums[index] ?? 0) + close; if (i >= p - 1) { const mean = closeSums[index] / params[index]; bias[figures[index].key] = (close - mean) / mean * 100; closeSums[index] -= dataList[i - (p - 1)].close; } }); return bias; }); } }; function getBollMd(dataList, ma) { const dataSize = dataList.length; let sum = 0; dataList.forEach((data) => { const closeMa = data.close - ma; sum += closeMa * closeMa; }); sum = Math.abs(sum); return Math.sqrt(sum / dataSize); } var bollingerBands = { name: "BOLL", shortName: "BOLL", series: "price", calcParams: [20, 2], precision: 2, shouldOhlc: true, figures: [ { key: "up", title: "UP: ", type: "line" }, { key: "mid", title: "MID: ", type: "line" }, { key: "dn", title: "DN: ", type: "line" } ], calc: (dataList, indicator) => { const params = indicator.calcParams; const p = params[0] - 1; let closeSum = 0; return dataList.map((kLineData, i) => { const close = kLineData.close; const boll = {}; closeSum += close; if (i >= p) { boll.mid = closeSum / params[0]; const md = getBollMd(dataList.slice(i - p, i + 1), boll.mid); boll.up = boll.mid + params[1] * md; boll.dn = boll.mid - params[1] * md; closeSum -= dataList[i - p].close; } return boll; }); } }; var brar = { name: "BRAR", shortName: "BRAR", calcParams: [26], figures: [{ key: "br", title: "BR: ", type: "line" }, { key: "ar", title: "AR: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; let hcy = 0; let cyl = 0; let ho = 0; let ol = 0; return dataList.map((kLineData, i) => { const brar = {}; const high = kLineData.high; const low = kLineData.low; const open = kLineData.open; const prevClose = (dataList[i - 1] ?? kLineData).close; ho += high - open; ol += open - low; hcy += high - prevClose; cyl += prevClose - low; if (i >= params[0] - 1) { if (ol !== 0) brar.ar = ho / ol * 100; else brar.ar = 0; if (cyl !== 0) brar.br = hcy / cyl * 100; else brar.br = 0; const agoKLineData = dataList[i - (params[0] - 1)]; const agoHigh = agoKLineData.high; const agoLow = agoKLineData.low; const agoOpen = agoKLineData.open; const agoPreClose = (dataList[i - params[0]] ?? dataList[i - (params[0] - 1)]).close; hcy -= agoHigh - agoPreClose; cyl -= agoPreClose - agoLow; ho -= agoHigh - agoOpen; ol -= agoOpen - agoLow; } return brar; }); } }; var bullAndBearIndex = { name: "BBI", shortName: "BBI", series: "price", precision: 2, calcParams: [ 3, 6, 12, 24 ], shouldOhlc: true, figures: [{ key: "bbi", title: "BBI: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; const maxPeriod = Math.max(...params); const closeSums = []; const mas = []; return dataList.map((kLineData, i) => { const bbi = {}; const close = kLineData.close; params.forEach((p, index) => { closeSums[index] = (closeSums[index] ?? 0) + close; if (i >= p - 1) { mas[index] = closeSums[index] / p; closeSums[index] -= dataList[i - (p - 1)].close; } }); if (i >= maxPeriod - 1) { let maSum = 0; mas.forEach((ma) => { maSum += ma; }); bbi.bbi = maSum / 4; } return bbi; }); } }; var commodityChannelIndex = { name: "CCI", shortName: "CCI", calcParams: [20], figures: [{ key: "cci", title: "CCI: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; const p = params[0] - 1; let tpSum = 0; const tpList = []; return dataList.map((kLineData, i) => { const cci = {}; const tp = (kLineData.high + kLineData.low + kLineData.close) / 3; tpSum += tp; tpList.push(tp); if (i >= p) { const maTp = tpSum / params[0]; const sliceTpList = tpList.slice(i - p, i + 1); let sum = 0; sliceTpList.forEach((tp) => { sum += Math.abs(tp - maTp); }); const md = sum / params[0]; cci.cci = md !== 0 ? (tp - maTp) / md / .015 : 0; const agoTp = (dataList[i - p].high + dataList[i - p].low + dataList[i - p].close) / 3; tpSum -= agoTp; } return cci; }); } }; var currentRatio = { name: "CR", shortName: "CR", calcParams: [ 26, 10, 20, 40, 60 ], figures: [ { key: "cr", title: "CR: ", type: "line" }, { key: "ma1", title: "MA1: ", type: "line" }, { key: "ma2", title: "MA2: ", type: "line" }, { key: "ma3", title: "MA3: ", type: "line" }, { key: "ma4", title: "MA4: ", type: "line" } ], calc: (dataList, indicator) => { const params = indicator.calcParams; const ma1ForwardPeriod = Math.ceil(params[1] / 2.5 + 1); const ma2ForwardPeriod = Math.ceil(params[2] / 2.5 + 1); const ma3ForwardPeriod = Math.ceil(params[3] / 2.5 + 1); const ma4ForwardPeriod = Math.ceil(params[4] / 2.5 + 1); let ma1Sum = 0; const ma1List = []; let ma2Sum = 0; const ma2List = []; let ma3Sum = 0; const ma3List = []; let ma4Sum = 0; const ma4List = []; const result = []; dataList.forEach((kLineData, i) => { const cr = {}; const prevData = dataList[i - 1] ?? kLineData; const prevMid = (prevData.high + prevData.close + prevData.low + prevData.open) / 4; const highSubPreMid = Math.max(0, kLineData.high - prevMid); const preMidSubLow = Math.max(0, prevMid - kLineData.low); if (i >= params[0] - 1) { if (preMidSubLow !== 0) cr.cr = highSubPreMid / preMidSubLow * 100; else cr.cr = 0; ma1Sum += cr.cr; ma2Sum += cr.cr; ma3Sum += cr.cr; ma4Sum += cr.cr; if (i >= params[0] + params[1] - 2) { ma1List.push(ma1Sum / params[1]); if (i >= params[0] + params[1] + ma1ForwardPeriod - 3) cr.ma1 = ma1List[ma1List.length - 1 - ma1ForwardPeriod]; ma1Sum -= result[i - (params[1] - 1)].cr ?? 0; } if (i >= params[0] + params[2] - 2) { ma2List.push(ma2Sum / params[2]); if (i >= params[0] + params[2] + ma2ForwardPeriod - 3) cr.ma2 = ma2List[ma2List.length - 1 - ma2ForwardPeriod]; ma2Sum -= result[i - (params[2] - 1)].cr ?? 0; } if (i >= params[0] + params[3] - 2) { ma3List.push(ma3Sum / params[3]); if (i >= params[0] + params[3] + ma3ForwardPeriod - 3) cr.ma3 = ma3List[ma3List.length - 1 - ma3ForwardPeriod]; ma3Sum -= result[i - (params[3] - 1)].cr ?? 0; } if (i >= params[0] + params[4] - 2) { ma4List.push(ma4Sum / params[4]); if (i >= params[0] + params[4] + ma4ForwardPeriod - 3) cr.ma4 = ma4List[ma4List.length - 1 - ma4ForwardPeriod]; ma4Sum -= result[i - (params[4] - 1)].cr ?? 0; } } result.push(cr); }); return result; } }; var differentOfMovingAverage = { name: "DMA", shortName: "DMA", calcParams: [ 10, 50, 10 ], figures: [{ key: "dma", title: "DMA: ", type: "line" }, { key: "ama", title: "AMA: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; const maxPeriod = Math.max(params[0], params[1]); let closeSum1 = 0; let closeSum2 = 0; let dmaSum = 0; const result = []; dataList.forEach((kLineData, i) => { const dma = {}; const close = kLineData.close; closeSum1 += close; closeSum2 += close; let ma1 = 0; let ma2 = 0; if (i >= params[0] - 1) { ma1 = closeSum1 / params[0]; closeSum1 -= dataList[i - (params[0] - 1)].close; } if (i >= params[1] - 1) { ma2 = closeSum2 / params[1]; closeSum2 -= dataList[i - (params[1] - 1)].close; } if (i >= maxPeriod - 1) { const dif = ma1 - ma2; dma.dma = dif; dmaSum += dif; if (i >= maxPeriod + params[2] - 2) { dma.ama = dmaSum / params[2]; dmaSum -= result[i - (params[2] - 1)].dma ?? 0; } } result.push(dma); }); return result; } }; var directionalMovementIndex = { name: "DMI", shortName: "DMI", calcParams: [14, 6], figures: [ { key: "pdi", title: "PDI: ", type: "line" }, { key: "mdi", title: "MDI: ", type: "line" }, { key: "adx", title: "ADX: ", type: "line" }, { key: "adxr", title: "ADXR: ", type: "line" } ], calc: (dataList, indicator) => { const params = indicator.calcParams; let trSum = 0; let hSum = 0; let lSum = 0; let mtr = 0; let dmp = 0; let dmm = 0; let dxSum = 0; let adx = 0; const result = []; dataList.forEach((kLineData, i) => { const dmi = {}; const prevKLineData = dataList[i - 1] ?? kLineData; const preClose = prevKLineData.close; const high = kLineData.high; const low = kLineData.low; const hl = high - low; const hcy = Math.abs(high - preClose); const lcy = Math.abs(preClose - low); const hhy = high - prevKLineData.high; const lyl = prevKLineData.low - low; const tr = Math.max(Math.max(hl, hcy), lcy); const h = hhy > 0 && hhy > lyl ? hhy : 0; const l = lyl > 0 && lyl > hhy ? lyl : 0; trSum += tr; hSum += h; lSum += l; if (i >= params[0] - 1) { if (i > params[0] - 1) { mtr = mtr - mtr / params[0] + tr; dmp = dmp - dmp / params[0] + h; dmm = dmm - dmm / params[0] + l; } else { mtr = trSum; dmp = hSum; dmm = lSum; } let pdi = 0; let mdi = 0; if (mtr !== 0) { pdi = dmp * 100 / mtr; mdi = dmm * 100 / mtr; } dmi.pdi = pdi; dmi.mdi = mdi; let dx = 0; if (mdi + pdi !== 0) dx = Math.abs(mdi - pdi) / (mdi + pdi) * 100; dxSum += dx; if (i >= params[0] * 2 - 2) { if (i > params[0] * 2 - 2) adx = (adx * (params[0] - 1) + dx) / params[0]; else adx = dxSum / params[0]; dmi.adx = adx; if (i >= params[0] * 2 + params[1] - 3) dmi.adxr = ((result[i - (params[1] - 1)].adx ?? 0) + adx) / 2; } } result.push(dmi); }); return result; } }; var easeOfMovementValue = { name: "EMV", shortName: "EMV", calcParams: [14, 9], figures: [{ key: "emv", title: "EMV: ", type: "line" }, { key: "maEmv", title: "MAEMV: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; let emvValueSum = 0; const emvValueList = []; return dataList.map((kLineData, i) => { const emv = {}; if (i > 0) { const prevKLineData = dataList[i - 1]; const high = kLineData.high; const low = kLineData.low; const volume = kLineData.volume ?? 0; const distanceMoved = (high + low) / 2 - (prevKLineData.high + prevKLineData.low) / 2; if (volume === 0 || high - low === 0) emv.emv = 0; else emv.emv = distanceMoved / (volume / 1e8 / (high - low)); emvValueSum += emv.emv; emvValueList.push(emv.emv); if (i >= params[0]) { emv.maEmv = emvValueSum / params[0]; emvValueSum -= emvValueList[i - params[0]]; } } return emv; }); } }; var exponentialMovingAverage = { name: "EMA", shortName: "EMA", series: "price", calcParams: [ 6, 12, 20 ], precision: 2, shouldOhlc: true, figures: [ { key: "ema1", title: "EMA6: ", type: "line" }, { key: "ema2", title: "EMA12: ", type: "line" }, { key: "ema3", title: "EMA20: ", type: "line" } ], regenerateFigures: (params) => params.map((p, i) => ({ key: `ema${i + 1}`, title: `EMA${p}: `, type: "line" })), calc: (dataList, indicator) => { const { calcParams: params, figures } = indicator; let closeSum = 0; const emaValues = []; return dataList.map((kLineData, i) => { const ema = {}; const close = kLineData.close; closeSum += close; params.forEach((p, index) => { if (i >= p - 1) { if (i > p - 1) emaValues[index] = (2 * close + (p - 1) * emaValues[index]) / (p + 1); else emaValues[index] = closeSum / p; ema[figures[index].key] = emaValues[index]; } }); return ema; }); } }; var momentum = { name: "MTM", shortName: "MTM", calcParams: [12, 6], figures: [{ key: "mtm", title: "MTM: ", type: "line" }, { key: "maMtm", title: "MAMTM: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; let mtmSum = 0; const result = []; dataList.forEach((kLineData, i) => { const mtm = {}; if (i >= params[0]) { mtm.mtm = kLineData.close - dataList[i - params[0]].close; mtmSum += mtm.mtm; if (i >= params[0] + params[1] - 1) { mtm.maMtm = mtmSum / params[1]; mtmSum -= result[i - (params[1] - 1)].mtm ?? 0; } } result.push(mtm); }); return result; } }; var movingAverage = { name: "MA", shortName: "MA", series: "price", calcParams: [ 5, 10, 30, 60 ], precision: 2, shouldOhlc: true, figures: [ { key: "ma1", title: "MA5: ", type: "line" }, { key: "ma2", title: "MA10: ", type: "line" }, { key: "ma3", title: "MA30: ", type: "line" }, { key: "ma4", title: "MA60: ", type: "line" } ], regenerateFigures: (params) => params.map((p, i) => ({ key: `ma${i + 1}`, title: `MA${p}: `, type: "line" })), calc: (dataList, indicator) => { const { calcParams: params, figures } = indicator; const closeSums = []; return dataList.map((kLineData, i) => { const ma = {}; const close = kLineData.close; params.forEach((p, index) => { closeSums[index] = (closeSums[index] ?? 0) + close; if (i >= p - 1) { ma[figures[index].key] = closeSums[index] / p; closeSums[index] -= dataList[i - (p - 1)].close; } }); return ma; }); } }; var movingAverageConvergenceDivergence = { name: "MACD", shortName: "MACD", calcParams: [ 12, 26, 9 ], figures: [ { key: "dif", title: "DIF: ", type: "line" }, { key: "dea", title: "DEA: ", type: "line" }, { key: "macd", title: "MACD: ", type: "bar", baseValue: 0, styles: ({ data, indicator, defaultStyles }) => { const { prev, current } = data; const prevMacd = prev?.macd ?? Number.MIN_SAFE_INTEGER; const currentMacd = current?.macd ?? Number.MIN_SAFE_INTEGER; let color = ""; if (currentMacd > 0) color = formatValue(indicator.styles, "bars[0].upColor", defaultStyles.bars[0].upColor); else if (currentMacd < 0) color = formatValue(indicator.styles, "bars[0].downColor", defaultStyles.bars[0].downColor); else color = formatValue(indicator.styles, "bars[0].noChangeColor", defaultStyles.bars[0].noChangeColor); return { style: prevMacd < currentMacd ? "stroke" : "fill", color, borderColor: color }; } } ], calc: (dataList, indicator) => { const params = indicator.calcParams; let closeSum = 0; let emaShort = 0; let emaLong = 0; let dif = 0; let difSum = 0; let dea = 0; const maxPeriod = Math.max(params[0], params[1]); return dataList.map((kLineData, i) => { const macd = {}; const close = kLineData.close; closeSum += close; if (i >= params[0] - 1) if (i > params[0] - 1) emaShort = (2 * close + (params[0] - 1) * emaShort) / (params[0] + 1); else emaShort = closeSum / params[0]; if (i >= params[1] - 1) if (i > params[1] - 1) emaLong = (2 * close + (params[1] - 1) * emaLong) / (params[1] + 1); else emaLong = closeSum / params[1]; if (i >= maxPeriod - 1) { dif = emaShort - emaLong; macd.dif = dif; difSum += dif; if (i >= maxPeriod + params[2] - 2) { if (i > maxPeriod + params[2] - 2) dea = (dif * 2 + dea * (params[2] - 1)) / (params[2] + 1); else dea = difSum / params[2]; macd.macd = (dif - dea) * 2; macd.dea = dea; } } return macd; }); } }; var onBalanceVolume = { name: "OBV", shortName: "OBV", calcParams: [30], figures: [{ key: "obv", title: "OBV: ", type: "line" }, { key: "maObv", title: "MAOBV: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; let obvSum = 0; let oldObv = 0; const result = []; dataList.forEach((kLineData, i) => { const prevKLineData = dataList[i - 1] ?? kLineData; if (kLineData.close < prevKLineData.close) oldObv -= kLineData.volume ?? 0; else if (kLineData.close > prevKLineData.close) oldObv += kLineData.volume ?? 0; const obv = { obv: oldObv }; obvSum += oldObv; if (i >= params[0] - 1) { obv.maObv = obvSum / params[0]; obvSum -= result[i - (params[0] - 1)].obv ?? 0; } result.push(obv); }); return result; } }; var priceAndVolumeTrend = { name: "PVT", shortName: "PVT", figures: [{ key: "pvt", title: "PVT: ", type: "line" }], calc: (dataList) => { let sum = 0; return dataList.map((kLineData, i) => { const pvt = {}; const close = kLineData.close; const volume = kLineData.volume ?? 1; const prevClose = (dataList[i - 1] ?? kLineData).close; let x = 0; const total = prevClose * volume; if (total !== 0) x = (close - prevClose) / total; sum += x; pvt.pvt = sum; return pvt; }); } }; var psychologicalLine = { name: "PSY", shortName: "PSY", calcParams: [12, 6], figures: [{ key: "psy", title: "PSY: ", type: "line" }, { key: "maPsy", title: "MAPSY: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; let upCount = 0; let psySum = 0; const upList = []; const result = []; dataList.forEach((kLineData, i) => { const psy = {}; const prevClose = (dataList[i - 1] ?? kLineData).close; const upFlag = kLineData.close - prevClose > 0 ? 1 : 0; upList.push(upFlag); upCount += upFlag; if (i >= params[0] - 1) { psy.psy = upCount / params[0] * 100; psySum += psy.psy; if (i >= params[0] + params[1] - 2) { psy.maPsy = psySum / params[1]; psySum -= result[i - (params[1] - 1)].psy ?? 0; } upCount -= upList[i - (params[0] - 1)]; } result.push(psy); }); return result; } }; var rateOfChange = { name: "ROC", shortName: "ROC", calcParams: [12, 6], figures: [{ key: "roc", title: "ROC: ", type: "line" }, { key: "maRoc", title: "MAROC: ", type: "line" }], calc: (dataList, indicator) => { const params = indicator.calcParams; const result = []; let rocSum = 0; dataList.forEach((kLineData, i) => { const roc = {}; if (i >= params[0] - 1) { const close = kLineData.close; const agoClose = (dataList[i - params[0]] ?? dataList[i - (params[0] - 1)]).close; if (agoClose !== 0) roc.roc = (close - agoClose) / agoClose * 100; else roc.roc = 0; rocSum += roc.roc; if (i >= params[0] - 1 + params[1] - 1) { roc.maRoc = rocSum / params[1]; rocSum -= result[i - (params[1] - 1)].roc ?? 0; } } result.push(roc); }); return result; } }; var relativeStrengthIndex = { name: "RSI", shortName: "RSI", calcParams: [ 6, 12, 24 ], figures: [ { key: "rsi1", title: "RSI1: ", type: "line" }, { key: "rsi2", title: "RSI2: ", type: "line" }, { key: "rsi3", title: "RSI3: ", type: "line" } ], regenerateFigures: (params) => params.map((_, index) => { const num = index + 1; return { key: `rsi${num}`, title: `RSI${num}: `, type: "line" }; }), calc: (dataList, indicator) => { const { calcParams: params, figures } = indicator; const sumCloseAs = []; const sumCloseBs = []; return dataList.map((kLineData, i) => { const rsi = {}; const prevClose = (dataList[i - 1] ?? kLineData).close; const tmp = kLineData.close - prevClose; params.forEach((p, index) => { if (tmp > 0) sumCloseAs[index] = (sumCloseAs[index] ?? 0) + tmp; else sumCloseBs[index] = (sumCloseBs[index] ?? 0) + Math.abs(tmp); if (i >= p - 1) { if (sumCloseBs[index] !== 0) rsi[figures[index].key] = 100 - 100 / (1 + sumCloseAs[index] / sumCloseBs[index]); else rsi[figures[index].key] = 0; const agoData = dataList[i - (p - 1)]; const agoPreData = dataList[i - p] ?? agoData; const agoTmp = agoData.close - agoPreData.close; if (agoTmp > 0) sumCloseAs[index] -= agoTmp; else sumCloseBs[index] -= Math.abs(agoTmp); } }); return rsi; }); } }