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