UNPKG

@azteam/candlestick-chart

Version:

N/A

905 lines (765 loc) 35.5 kB
import _ from 'lodash'; import moment from 'moment-timezone'; import {decreasePercent, getPercentDifference, increasePercent} from '@azteam/util'; import {BB_DEVIATION, BB_POSITION, CANDLE_COLOR, CHART_SIZE, TIME_CHART, TREND} from './constant'; import { generateTickBBPercentRange, generateTickCandleSizeRange, generateTickColor, generateTickCompare, generateTickProfitPercentRange, generateTickSMACompare51020, generateTickSMACompareBB, } from './util'; function roundDown(m, downTo, timeType) { let newMoment = m; if (downTo > 1) { let diff = 0; if (['day', 'days'].includes(timeType)) { diff = newMoment.date() % downTo; } else if (['hour', 'hours'].includes(timeType)) { diff = newMoment.hours() % downTo; } else if (['minute', 'minutes'].includes(timeType)) { diff = newMoment.minutes() % downTo; } else if (['second', 'seconds'].includes(timeType)) { diff = newMoment.seconds() % downTo; } newMoment = newMoment.subtract(diff, timeType); } return newMoment.startOf(timeType); } function groupBlockTimeByTimeChart(ts, timeChart) { if (timeChart === TIME_CHART._1_DAY.name) { const period = 14400; return [0, 1, 2, 3, 4, 5].map((x) => ts + period * x); } if (timeChart === TIME_CHART._4_HOUR.name) { const period = 3600; return [0, 1, 2, 3].map((x) => ts + period * x); } if (timeChart === TIME_CHART._60_MIN.name) { const period = 1800; return [0, 1].map((x) => ts + period * x); } if (timeChart === TIME_CHART._30_MIN.name) { const period = 900; return [0, 1].map((x) => ts + period * x); } if (timeChart === TIME_CHART._15_MIN.name) { const period = 300; return [0, 1, 2].map((x) => ts + period * x); } if (timeChart === TIME_CHART._5_MIN.name) { const period = 60; return [0, 1, 2, 3, 4].map((x) => ts + period * x); } return [ts]; } class CandlestickChart { constructor(timezone = 'Asia/Ho_Chi_Minh', pricePrecision = null) { this.timezone = timezone; this.isInitChart = false; this.chart = _.reduce( TIME_CHART, function (result, value) { // eslint-disable-next-line no-param-reassign result[value.name] = []; return result; }, {} ); this.pricePrecision = pricePrecision; } static generateTickIndicator(tickName, current, prev1, prev2) { return [ ...generateTickCompare(`${tickName}`, prev2, prev1, current), ...generateTickSMACompareBB(`${tickName}_current`, 'close', current), ...generateTickSMACompareBB(`${tickName}_prev1`, 'close', prev1), ...generateTickSMACompareBB(`${tickName}_prev2`, 'close', prev2), generateTickColor(`${tickName}`, prev2, prev1, current), generateTickBBPercentRange(`${tickName}_current`, current), generateTickBBPercentRange(`${tickName}_prev1`, prev1), generateTickBBPercentRange(`${tickName}_prev2`, prev2), generateTickProfitPercentRange(`${tickName}_current`, current, current), generateTickProfitPercentRange(`${tickName}_prev1`, prev1, current), generateTickProfitPercentRange(`${tickName}_prev2`, prev2, current), generateTickCandleSizeRange(`${tickName}_current`, current, current), generateTickCandleSizeRange(`${tickName}_prev1`, prev1, current), generateTickCandleSizeRange(`${tickName}_prev2`, prev2, current), generateTickSMACompare51020(`${tickName}_current`, 'close', current), generateTickSMACompare51020(`${tickName}_prev1`, 'close', prev1), generateTickSMACompare51020(`${tickName}_prev2`, 'close', prev2), ]; } static generateTickIndicators(ticks) { const { tick5minCurrent, tick5minPrev1, tick5minPrev2, tick15minCurrent, tick15minPrev1, tick15minPrev2, tick30minCurrent, tick30minPrev1, tick30minPrev2, tick60minCurrent, tick60minPrev1, tick60minPrev2, tick4hourCurrent, tick4hourPrev1, tick4hourPrev2, tick1dayCurrent, tick1dayPrev1, tick1dayPrev2, } = ticks; return [ ...CandlestickChart.generateTickIndicator('tick_5min', tick5minCurrent, tick5minPrev1, tick5minPrev2), ...CandlestickChart.generateTickIndicator('tick_15min', tick15minCurrent, tick15minPrev1, tick15minPrev2), ...CandlestickChart.generateTickIndicator('tick_30min', tick30minCurrent, tick30minPrev1, tick30minPrev2), ...CandlestickChart.generateTickIndicator('tick_60min', tick60minCurrent, tick60minPrev1, tick60minPrev2), ...CandlestickChart.generateTickIndicator('tick_4hour', tick4hourCurrent, tick4hourPrev1, tick4hourPrev2), ...CandlestickChart.generateTickIndicator('tick_1day', tick1dayCurrent, tick1dayPrev1, tick1dayPrev2), ]; } static convertIndicatorToAlgo(index) { const matches = index.match(/tick_(.*?)_(.*?)_(.*)/iu); if (matches) { const [, tickName, tickType, tickArg] = matches; if (tickType === 'color') { const [, prev2Color, prev1Color, color] = tickArg.match(/(.*?)_(.*?)_(.*)/iu); return `prev2Tick${tickName}.color === '${prev2Color}' && prevTick${tickName}.color === '${prev1Color}' && tick${tickName}.color === '${color}'`; } if (tickType === 'straight') { const [, type, i2, i1, i] = tickArg.match(/(.*?)_(\d+)_(\d+)_(\d+)/iu); const ticks = { 0: 'tick', 1: 'prevTick', 2: 'prev2Tick', }; return `${ticks[i2]}${tickName}.${type} > ${ticks[i1]}${tickName}.${type} && ${ticks[i1]}${tickName}.${type} > ${ticks[i]}${tickName}.${type}`; } if (['prev2', 'prev1', 'current'].includes(tickType)) { const tickTypes = { current: (name) => `tick${name}`, prev1: (name) => `prevTick${name}`, prev2: (name) => `prev2Tick${name}`, }; const newTickName = tickTypes[tickType](tickName); if (tickArg.startsWith('bbp_range')) { const [, range] = tickArg.match(/bbp_range_(\d+)/iu); const tickRange = { 0: (name) => `${name}.bb_percent <= 0`, 1: (name) => `${name}.bb_percent <= 0.2 && ${name}.bb_percent > 0`, 2: (name) => `${name}.bb_percent <= 0.4 && ${name}.bb_percent > 0.2`, 3: (name) => `${name}.bb_percent <= 0.6 && ${name}.bb_percent > 0.4`, 4: (name) => `${name}.bb_percent <= 0.8 && ${name}.bb_percent > 0.6`, 5: (name) => `${name}.bb_percent <= 1 && ${name}.bb_percent > 0.8`, 6: (name) => `${name}.bb_percent > 1`, }; return tickRange[range](newTickName); } if (tickArg.startsWith('profit_per_range')) { let [, range, currentRange] = tickArg.match(/profit_per_range_(-?\d*\.{0,1}\d+)_(-?\d*\.{0,1}\d+)/iu); range = Number(range); currentRange = Number(currentRange); let condition1; let condition2; if (range <= 0) { condition1 = `Math.floor(${newTickName}.profit_percent / 0.5) === ${range}`; if (range <= -10) { condition1 = `Math.floor(${newTickName}.profit_percent / 0.5) <= -10`; } } else { condition1 = `Math.ceil(${newTickName}.profit_percent / 0.5) === ${range}`; if (range >= 10) { condition1 = `Math.ceil(${newTickName}.profit_percent / 0.5) >= 10`; } } if (currentRange <= 0) { condition2 = `Math.floor(tick${tickName}.profit_percent / 0.5) === ${currentRange}`; if (currentRange <= -10) { condition2 = `Math.floor(tick${tickName}.profit_percent / 0.5) <= -10`; } } else { condition2 = `Math.ceil(tick${tickName}.profit_percent / 0.5) === ${currentRange}`; if (currentRange >= 10) { condition2 = `Math.ceil(tick${tickName}.profit_percent / 0.5) >= 10`; } } return `${condition1} && ${condition2}`; } if (tickArg.startsWith('candle_size_range')) { let [, range, currentRange] = tickArg.match(/candle_size_range_(-?\d*\.{0,1}\d+)_(-?\d*\.{0,1}\d+)/iu); range = Number(range); currentRange = Number(currentRange); let condition1; let condition2; if (range <= 0) { condition1 = `${newTickName}.color === 'RED' && Math.floor(${newTickName}.candle_size / 0.5) === ${range}`; if (range <= -10) { condition1 = `${newTickName}.color === 'RED' && Math.floor(${newTickName}.candle_size / 0.5) <= -10`; } } else { condition1 = `${newTickName}.color === 'GREEN' && Math.ceil(${newTickName}.candle_size / 0.5) === ${range}`; if (range >= 10) { condition1 = `${newTickName}.color === 'GREEN' && Math.ceil(${newTickName}.candle_size / 0.5) >= 10`; } } if (currentRange <= 0) { condition2 = `tick${tickName}.color === 'RED' && Math.floor(tick${tickName}.candle_size / 0.5) === ${currentRange}`; if (currentRange <= -10) { condition2 = `tick${tickName}.color === 'RED' && Math.floor(tick${tickName}.candle_size / 0.5) <= -10`; } } else { condition2 = `tick${tickName}.color === 'GREEN' && Math.ceil(tick${tickName}.candle_size / 0.5) === ${currentRange}`; if (currentRange >= 10) { condition2 = `tick${tickName}.color === 'GREEN' && Math.ceil(tick${tickName}.candle_size / 0.5) >= 10`; } } return `${condition1} && ${condition2}`; } if (tickArg.includes('sma_straight')) { const [, type, ma1, ma2, ma3] = tickArg.match(/(.*?)_straight_(\d+)_(\d+)_(\d+)/iu); return `${newTickName}.${type}_${ma1} > ${newTickName}.${type}_${ma2} && ${newTickName}.${type}_${ma2} > ${newTickName}.${type}_${ma3}`; } if (tickArg.includes('_bb_')) { const [, t, p] = tickArg.match(/(.*?)_bb_(.*)/iu); const tickPositions = { TOP: (name, type) => `${name}.${type} > ${name}.bb_upper_band`, ABOVE: (name, type) => `${name}.${type} < ${name}.bb_upper_band && ${name}.${type} > ${name}.bb_sma`, BELOW: (name, type) => `${name}.${type} > ${name}.bb_lower_band && ${name}.${type} < ${name}.bb_sma`, BOTTOM: (name, type) => `${name}.${type} < ${name}.bb_lower_band`, }; return tickPositions[p](newTickName, t); } } } return 'fail'; } init() { this.isInitChart = true; } convertTickId(tick1minId) { const m = moment.unix(tick1minId).tz(this.timezone); return { tick1minId: roundDown(m, 1, 'minutes').unix(), tick5minId: roundDown(m, 5, 'minutes').unix(), tick15minId: roundDown(m, 15, 'minutes').unix(), tick30minId: roundDown(m, 30, 'minutes').unix(), tick60minId: roundDown(m, 1, 'hours').unix(), tick4hourId: roundDown(m, 4, 'hours').unix(), tick1dayId: roundDown(m, 1, 'days').unix(), }; } generateOpenId(tickId, index) { return `${this.serial}${index}${tickId}`; } generateLongEntry(entry) { const {symbol, openAt, openID, openPrice, closePrice, rate, stopLossPercent, takeProfitPercent, algo, ...info} = entry; const orderPrice = increasePercent(closePrice, 0.02); const takeProfitPrice = increasePercent(openPrice, takeProfitPercent); const stopLossPrice = decreasePercent(openPrice, stopLossPercent); return { symbol, open_at: openAt, open_type: TREND.LONG, open_id: openID, open_price: _.floor(openPrice, this.pricePrecision), order_price: _.floor(orderPrice, this.pricePrecision), stop_loss_price: _.floor(stopLossPrice, this.pricePrecision), stop_loss_percent: stopLossPercent, take_profit_price: _.floor(takeProfitPrice, this.pricePrecision), take_profit_percent: takeProfitPercent, rate, algo, ...info, }; } generateShortEntry(entry) { const {symbol, openAt, openID, openPrice, closePrice, rate, stopLossPercent, takeProfitPercent, algo, ...info} = entry; const orderPrice = decreasePercent(closePrice, 0.02); const takeProfitPrice = decreasePercent(openPrice, takeProfitPercent); const stopLossPrice = increasePercent(openPrice, stopLossPercent); return { symbol, open_type: TREND.SHORT, open_at: openAt, open_id: openID, open_price: _.floor(openPrice, this.pricePrecision), order_price: _.floor(orderPrice, this.pricePrecision), stop_loss_price: _.floor(stopLossPrice, this.pricePrecision), stop_loss_percent: stopLossPercent, take_profit_price: _.floor(takeProfitPrice, this.pricePrecision), take_profit_percent: takeProfitPercent, algo, rate, ...info, }; } convertAlgorithm(ticks, algo = {}, index) { const {symbol, detectFunc, trend, name} = algo; const cacheTick = ticks.tick1min; const {status, indicators, rate} = detectFunc(ticks); if (status) { const {tick1min} = ticks; const entryInfo = { symbol, openAt: tick1min.id, openPrice: tick1min.close, closePrice: tick1min.close, openID: this.generateOpenId(cacheTick.id, index), stopLossPercent: rate, takeProfitPercent: rate, rate, algo: name, indicators, }; if (trend === TREND.LONG) { return this.generateLongEntry(entryInfo); } if (trend === TREND.SHORT) { return this.generateShortEntry(entryInfo); } } return null; } getTicks(position = 0) { const ticks = {}; const {name: chart1minName} = TIME_CHART._1_MIN; ticks.tick1min = this.chart[chart1minName][position]; ticks.prevTick1min = this.chart[chart1minName][position + 1]; ticks.prev2Tick1min = this.chart[chart1minName][position + 2]; ticks.prev3Tick1min = this.chart[chart1minName][position + 3]; const {tick5minId, tick15minId, tick30minId, tick60minId, tick4hourId, tick1dayId} = this.convertTickId(ticks.tick1min.id); const {name: chart5minName} = TIME_CHART._5_MIN; const indexTick5min = this.chart[chart5minName].findIndex((obj) => obj.id === tick5minId); ticks.tick5min = this.chart[chart5minName][indexTick5min]; ticks.prevTick5min = this.chart[chart5minName][indexTick5min + 1]; ticks.prev2Tick5min = this.chart[chart5minName][indexTick5min + 2]; ticks.prev3Tick5min = this.chart[chart5minName][indexTick5min + 3]; const {name: chart15minName} = TIME_CHART._15_MIN; const indexTick15min = this.chart[chart15minName].findIndex((obj) => obj.id === tick15minId); ticks.tick15min = this.chart[chart15minName][indexTick15min]; ticks.prevTick15min = this.chart[chart15minName][indexTick15min + 1]; ticks.prev2Tick15min = this.chart[chart15minName][indexTick15min + 2]; ticks.prev3Tick15min = this.chart[chart15minName][indexTick15min + 3]; const {name: chart30minName} = TIME_CHART._30_MIN; const indexTick30min = this.chart[chart30minName].findIndex((obj) => obj.id === tick30minId); ticks.tick30min = this.chart[chart30minName][indexTick30min]; ticks.prevTick30min = this.chart[chart30minName][indexTick30min + 1]; ticks.prev2Tick30min = this.chart[chart30minName][indexTick30min + 2]; ticks.prev3Tick30min = this.chart[chart30minName][indexTick30min + 3]; const {name: chart60minName} = TIME_CHART._60_MIN; const indexTick60min = this.chart[chart60minName].findIndex((obj) => obj.id === tick60minId); ticks.tick60min = this.chart[chart60minName][indexTick60min]; ticks.prevTick60min = this.chart[chart60minName][indexTick60min + 1]; ticks.prev2Tick60min = this.chart[chart60minName][indexTick60min + 2]; ticks.prev3Tick60min = this.chart[chart60minName][indexTick60min + 3]; const {name: chart4hourName} = TIME_CHART._4_HOUR; const indexTick4hour = this.chart[chart4hourName].findIndex((obj) => obj.id === tick4hourId); ticks.tick4hour = this.chart[chart4hourName][indexTick4hour]; ticks.prevTick4hour = this.chart[chart4hourName][indexTick4hour + 1]; ticks.prev2Tick4hour = this.chart[chart4hourName][indexTick4hour + 2]; ticks.prev3Tick4hour = this.chart[chart4hourName][indexTick4hour + 3]; const {name: chart1dayName} = TIME_CHART._1_DAY; const indexTick1day = this.chart[chart1dayName].findIndex((obj) => obj.id === tick1dayId); ticks.tick1day = this.chart[chart1dayName][indexTick1day]; ticks.prevTick1day = this.chart[chart1dayName][indexTick1day + 1]; ticks.prev2Tick1day = this.chart[chart1dayName][indexTick1day + 2]; ticks.prev3Tick1day = this.chart[chart1dayName][indexTick1day + 3]; return ticks; } getChart() { return this.chart; } /* eslint-disable no-param-reassign */ calculateTickMA(data, index) { let bbSMA = 0; let closeSMA5 = 0; let closeSMA10 = 0; let closeSMA20 = 0; data[index].max5 = data[index].high; data[index].max10 = data[index].high; data[index].max20 = data[index].high; data[index].min5 = data[index].low; data[index].min10 = data[index].low; data[index].min20 = data[index].low; for (let i = index; i < BB_DEVIATION + index; i += 1) { const tick = data[i]; if (i < BB_DEVIATION + index) { bbSMA += tick.close; } if (i < 5 + index) { closeSMA5 += tick.close; data[index].max5 = Math.max(data[index].max5, tick.high); data[index].min5 = Math.min(data[index].min5, tick.low); } if (i < 10 + index) { closeSMA10 += tick.close; data[index].max10 = Math.max(data[index].max10, tick.high); data[index].min10 = Math.min(data[index].min10, tick.low); } if (i < 20 + index) { closeSMA20 += tick.close; data[index].max20 = Math.max(data[index].max20, tick.high); data[index].min20 = Math.min(data[index].min20, tick.low); } } data[index].bb_sma = _.round(bbSMA / BB_DEVIATION, this.pricePrecision); data[index].close_sma_5 = _.round(closeSMA5 / 5, this.pricePrecision); data[index].close_sma_10 = _.round(closeSMA10 / 10, this.pricePrecision); data[index].close_sma_20 = _.round(closeSMA20 / 20, this.pricePrecision); data[index].trend5 = TREND.SIDEWAY; data[index].trend10 = TREND.SIDEWAY; data[index].trend20 = TREND.SIDEWAY; const prev2 = data[index + 2]; const prev1 = data[index + 1]; const current = data[index]; if (prev2) { if (prev2.close_sma_5 < prev1.close_sma_5 && prev1.close_sma_5 < current.close_sma_5) { data[index].trend5 = TREND.LONG; } else if (prev2.close_sma_5 > prev1.close_sma_5 && prev1.close_sma_5 > current.close_sma_5) { data[index].trend5 = TREND.SHORT; } if (prev2.close_sma_10 < prev1.close_sma_10 && prev1.close_sma_10 < current.close_sma_10) { data[index].trend10 = TREND.LONG; } else if (prev2.close_sma_10 > prev1.close_sma_10 && prev1.close_sma_10 > current.close_sma_10) { data[index].trend10 = TREND.SHORT; } if (prev2.close_sma_20 < prev1.close_sma_20 && prev1.close_sma_20 < current.close_sma_20) { data[index].trend20 = TREND.LONG; } else if (prev2.close_sma_20 > prev1.close_sma_20 && prev1.close_sma_20 > current.close_sma_20) { data[index].trend20 = TREND.SHORT; } } data[index].straight = `${_.map( _.orderBy( [ { key: 5, value: data[index].close_sma_5, }, { key: 10, value: data[index].close_sma_10, }, { key: 20, value: data[index].close_sma_20, }, ], ['value'], ['desc'] ), 'key' ).join('_')}`; data[index].above_sma5 = 0; data[index].above_sma10 = 0; data[index].above_sma20 = 0; data[index].below_sma5 = 0; data[index].below_sma10 = 0; data[index].below_sma20 = 0; let isNextAbove5 = true; let isNextAbove10 = true; let isNextAbove20 = true; let isNextBelow5 = true; let isNextBelow10 = true; let isNextBelow20 = true; for (let i = index + 1; i < BB_DEVIATION + index; i += 1) { const tick = data[i]; if (isNextAbove5) { if (tick.low > tick.close_sma_5) { data[index].above_sma5 += 1; } else { isNextAbove5 = false; } } if (isNextBelow5) { if (tick.high < tick.close_sma_5) { data[index].below_sma5 += 1; } else { isNextBelow5 = false; } } if (isNextAbove10) { if (tick.low > tick.close_sma_10) { data[index].above_sma10 += 1; } else { isNextAbove10 = false; } } if (isNextBelow10) { if (tick.high < tick.close_sma_10) { data[index].below_sma10 += 1; } else { isNextBelow10 = false; } } if (isNextAbove20) { if (tick.low > tick.close_sma_20) { data[index].above_sma20 += 1; } else { isNextAbove20 = false; } } if (isNextBelow20) { if (tick.high < tick.close_sma_20) { data[index].below_sma20 += 1; } else { isNextBelow20 = false; } } if (!isNextAbove5 && !isNextAbove10 && !isNextAbove20 && !isNextBelow5 && !isNextBelow10 && !isNextBelow20) { break; } } } /* eslint-enable */ 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 = _.mean([tick.high, tick.low]); tick.color = tick.open < tick.close ? CANDLE_COLOR.GREEN : CANDLE_COLOR.RED; tick.time = moment.unix(tick.id).tz('Asia/Ho_Chi_Minh').format('HH:mm:ss DD-MM-YYYY UTCZ'); } calculateBollingerBandsForTick(data, index) { const tick = data[index]; let variance = 0; for (let j = index; j < index + BB_DEVIATION; j += 1) { variance += (data[j].close - tick.bb_sma) ** 2; } variance /= BB_DEVIATION; const standardDeviation = Math.sqrt(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.pos = BB_POSITION.MIDDLE; if (tick.bb_size > 5) { if (tick.close > tick.bb_upper_band) { tick.pos = BB_POSITION.OUT_TOP; } else if (tick.close < tick.bb_lower_band) { tick.pos = BB_POSITION.OUT_BOTTOM; } else if (tick.close > (tick.bb_upper_band + tick.bb_sma) / 2) { tick.pos = BB_POSITION.TOP; } else if (tick.close < (tick.bb_lower_band + tick.bb_sma) / 2) { tick.pos = BB_POSITION.BOTTOM; } else if (tick.close > tick.bb_sma) { tick.pos = BB_POSITION.UPPER_MIDDLE; } else if (tick.close < tick.bb_sma) { tick.pos = BB_POSITION.LOWER_MIDDLE; } } tick.time_at = tick.id; tick.tick_id = tick.id; tick.is_bb_tick = true; } reverseAndClearSAR(data) { const reversed = []; for (let i = data.length - 1; i >= 0; i -= 1) { const item = {...data[i]}; reversed.push(item); } return reversed; } calculateSAR(data, afStep = 0.02, afMax = 0.2) { if (data.length < 2) return; const reversed = this.reverseAndClearSAR(data); let isUp = reversed[1].close > reversed[0].close; let ep = isUp ? reversed[1].high : reversed[1].low; let sar = isUp ? reversed[0].low : reversed[0].high; let af = afStep; reversed[1].sar = sar; for (let i = 2; i < reversed.length; i += 1) { const curr = reversed[i]; const prev = reversed[i - 1]; const prev2 = reversed[i - 2]; sar += af * (ep - sar); if (isUp) { const prev2Low = prev2 && prev2.low != null ? prev2.low : prev.low; sar = Math.min(sar, prev.low, prev2Low); } else { const prev2High = prev2 && prev2.high != null ? prev2.high : prev.high; sar = Math.max(sar, prev.high, prev2High); } if (isUp && curr.low < sar) { isUp = false; sar = ep; ep = curr.low; af = afStep; } else if (!isUp && curr.high > sar) { isUp = true; sar = ep; ep = curr.high; af = afStep; } else { if (isUp && curr.high > ep) { ep = curr.high; af = Math.min(af + afStep, afMax); } if (!isUp && curr.low < ep) { ep = curr.low; af = Math.min(af + afStep, afMax); } } curr.sar = sar; } // Gán lại kết quả vào data gốc for (let i = 0; i < data.length; i += 1) { data[i].sar = _.floor(reversed[data.length - 1 - i].sar, this.pricePrecision); data[i].sar_trend = data[i].close > data[i].sar ? TREND.LONG : TREND.SHORT; } } convertBBTick(data, index) { if (data.length >= CHART_SIZE) { this.calculateProfitAndTimeTick(data, index); this.calculateTickMA(data, index); this.calculateBollingerBandsForTick(data, index); } return data; } convertBBFirstTick(data) { return this.convertBBTick(data, 0); } convertBollingerBands(data) { if (data.length < BB_DEVIATION + CHART_SIZE) { return []; } for (let i = 0; i <= CHART_SIZE; i += 1) { this.calculateProfitAndTimeTick(data, i); this.calculateTickMA(data, i); } for (let i = 0; i <= CHART_SIZE; i += 1) { this.calculateBollingerBandsForTick(data, i); } return data; } sumVolume(ts, timeChart) { const blockTimes = groupBlockTimeByTimeChart(ts, timeChart); let ticks = []; let findChartName = ''; if (timeChart === TIME_CHART._1_DAY.name) { findChartName = TIME_CHART._4_HOUR.name; } if (timeChart === TIME_CHART._4_HOUR.name) { findChartName = TIME_CHART._60_MIN.name; } if (timeChart === TIME_CHART._60_MIN.name) { findChartName = TIME_CHART._30_MIN.name; } if (timeChart === TIME_CHART._30_MIN.name) { findChartName = TIME_CHART._15_MIN.name; } if (timeChart === TIME_CHART._15_MIN.name) { findChartName = TIME_CHART._5_MIN.name; } if (timeChart === TIME_CHART._5_MIN.name) { findChartName = TIME_CHART._1_MIN.name; } ticks = this.chart[findChartName].filter((t) => blockTimes.includes(t.time_at)); return { vol: _.sumBy(ticks, 'vol'), amount: _.sumBy(ticks, 'amount'), }; } addTickToChart(tick, timeChart = TIME_CHART._1_MIN.name, isInitialized = false) { // eslint-disable-next-line no-param-reassign tick.debug_time = moment.unix(tick.id).tz('Asia/Ho_Chi_Minh').format('HH:mm:ss DD-MM-YYYY UTCZ'); tick.isInitialized = isInitialized; const chart = this.chart[timeChart]; const {name: chart1minName} = TIME_CHART._1_MIN; const tickIds = this.convertTickId(tick.id); const currentTickIds = this.convertTickId(moment().unix()); const {tickId, currentTickId, period} = _.find(TIME_CHART, function (tc) { if (tc.name === timeChart) { // eslint-disable-next-line no-param-reassign tc.tickId = tickIds[`tick${tc.name}Id`]; // eslint-disable-next-line no-param-reassign tc.currentTickId = currentTickIds[`tick${tc.name}Id`]; return tc; } }); if (timeChart !== chart1minName && !tick.isInitialized) { const {vol, amount} = this.sumVolume(tickId, timeChart); } if (chart.length === 0) { chart.unshift({ ...tick, id: tickId, // required chart_type: timeChart, }); } if (chart[0].id === tickId) { chart[0].debug_time = tick.debug_time; if (timeChart === chart1minName) { chart[0].open = tick.open; } chart[0].close = tick.close; if (chart[0].high < tick.high) { chart[0].high = tick.high; } if (chart[0].low > tick.low) { chart[0].low = tick.low; } if (timeChart !== chart1minName && !tick.isInitialized) { const {vol, amount} = this.sumVolume(tickId, timeChart); chart[0].vol = vol; chart[0].amount = amount; } this.convertBBFirstTick(chart); } else if (tickId - chart[0].id === period) { chart.unshift({ ...tick, chart_type: timeChart, }); this.convertBBFirstTick(chart); } else if (!chart.findIndex((t) => t.id === tickId)) { throw new Error(`Add wrong tick ts [${timeChart}, ${tickId}, ${chart[0].id}]`); } else if (timeChart === chart1minName) { if (chart[0].id - chart[1].id !== period) { throw new Error(`Add wrong tick ts [${timeChart}, ${tickId}, ${chart[0].id}]`); } } if (timeChart === chart1minName) { const {tick5min, tick15min, tick30min, tick60min, tick4hour, tick1day} = this.getTicks(); chart[0].ticks = { tick5min: _.clone(tick5min), tick15min: _.clone(tick15min), tick30min: _.clone(tick30min), tick60min: _.clone(tick60min), tick4hour: _.clone(tick4hour), tick1day: _.clone(tick1day), }; } if (chart.length > CHART_SIZE) { chart.pop(); } // this.calculateSAR(chart); return this.getCurrentTick(timeChart); } addTickToAllChart(tick) { this.addTickToChart(tick, TIME_CHART._1_MIN.name); this.addTickToChart(tick, TIME_CHART._5_MIN.name); this.addTickToChart(tick, TIME_CHART._15_MIN.name); this.addTickToChart(tick, TIME_CHART._30_MIN.name); this.addTickToChart(tick, TIME_CHART._60_MIN.name); this.addTickToChart(tick, TIME_CHART._4_HOUR.name); this.addTickToChart(tick, TIME_CHART._1_DAY.name); } getCurrentTick(timeChart = TIME_CHART._1_MIN.name) { const chart = this.chart[timeChart]; return chart[0]; } getPreviousTick(timeChart = TIME_CHART._1_MIN.name) { const chart = this.chart[timeChart]; return chart[1]; } } export default CandlestickChart;