UNPKG

@azteam/candlestick-chart

Version:

N/A

777 lines (668 loc) 30.1 kB
import _ from 'lodash'; import {getPercentDifference} from '@azteam/util'; import {BB_DEVIATION, BB_POSITION, CANDLE_COLOR, CHART_SIZE, TIME_CHART, TREND, TZ_OFFSET_MAP} from './constant.js'; function roundDown(ts, period, offset = 25200) { return Math.floor((ts + offset) / period) * period - offset; } class CandlestickChart { constructor({timezone = 'UTC', pricePrecision = 4, symbol} = {}) { this.symbol = symbol; this.timezone = timezone; this.tzOffset = TZ_OFFSET_MAP[timezone] || 0; this.isInitChart = false; this.rolling = {}; this.chart = Object.values(TIME_CHART).reduce((result, value) => { result[value.name] = []; return result; }, {}); this.pricePrecision = pricePrecision; } init() { this.isInitChart = true; } convertTickId(tick1minId) { const ts = tick1minId; const off = this.tzOffset; return { tick1minId: roundDown(ts, 60, off), tick5minId: roundDown(ts, 300, off), tick15minId: roundDown(ts, 900, off), tick30minId: roundDown(ts, 1800, off), tick60minId: roundDown(ts, 3600, off), tick4hourId: roundDown(ts, 14400, off), tick1dayId: roundDown(ts, 86400, off), }; } getTicks(position = 0) { const ticks = {}; const tfNames = ['1min', '5min', '15min', '30min', '60min', '4hour', '1day']; const baseArr = this.chart['1min']; const baseTick = baseArr[position]; if (!baseTick) return ticks; const setTicksLayer = (arr, idx, key) => { ticks[`tick${key}`] = arr[idx] || null; ticks[`prevTick${key}`] = arr[idx + 1] || null; ticks[`prev2Tick${key}`] = arr[idx + 2] || null; ticks[`prev3Tick${key}`] = arr[idx + 3] || null; }; setTicksLayer(baseArr, position, '1min'); const ids = this.convertTickId(baseTick.id); const idMap = { '5min': ids.tick5minId, '15min': ids.tick15minId, '30min': ids.tick30minId, '60min': ids.tick60minId, '4hour': ids.tick4hourId, '1day': ids.tick1dayId, }; for (let i = 1; i < tfNames.length; i++) { const key = tfNames[i]; const arr = this.chart[key]; if (!arr || arr.length === 0) continue; const targetId = idMap[key]; let idx = -1; if (arr[0].id === targetId) idx = 0; else if (arr[1] && arr[1].id === targetId) idx = 1; else idx = arr.findIndex((x) => x.id === targetId); if (idx !== -1) setTicksLayer(arr, idx, key); } return ticks; } getChart() { return this.chart; } detectHighVolatility(minutes, tf = '1min') { const count = parseInt(minutes, 10); if (!count || isNaN(count)) { return {min_size: 0, max_size: 0, avg_size: 0, highest_price: 0, lowest_price: 0}; } const baseArr = this.chart[tf]; if (!baseArr || baseArr.length === 0) { return {min_size: 0, max_size: 0, avg_size: 0, highest_price: 0, lowest_price: 0}; } const limit = Math.min(baseArr.length, count); let minSize = null; let maxSize = null; let sumSize = 0; let validCount = 0; let minPrice = null; let maxPrice = null; for (let i = 0; i < limit; i++) { const candle = baseArr[i]; const size = candle.candle_size; if (size != null) { if (minSize === null || size < minSize) minSize = size; if (maxSize === null || size > maxSize) maxSize = size; sumSize += size; validCount++; } if (minPrice === null || candle.low < minPrice) minPrice = candle.low; if (maxPrice === null || candle.high > maxPrice) maxPrice = candle.high; } return { min_size: minSize !== null ? _.round(minSize, 4) : 0, max_size: maxSize !== null ? _.round(maxSize, 4) : 0, avg_size: validCount ? _.round(sumSize / validCount, 4) : 0, highest_price: maxPrice || 0, lowest_price: minPrice || 0, }; } predictMA5CrossMA10(tf = '1min') { const arr = this.chart[tf]; if (!arr || arr.length < 19) return null; const currentCandle = arr[0]; // Calculate Sum(C4..C8) let sum4to8 = 0; for (let i = 4; i <= 8; i++) { sum4to8 += arr[i].close; } // Calculate Sum(C0..C3) let sum0to3 = 0; for (let i = 0; i <= 3; i++) { sum0to3 += arr[i].close; } // Formula: C_next = Sum(C4..C8) - Sum(C0..C3) const nextPrice = sum4to8 - sum0to3; // Calculate SMA20 next to determine full straight let sum0to18 = 0; for (let i = 0; i <= 18; i++) { sum0to18 += arr[i].close; } const sma20Next = (nextPrice + sum0to18) / 20; const sma5Next = (nextPrice + sum0to3) / 5; // Determine new straight direction const isCrossingDown = currentCandle.close_sma_5 > currentCandle.close_sma_10; // Slightly adjust to resolve tie in sorting exactly at cross point const simulatedSma5 = isCrossingDown ? sma5Next - 0.000001 : sma5Next + 0.000001; const simulatedSma10 = sma5Next; const simulatedSma20 = sma20Next; const smaList = [ {key: 5, value: simulatedSma5}, {key: 10, value: simulatedSma10}, {key: 20, value: simulatedSma20}, ]; smaList.sort((a, b) => b.value - a.value); const direction = smaList.map((obj) => obj.key).join('_'); const currentPrice = currentCandle.close; const percent = _.round(((nextPrice - currentPrice) / currentPrice) * 100, 4); return { next_price: _.round(nextPrice, this.pricePrecision), percent: percent, direction: direction }; } predictMA10CrossMA20(tf = '1min') { const arr = this.chart[tf]; if (!arr || arr.length < 19) return null; const currentCandle = arr[0]; // Calculate Sum(C9..C18) let sum9to18 = 0; for (let i = 9; i <= 18; i++) { sum9to18 += arr[i].close; } // Calculate Sum(C0..C8) let sum0to8 = 0; for (let i = 0; i <= 8; i++) { sum0to8 += arr[i].close; } // Formula: C_next = Sum(C9..C18) - Sum(C0..C8) const nextPrice = sum9to18 - sum0to8; // Need Sum(C0..C3) to calculate SMA5 let sum0to3 = 0; for (let i = 0; i <= 3; i++) { sum0to3 += arr[i].close; } const sma10Next = (nextPrice + sum0to8) / 10; const sma5Next = (nextPrice + sum0to3) / 5; const sma20Next = sma10Next; const isCrossingDown = currentCandle.close_sma_10 > currentCandle.close_sma_20; const simulatedSma10 = isCrossingDown ? sma10Next - 0.000001 : sma10Next + 0.000001; const simulatedSma5 = sma5Next; const simulatedSma20 = sma20Next; const smaList = [ {key: 5, value: simulatedSma5}, {key: 10, value: simulatedSma10}, {key: 20, value: simulatedSma20}, ]; smaList.sort((a, b) => b.value - a.value); const direction = smaList.map((obj) => obj.key).join('_'); const currentPrice = currentCandle.close; const percent = _.round(((nextPrice - currentPrice) / currentPrice) * 100, 4); return { next_price: _.round(nextPrice, this.pricePrecision), percent: percent, direction: direction }; } predictMA5CrossMA20(tf = '1min') { const arr = this.chart[tf]; if (!arr || arr.length < 19) return null; const currentCandle = arr[0]; // Calculate Sum(C4..C18) let sum4to18 = 0; for (let i = 4; i <= 18; i++) { sum4to18 += arr[i].close; } // Calculate Sum(C0..C3) let sum0to3 = 0; for (let i = 0; i <= 3; i++) { sum0to3 += arr[i].close; } // Formula: C_next = (Sum(C4..C18) - 3 * Sum(C0..C3)) / 3 const nextPrice = (sum4to18 - 3 * sum0to3) / 3; // Need Sum(C0..C8) to calculate SMA10 let sum0to8 = 0; for (let i = 0; i <= 8; i++) { sum0to8 += arr[i].close; } const sma5Next = (nextPrice + sum0to3) / 5; const sma10Next = (nextPrice + sum0to8) / 10; const sma20Next = sma5Next; const isCrossingDown = currentCandle.close_sma_5 > currentCandle.close_sma_20; const simulatedSma5 = isCrossingDown ? sma5Next - 0.000001 : sma5Next + 0.000001; const simulatedSma10 = sma10Next; const simulatedSma20 = sma20Next; const smaList = [ {key: 5, value: simulatedSma5}, {key: 10, value: simulatedSma10}, {key: 20, value: simulatedSma20}, ]; smaList.sort((a, b) => b.value - a.value); const direction = smaList.map((obj) => obj.key).join('_'); const currentPrice = currentCandle.close; const percent = _.round(((nextPrice - currentPrice) / currentPrice) * 100, 4); return { next_price: _.round(nextPrice, this.pricePrecision), percent: percent, direction: direction }; } calculateTickMA(data, index, timeChart) { this._calculateBasicSmaAndMinMax(data, index, timeChart); this._calculateTrendAndStraight(data, index); this._calculateAboveBelowCounters(data, index); this._calculateEMA(data, index, 34, 'ema34'); this._calculateEMA(data, index, 89, 'ema89'); this._calculateRSI(data, index, 14); this._calculateMACD(data, index); this._calculateATR(data, index, 14); } _calculateBasicSmaAndMinMax(data, index, timeChart) { const tick = data[index]; const close = tick.close; const low = tick.low; const high = tick.high; if (!this.rolling[timeChart]) { this.rolling[timeChart] = {count: 0, sum20: 0, sum10: 0, sum5: 0, sqSum20: 0}; } const roll = this.rolling[timeChart]; if (roll.count % 100 === 0 || roll.sum20 === 0 || index !== 0 || data.length <= index + 20) { // console.log(`[DEBUG] Resetting SMA/BB for ${timeChart} at index ${index}, length ${data.length}`); let s20 = 0, s10 = 0, s5 = 0, sq20 = 0; let mx20 = high, mx10 = high, mx5 = high; let mn20 = low, mn10 = low, mn5 = low; const limit = Math.min(data.length, index + 20); for (let i = index; i < limit; i++) { const t = data[i], v = t.close, h = t.high, l = t.low; s20 += v; sq20 += v * v; if (h > mx20) mx20 = h; if (l < mn20) mn20 = l; if (i < index + 10) { s10 += v; if (h > mx10) mx10 = h; if (l < mn10) mn10 = l; if (i < index + 5) { s5 += v; if (h > mx5) mx5 = h; if (l < mn5) mn5 = l; } } } if (data.length >= index + 20) { roll.sum20 = s20; roll.sum10 = s10; roll.sum5 = s5; roll.sqSum20 = sq20; } tick.max20 = mx20; tick.max10 = mx10; tick.max5 = mx5; tick.min20 = mn20; tick.min10 = mn10; tick.min5 = mn5; if (data.length <= index + 20) return; } else { const displaced20 = data[index + 20], displaced10 = data[index + 10], displaced5 = data[index + 5]; if (tick.isNewCandle) { roll.sum20 += close - (displaced20 ? displaced20.close : 0); roll.sum10 += close - (displaced10 ? displaced10.close : 0); roll.sum5 += close - (displaced5 ? displaced5.close : 0); roll.sqSum20 += close * close - (displaced20 ? displaced20.close * displaced20.close : 0); roll.count++; } else { const prevClose = tick.prevClose || close; const diff = close - prevClose; roll.sum20 += diff; roll.sum10 += diff; roll.sum5 += diff; roll.sqSum20 += close * close - prevClose * prevClose; } let mx20 = high, mx10 = high, mx5 = high; let mn20 = low, mn10 = low, mn5 = low; const limit = Math.min(data.length, index + 20); for (let i = index; i < limit; i++) { const t = data[i]; if (t.high > mx20) mx20 = t.high; if (t.low < mn20) mn20 = t.low; if (i < index + 10) { if (t.high > mx10) mx10 = t.high; if (t.low < mn10) mn10 = t.low; if (i < index + 5) { if (t.high > mx5) mx5 = t.high; if (t.low < mn5) mn5 = t.low; } } } tick.max20 = mx20; tick.max10 = mx10; tick.max5 = mx5; tick.min20 = mn20; tick.min10 = mn10; tick.min5 = mn5; } const prec = this.pricePrecision; tick.bb_sma = _.round(roll.sum20 / 20, prec); tick.close_sma_5 = _.round(roll.sum5 / 5, prec); tick.close_sma_10 = _.round(roll.sum10 / 10, prec); tick.close_sma_20 = tick.bb_sma; tick.prevClose = close; delete tick.isNewCandle; } _calculateTrendAndStraight(data, index) { const current = data[index]; const prev2 = data[index + 2], prev1 = data[index + 1]; if (!prev2 || current.close_sma_5 === undefined) return; const determineTrend = (p2, p1, cur) => { if (p2 < p1 && p1 < cur) return TREND.LONG; if (p2 > p1 && p1 > cur) return TREND.SHORT; return TREND.SIDEWAY; }; current.trend5 = determineTrend(prev2.close_sma_5, prev1.close_sma_5, current.close_sma_5); current.trend10 = determineTrend(prev2.close_sma_10, prev1.close_sma_10, current.close_sma_10); current.trend20 = determineTrend(prev2.close_sma_20, prev1.close_sma_20, current.close_sma_20); const smaList = [ {key: 5, value: current.close_sma_5}, {key: 10, value: current.close_sma_10}, {key: 20, value: current.close_sma_20}, ]; smaList.sort((a, b) => b.value - a.value); current.straight = smaList.map((obj) => obj.key).join('_'); } _calculateAboveBelowCounters(data, index) { if (data.length <= index + BB_DEVIATION) return; const current = data[index]; current.above_sma5 = current.above_sma10 = current.above_sma20 = 0; current.below_sma5 = current.below_sma10 = current.below_sma20 = 0; let flags = {a5: true, a10: true, a20: true, b5: true, b10: true, b20: true}; for (let i = index + 1; i < BB_DEVIATION + index; i += 1) { const tick = data[i]; if (flags.a5) tick.low > tick.close_sma_5 ? current.above_sma5++ : (flags.a5 = false); if (flags.b5) tick.high < tick.close_sma_5 ? current.below_sma5++ : (flags.b5 = false); if (flags.a10) tick.low > tick.close_sma_10 ? current.above_sma10++ : (flags.a10 = false); if (flags.b10) tick.high < tick.close_sma_10 ? current.below_sma10++ : (flags.b10 = false); if (flags.a20) tick.low > tick.close_sma_20 ? current.above_sma20++ : (flags.a20 = false); if (flags.b20) tick.high < tick.close_sma_20 ? current.below_sma20++ : (flags.b20 = false); if (!flags.a5 && !flags.a10 && !flags.a20 && !flags.b5 && !flags.b10 && !flags.b20) break; } } calculateProfitAndTimeTick(data, index) { const tick = data[index]; tick.profit = _.round(tick.close - tick.open, 2); tick.profit_percent = getPercentDifference(tick.close, tick.open); tick.max_profit_percent = getPercentDifference(tick.high, tick.open); tick.max_loss_percent = getPercentDifference(tick.low, tick.open); tick.candle_size = getPercentDifference(tick.high, tick.low); tick.lower_wick_percent = getPercentDifference(tick.close, tick.low); tick.upper_wick_percent = getPercentDifference(tick.high, tick.close); tick.center_price = (tick.high + tick.low) / 2; tick.color = tick.open < tick.close ? CANDLE_COLOR.GREEN : CANDLE_COLOR.RED; } calculateBollingerBandsForTick(data, index, timeChart) { if (data.length < index + BB_DEVIATION) return; this._calculateBasicSmaAndMinMax(data, index, timeChart); if (data.length < index + BB_DEVIATION) return; const tick = data[index], roll = this.rolling[timeChart]; const n = BB_DEVIATION, mean = roll.sum20 / n; const variance = roll.sqSum20 / n - mean * mean; const standardDeviation = Math.sqrt(Math.max(0, variance)); tick.bb_upper_band = _.round(tick.bb_sma + 2 * standardDeviation, this.pricePrecision); tick.bb_lower_band = _.round(tick.bb_sma - 2 * standardDeviation, this.pricePrecision); tick.bb_size = getPercentDifference(tick.bb_upper_band, tick.bb_lower_band); tick.bb_width = _.round((tick.bb_upper_band - tick.bb_lower_band) / tick.bb_sma, 4); tick.bb_percent = _.round((tick.close - tick.bb_lower_band) / (tick.bb_upper_band - tick.bb_lower_band), 4) || 0; tick.bb_width_percentile = this.calculatePercentile(data, index, 'bb_width', 50); tick.bb_size_percentile = this.calculatePercentile(data, index, 'bb_size', 50); tick.bb_percent_percentile = this.calculatePercentile(data, index, 'bb_percent', 50); tick.pos = BB_POSITION.MIDDLE; if (tick.bb_size > 5) { const {bb_upper_band, bb_lower_band, bb_sma, close} = tick; if (close > bb_upper_band) tick.pos = BB_POSITION.OUT_TOP; else if (close < bb_lower_band) tick.pos = BB_POSITION.OUT_BOTTOM; else if (close > (bb_upper_band + bb_sma) / 2) tick.pos = BB_POSITION.TOP; else if (close < (bb_lower_band + bb_sma) / 2) tick.pos = BB_POSITION.BOTTOM; else if (close > bb_sma) tick.pos = BB_POSITION.UPPER_MIDDLE; else if (close < bb_sma) tick.pos = BB_POSITION.LOWER_MIDDLE; } tick.time_at = tick.tick_id = tick.id; tick.is_bb_tick = true; } calculatePercentile(data, index, field, lookback = 50) { if (data.length <= index + lookback) return null; const current = data[index][field]; if (current == null) return null; let smaller = 0, count = 0; for (let i = index + 1; i <= index + lookback; i++) { const val = data[i]?.[field]; if (val != null) { if (val <= current) smaller++; count++; } } return count ? Math.round((smaller / count) * 100) : null; } _calculateEMA(data, index, period, key) { const tick = data[index], prev = data[index + 1]; if (data.length <= index + period) return; const multiplier = 2 / (period + 1); if (prev && prev[key] !== undefined) { tick[key] = _.round((tick.close - prev[key]) * multiplier + prev[key], this.pricePrecision); } else { let sum = 0; for (let i = index; i < index + period; i++) sum += data[i].close; tick[key] = _.round(sum / period, this.pricePrecision); } } _calculateRSI(data, index, period) { const tick = data[index], prev = data[index + 1]; if (data.length <= index + period + 2) return; const calculateInitialAvg = () => { let totalGain = 0, totalLoss = 0; for (let i = index + period; i > index; i--) { const diff = data[i - 1].close - data[i].close; if (diff >= 0) totalGain += diff; else totalLoss -= diff; } return {avgGain: totalGain / period, avgLoss: totalLoss / period}; }; if (prev && prev.rsi_avg_gain !== undefined) { const diff = tick.close - prev.close; const currentGain = diff >= 0 ? diff : 0, currentLoss = diff < 0 ? -diff : 0; tick.rsi_avg_gain = (prev.rsi_avg_gain * (period - 1) + currentGain) / period; tick.rsi_avg_loss = (prev.rsi_avg_loss * (period - 1) + currentLoss) / period; } else { const {avgGain, avgLoss} = calculateInitialAvg(); tick.rsi_avg_gain = avgGain; tick.rsi_avg_loss = avgLoss; } if (tick.rsi_avg_loss === 0) tick.rsi = 100; else { const rs = tick.rsi_avg_gain / tick.rsi_avg_loss; tick.rsi = _.round(100 - 100 / (1 + rs), 2); } } _calculateMACD(data, index) { const period = 9, tick = data[index], prev = data[index + 1]; if (data.length <= index + 26 + period) return; this._calculateEMA(data, index, 12, 'ema12'); this._calculateEMA(data, index, 26, 'ema26'); if (tick.ema12 === undefined || tick.ema26 === undefined) return; tick.macd_line = _.round(tick.ema12 - tick.ema26, this.pricePrecision); const multiplier = 2 / (period + 1); if (prev && prev.macd_signal !== undefined) { tick.macd_signal = _.round((tick.macd_line - prev.macd_signal) * multiplier + prev.macd_signal, this.pricePrecision); } else { let sum = 0; for (let i = index; i < index + period; i++) { if (data[i].macd_line === undefined) { this._calculateEMA(data, i, 12, 'ema12'); this._calculateEMA(data, i, 26, 'ema26'); data[i].macd_line = _.round(data[i].ema12 - data[i].ema26, this.pricePrecision); } sum += data[i].macd_line; } tick.macd_signal = _.round(sum / period, this.pricePrecision); } if (tick.macd_signal !== undefined) tick.macd_hist = _.round(tick.macd_line - tick.macd_signal, this.pricePrecision); } _calculateATR(data, index, period) { const tick = data[index], prev = data[index + 1]; if (data.length <= index + period + 1) return; // True Range = max(high - low, |high - prevClose|, |low - prevClose|) const prevClose = prev ? prev.close : tick.open; tick.tr = Math.max(tick.high - tick.low, Math.abs(tick.high - prevClose), Math.abs(tick.low - prevClose)); if (prev && prev.atr !== undefined) { // Wilder's smoothing: ATR = (prevATR * (period - 1) + TR) / period tick.atr = _.round((prev.atr * (period - 1) + tick.tr) / period, this.pricePrecision); } else { // Seed: SMA of first N true ranges let sumTR = tick.tr; for (let i = index + 1; i < index + period; i++) { const t = data[i], p = data[i + 1]; const pc = p ? p.close : t.open; sumTR += Math.max(t.high - t.low, Math.abs(t.high - pc), Math.abs(t.low - pc)); } tick.atr = _.round(sumTR / period, this.pricePrecision); } // ATR as percentage of close price tick.atr_percent = _.round((tick.atr / tick.close) * 100, 4); } sumVolume(ts, timeChart) { const blockTimes = (ts, timeChart) => { const configMap = { [TIME_CHART._1_DAY.name]: {period: 14400, steps: [0, 1, 2, 3, 4, 5]}, [TIME_CHART._4_HOUR.name]: {period: 3600, steps: [0, 1, 2, 3]}, [TIME_CHART._60_MIN.name]: {period: 1800, steps: [0, 1]}, [TIME_CHART._30_MIN.name]: {period: 900, steps: [0, 1]}, [TIME_CHART._15_MIN.name]: {period: 300, steps: [0, 1, 2]}, [TIME_CHART._5_MIN.name]: {period: 60, steps: [0, 1, 2, 3, 4]}, }; const config = configMap[timeChart]; return config ? config.steps.map((x) => ts + config.period * x) : [ts]; }; const blocks = blockTimes(ts, timeChart); const minTs = blocks[0], maxTs = blocks[blocks.length - 1]; const tfMap = { [TIME_CHART._1_DAY.name]: '4hour', [TIME_CHART._4_HOUR.name]: '60min', [TIME_CHART._60_MIN.name]: '30min', [TIME_CHART._30_MIN.name]: '15min', [TIME_CHART._15_MIN.name]: '5min', [TIME_CHART._5_MIN.name]: '1min', }; const chartArr = this.chart[tfMap[timeChart]]; if (!chartArr) return {vol: 0, amount: 0, taker_vol: 0, taker_amount: 0, count: 0}; let v = 0, a = 0, tv = 0, ta = 0, c = 0; for (let i = 0; i < chartArr.length; i++) { const t = chartArr[i], tId = t.time_at ?? t.id; if (tId < minTs) break; if (tId <= maxTs && blocks.includes(tId)) { v += t.vol; a += t.amount; tv += t.taker_vol; ta += t.taker_amount; c += t.count; } } return {vol: v, amount: a, taker_vol: tv, taker_amount: ta, count: c}; } addTickToChart(tick, timeChart = '1min', isInitialized = false, momentObj = null, tickIds = null) { const chart = this.chart[timeChart]; const ids = tickIds || this.convertTickId(tick.id); const tickId = ids[`tick${timeChart}Id` || `tick${timeChart.toUpperCase()}Id` || 0]; const tc = Object.values(TIME_CHART).find((x) => x.name === timeChart); if (chart.length === 0) { chart.unshift({...tick, id: tickId, chart_type: timeChart}); this.convertBBFirstTick(chart, timeChart); } else if (chart[0].id === tickId) { this._updateExistingTick(chart[0], tick, timeChart, tickId); } else if (tickId - chart[0].id === tc.period) { tick.isNewCandle = true; chart.unshift({...tick, id: tickId, chart_type: timeChart}); this.convertBBFirstTick(chart, timeChart); } else { this._validateTickContinuity(chart, tickId, timeChart, tc.period); } if (chart.length > (tc.size || CHART_SIZE)) chart.pop(); this.calculateProfitAndTimeTick(chart, 0); this.calculateBollingerBandsForTick(chart, 0, timeChart); this._calculateTrendAndStraight(chart, 0); this._calculateAboveBelowCounters(chart, 0); this._calculateEMA(chart, 0, 34, 'ema34'); this._calculateEMA(chart, 0, 89, 'ema89'); this._calculateRSI(chart, 0, 14); this._calculateMACD(chart, 0); this._calculateATR(chart, 0, 14); if (timeChart === '1min' && !isInitialized) this._updateSubTicks(chart[0]); return chart[0]; } _updateSubTicks(candle1min) { const ticks = this.getTicks(); candle1min.ticks = { tick5min: ticks.tick5min ? {...ticks.tick5min} : null, tick15min: ticks.tick15min ? {...ticks.tick15min} : null, tick30min: ticks.tick30min ? {...ticks.tick30min} : null, tick60min: ticks.tick60min ? {...ticks.tick60min} : null, tick4hour: ticks.tick4hour ? {...ticks.tick4hour} : null, tick1day: ticks.tick1day ? {...ticks.tick1day} : null, }; } _updateExistingTick(existingTick, tick, timeChart, tickId) { if (timeChart === '1min') existingTick.open = tick.open; existingTick.close = tick.close; if (existingTick.high < tick.high) existingTick.high = tick.high; if (existingTick.low > tick.low) existingTick.low = tick.low; if (timeChart !== '1min') { const aggregated = this.sumVolume(tickId, timeChart); if (aggregated.vol > 0) { Object.assign(existingTick, aggregated); } else { existingTick.vol += tick.vol; existingTick.amount += tick.amount; existingTick.taker_vol += tick.taker_vol; existingTick.taker_amount += tick.taker_amount; existingTick.count += tick.count; } } } _validateTickContinuity(chart, tickId, timeChart, period) { if (!chart.some((t) => t.id === tickId)) { if (timeChart === '1min' && Math.abs(tickId - chart[0].id) !== period) { throw new Error(`Add wrong tick ts [${timeChart}, ${tickId}, ${chart[0].id}]`); } } } convertBBFirstTick(chart, timeChart) { this.calculateProfitAndTimeTick(chart, 0); this.calculateBollingerBandsForTick(chart, 0, timeChart); } addTickToAllChart(tick) { const ids = this.convertTickId(tick.id); Object.values(TIME_CHART).forEach((tc) => this.addTickToChart(tick, tc.name, false, null, ids)); } } export default CandlestickChart;