UNPKG

@amcharts/amcharts5

Version:
460 lines • 16.7 kB
import { __awaiter } from "tslib"; import { Container } from "../../../core/render/Container"; import { LineSeries } from "../../xy/series/LineSeries"; import { BaseColumnSeries } from "../../xy/series/BaseColumnSeries"; import { MultiDisposer } from "../../../core/util/Disposer"; import * as $array from "../../../core/util/Array"; import * as $math from "../../../core/util/Math"; import * as $type from "../../../core/util/Type"; /** * Base class for [[StockChart]] indicators. * * @see {@link https://www.amcharts.com/docs/v5/charts/stock/indicators/} for more info */ export class Indicator extends Container { constructor() { super(...arguments); this._editableSettings = []; this._dataDirty = false; } /** * Clamps numeric settings that carry `minValue`/`maxValue`/`step` limits in * `_editableSettings` back into their allowed range, so out-of-range values * (from the settings modal, deserialized configs, or the API) can never reach * the heavy `prepareData()` — which, for e.g. Volume Profile, would otherwise * freeze the page. A `step` of `1` marks an integer setting, so typed decimals * are rounded. Called with `force` on add/deserialize (nothing is "dirty" * yet); otherwise only dirty keys are checked. */ _clampSettings(force = false) { $array.each(this._editableSettings, (setting) => { if (setting.type != "number") { return; } const key = setting.key; if (!force && !this.isDirty(key)) { return; } let value = this.get(key); if (!$type.isNumber(value)) { return; } let clamped = value; if (setting.step != null && setting.step >= 1) { clamped = Math.round(clamped); } clamped = $math.fitToRange(clamped, setting.minValue != null ? setting.minValue : -Infinity, setting.maxValue != null ? setting.maxValue : Infinity); if (clamped !== value) { // setRaw (not set): writes the value without marking the key dirty, so // clamping here can't re-enter the dirty cycle. (The per-setting // `on(key)` event still fires; that's harmless and the clamp converges.) this.setRaw(key, clamped); } }); } /** * Assigns numeric limits to an editable setting by key. Used for entries that * are inherited from a base indicator (e.g. overbought/oversold) whose range * differs per subclass. */ _setEditableSettingBounds(key, minValue, maxValue, step) { $array.each(this._editableSettings, (setting) => { if (setting.type == "number" && setting.key == key) { setting.minValue = minValue; setting.maxValue = maxValue; setting.step = step; } }); } _prepareChildren() { super._prepareChildren(); this._clampSettings(); if (this.isDirty("stockSeries") || this.isDirty("volumeSeries")) { this.markDataDirty(); const stockSeries = this.get("stockSeries"); const previousS = this._prevSettings.stockSeries; if (previousS && this._sDP) { this._sDP.dispose(); } if (stockSeries) { this._sDP = new MultiDisposer([ stockSeries.events.on("datavalidated", () => { this._markDataDirty(); }), stockSeries.events.on("datasetchanged", () => { this._markDataDirty(); }) ]); } const previousV = this._prevSettings.volumeSeries; if (previousV && this._vDP) { this._vDP.dispose(); } const volumeSeries = this.get("volumeSeries"); if (volumeSeries) { this._vDP = new MultiDisposer([ volumeSeries.events.on("datavalidated", () => { this._markDataDirty(); }), volumeSeries.events.on("datasetchanged", () => { this._markDataDirty(); }) ]); } } if (this.isDirty("field")) { if (this.get("field")) { this.markDataDirty(); } } if (this.isDirty("period")) { this.markDataDirty(); this.setCustomData("period", this.get("period")); } if (this._dataDirty) { this.prepareData(); this._dataDirty = false; } } _markDataDirty() { this._dataDirty = true; this.markDirty(); } markDataDirty() { this._root.events.once("frameended", () => { this._markDataDirty(); }); } _updateChildren() { super._updateChildren(); if (this.isDirty("seriesColor")) { this._updateSeriesColor(this.series, this.get("seriesColor"), "seriesColor"); } this.setCustomData("period", this.get("period")); this.setCustomData("field", this.get("field")); this.setCustomData("name", this.get("name")); this.setCustomData("shortName", this.get("shortName")); } _dispose() { super._dispose(); if (this._sDP) { this._sDP.dispose(); } if (this._vDP) { this._vDP.dispose(); } const series = this.series; if (series) { series.dispose(); const yAxis = series.get("yAxis"); if (yAxis) { yAxis.markDirtySelectionExtremes(); } } const stockChart = this.get("stockChart"); if (stockChart) { const legend = this.get("legend"); if (legend) { const legendDataItem = series.get("legendDataItem"); if (legendDataItem) { legend.disposeDataItem(legendDataItem); } } stockChart.indicators.removeValue(this); } } hide(duration) { const _super = Object.create(null, { hide: { get: () => super.hide } }); return __awaiter(this, void 0, void 0, function* () { return Promise.all([ _super.hide.call(this, duration), this.series.hide(duration) ]); }); } show(duration) { const _super = Object.create(null, { show: { get: () => super.show } }); return __awaiter(this, void 0, void 0, function* () { return Promise.all([ _super.show.call(this, duration), this.series.show(duration) ]); }); } _handleLegend(series) { const legend = this.get("legend"); if (legend) { legend.data.push(series); const legendDataItem = legend.dataItems[legend.dataItems.length - 1]; legendDataItem.get("marker").set("forceHidden", true); const closeButton = legendDataItem.get("closeButton"); closeButton.set("forceHidden", false); closeButton.events.on("click", () => { this.dispose(); }); const settingsButton = legendDataItem.get("settingsButton"); settingsButton.setPrivate("customData", this); const editableSettings = this._editableSettings; if (!editableSettings || editableSettings.length == 0) { settingsButton.set("forceHidden", true); } } } _updateSeriesColor(series, color, contextName) { if (series) { series.set("stroke", color); series.set("fill", color); if (series instanceof LineSeries) { series.strokes.template.set("stroke", color); series.fills.template.set("fill", color); } if (series instanceof BaseColumnSeries) { series.columns.template.setAll({ stroke: color, fill: color }); } if (contextName && color) { this.setCustomData(contextName, color.toCSSHex()); } } } setCustomData(name, value) { const customData = this.series.getPrivate("customData"); if (customData) { customData[name] = value; } } /** * @ignore */ prepareData() { } _getValue(dataItem) { const field = this.get("field"); let o = dataItem.get("openValueY"); let h = dataItem.get("highValueY"); let l = dataItem.get("lowValueY"); let c = dataItem.get("valueY"); switch (field) { case "close": return c; break; case "open": return o; break; case "high": return h; break; case "low": return l; break; case "hl/2": return (h + l) / 2; break; case "hlc/3": return (h + l + c) / 3; break; case "hlcc/4": return (h + l + c + c) / 4; break; case "ohlc/4": return (o + h + l + c) / 4; break; } } /** * @ignore */ _getDataArray(dataItems) { const data = []; $array.each(dataItems, (dataItem) => { data.push({ valueX: dataItem.get("valueX"), value_y: this._getValue(dataItem) }); }); return data; } /** * @ignore */ _getTypicalPrice(dataItems) { const data = []; $array.each(dataItems, (dataItem) => { data.push({ valueX: dataItem.get("valueX"), value_y: (dataItem.get("valueY", 0) + dataItem.get("highValueY", 0) + dataItem.get("lowValueY", 0)) / 3 }); }); return data; } _sma(data, period, field, toField) { if (!data || data.length === 0 || period <= 0) { return; } const invPeriod = 1 / period; const len = data.length; let count = 0; // number of non-null values encountered let sum = 0; // sum of values in the current window for (let i = 0; i < len; i++) { const dataItem = data[i]; const value = dataItem[field]; if (value != null) { count++; sum += value; // remove value that goes out of window (by index, to preserve original behavior) if (count > period) { const valueToRemove = data[i - period][field]; if (valueToRemove != null) { sum -= valueToRemove; } } if (count >= period) { dataItem[toField] = sum * invPeriod; } } } } _wma(data, period, field, toField) { const denom = period * (period + 1) / 2; const buffer = new Array(period); let start = 0; let len = 0; let sum = 0; // sum of values in the window let weighted = 0; // weighted sum (1*v1 + 2*v2 + ... + p*vp) for (let i = 0; i < data.length; i++) { const dataItem = data[i]; const value = dataItem[field]; if (value == null) { continue; } if (len < period) { const pos = (start + len) % period; buffer[pos] = value; len++; sum += value; weighted += len * value; if (len === period) { dataItem[toField] = weighted / denom; } } else { const oldest = buffer[start]; weighted = weighted - sum + period * value; sum = sum - oldest + value; buffer[start] = value; start = (start + 1) % period; dataItem[toField] = weighted / denom; } } } _ema(data, period, field, toField) { const len = data.length; if (len === 0 || period <= 0) { return; } const invPeriod = 1 / period; const multiplier = 2 / (1 + period); const oneMinus = 1 - multiplier; let ma = 0; let count = 0; for (let i = 0; i < len; i++) { const dataItem = data[i]; const value = dataItem[field]; if (value == null) { continue; } count++; if (count > period) { ma = value * multiplier + ma * oneMinus; dataItem[toField] = ma; } else { ma += value * invPeriod; if (count === period) { dataItem[toField] = ma; } } } } _dema(data, period, field, toField) { if (period <= 0 || data.length === 0) { return; } const multiplier = 2 / (1 + period); const oneMinus = 1 - multiplier; const invPeriod = 1 / period; let ema = 0; let emaCount = 0; let ema2 = 0; let ema2Count = 0; for (let i = 0, len = data.length; i < len; i++) { const dataItem = data[i]; const value = dataItem[field]; if (value == null) { continue; } // compute EMA (same logic as _ema) emaCount++; if (emaCount > period) { ema = value * multiplier + ema * oneMinus; dataItem.ema = ema; } else { ema += value * invPeriod; if (emaCount === period) { dataItem.ema = ema; } } // if EMA was produced for this item, update EMA of EMA (ema2) and DEMA const emaVal = dataItem.ema; if (emaVal != null) { ema2Count++; if (ema2Count > period) { ema2 = emaVal * multiplier + ema2 * oneMinus; dataItem[toField] = 2 * emaVal - ema2; dataItem.ema2 = ema2; } else { ema2 += emaVal / period; if (ema2Count === period) { dataItem[toField] = 2 * emaVal - ema2; dataItem.ema2 = ema2; } } } } } _tema(data, period, field, toField) { if (period <= 0 || data.length === 0) { return; } const multiplier = 2 / (1 + period); const oneMinus = 1 - multiplier; // Ensure ema and ema2 are present this._dema(data, period, field, "dema"); let ema3 = 0; let count = 0; for (let i = 0, len = data.length; i < len; i++) { const dataItem = data[i]; const ema = dataItem.ema; const ema2 = dataItem.ema2; if (ema2 == null) { continue; } count++; if (count > period) { ema3 = ema2 * multiplier + ema3 * oneMinus; // guard ema just in case (should be present when ema2 is) const emaVal = (ema == null) ? 0 : ema; dataItem[toField] = 3 * emaVal - 3 * ema2 + ema3; } else { ema3 += ema2 / period; if (count === period) { const emaVal = (ema == null) ? 0 : ema; dataItem[toField] = 3 * emaVal - 3 * ema2 + ema3; } } } } } Indicator.className = "Indicator"; Indicator.classNames = Container.classNames.concat([Indicator.className]); //# sourceMappingURL=Indicator.js.map