@azteam/candlestick-chart
Version:
N/A
777 lines (668 loc) • 30.1 kB
JavaScript
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;