frequi_origin/src/components/charts/CandleChart.vue

572 lines
16 KiB
Vue
Raw Normal View History

2020-06-17 18:38:25 +00:00
<template>
2020-08-08 13:37:18 +00:00
<div class="row flex-grow-1 chart-wrapper">
<v-chart v-if="hasData" ref="candleChart" :theme="theme" autoresize manual-update />
2020-06-17 18:38:25 +00:00
</div>
</template>
2020-06-23 19:48:37 +00:00
<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
2020-06-17 18:38:25 +00:00
import ECharts from 'vue-echarts';
2020-06-25 04:51:47 +00:00
import * as echarts from 'echarts';
2020-08-08 13:57:36 +00:00
import { Trade, PairHistory, PlotConfig } from '@/types';
2020-07-24 05:57:14 +00:00
import randomColor from '@/shared/randomColor';
import { roundTimeframe } from '@/shared/timemath';
2020-07-13 19:38:18 +00:00
2020-06-17 18:38:25 +00:00
import 'echarts';
2020-06-24 05:01:03 +00:00
// Chart default options
2020-09-14 17:55:21 +00:00
const MARGINLEFT = '4%';
const MARGINRIGHT = '1%';
2020-09-12 07:30:40 +00:00
// Binance colors
const upColor = '#2ed191';
const upBorderColor = '#19d189';
const downColor = '#f84960';
const downBorderColor = '#e33249';
2020-06-20 06:35:22 +00:00
2020-12-19 11:45:42 +00:00
const buySignalColor = '#00ff26';
const sellSignalColor = '#faba25';
const tradeBuyColor = 'cyan';
const tradeSellColor = 'pink';
2020-06-23 19:48:37 +00:00
@Component({
2020-08-08 13:37:18 +00:00
components: { 'v-chart': ECharts },
2020-06-23 19:48:37 +00:00
})
export default class CandleChart extends Vue {
$refs!: {
candleChart: typeof ECharts;
};
2020-07-13 18:52:29 +00:00
@Prop({ required: false, default: [] }) readonly trades!: Array<Trade>;
2020-06-25 04:51:47 +00:00
@Prop({ required: true }) readonly dataset!: PairHistory;
2020-06-20 06:35:22 +00:00
2020-08-08 13:37:18 +00:00
@Prop({ default: true }) readonly useUTC!: boolean;
2020-06-20 06:35:22 +00:00
2020-08-08 13:37:18 +00:00
@Prop({ required: true }) plotConfig!: PlotConfig;
2020-07-28 18:39:42 +00:00
2020-12-28 19:52:18 +00:00
@Prop({ default: 'dark' }) theme!: string;
buyData = [] as Array<number>[];
sellData = [] as Array<number>[];
chartOptions: echarts.EChartsOption = {};
@Watch('dataset')
datasetChanged() {
this.updateChart();
}
2020-12-21 19:46:22 +00:00
@Watch('plotConfig')
plotConfigChanged() {
this.initializeChartOptions();
}
get strategy() {
return this.dataset ? this.dataset.strategy : '';
}
get pair() {
return this.dataset ? this.dataset.pair : '';
}
2020-09-14 06:02:37 +00:00
get timeframe() {
return this.dataset ? this.dataset.timeframe : '';
}
get timeframems() {
return this.dataset ? this.dataset.timeframe_ms : 0;
}
2020-07-28 18:34:32 +00:00
get datasetColumns() {
return this.dataset ? this.dataset.columns : [];
}
2020-06-23 19:48:37 +00:00
get hasData() {
return this.dataset !== null && typeof this.dataset === 'object';
}
2020-07-13 18:52:29 +00:00
get filteredTrades() {
return this.trades.filter((item: Trade) => item.pair === this.pair);
}
mounted() {
this.initializeChartOptions();
}
2021-04-10 09:14:41 +00:00
get chartTitle() {
return `${this.strategy} - ${this.pair} - ${this.timeframe}`;
}
initializeChartOptions() {
this.chartOptions = {
2021-04-10 09:14:41 +00:00
title: [
{
text: this.chartTitle,
show: true,
},
],
2020-07-28 18:39:42 +00:00
useUTC: this.useUTC,
2020-06-23 19:48:37 +00:00
animation: false,
legend: {
2020-12-21 19:46:22 +00:00
// Initial legend, further entries are pushed to the below list
2020-06-25 04:51:47 +00:00
data: ['Candles', 'Volume', 'Buy', 'Sell'],
2020-09-16 17:37:28 +00:00
right: '1%',
2020-06-23 19:48:37 +00:00
},
tooltip: {
2020-10-02 05:02:30 +00:00
show: true,
2020-06-23 19:48:37 +00:00
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#cccccc',
width: 1,
opacity: 1,
},
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
},
axisPointer: {
2020-06-25 04:51:47 +00:00
link: [{ xAxisIndex: 'all' }],
2020-06-23 19:48:37 +00:00
label: {
backgroundColor: '#777',
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
},
xAxis: [
{
2020-07-13 18:52:29 +00:00
type: 'time',
2020-06-23 19:48:37 +00:00
scale: true,
boundaryGap: false,
axisLine: { onZero: false },
2020-09-16 17:37:28 +00:00
axisTick: { show: true },
axisLabel: { show: true },
position: 'top',
2020-06-23 19:48:37 +00:00
splitLine: { show: false },
splitNumber: 20,
min: 'dataMin',
max: 'dataMax',
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
{
2020-07-13 18:52:29 +00:00
type: 'time',
2020-06-23 19:48:37 +00:00
gridIndex: 1,
scale: true,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
axisLabel: { show: false },
2020-09-16 17:37:28 +00:00
splitLine: { show: false },
2020-06-23 19:48:37 +00:00
splitNumber: 20,
min: 'dataMin',
max: 'dataMax',
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
],
yAxis: [
{
scale: true,
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
],
2020-12-21 19:37:42 +00:00
dataZoom: [
// Start values are recalculated once the data is known
{
type: 'inside',
xAxisIndex: [0, 1],
start: 80,
end: 100,
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
bottom: 10,
start: 80,
end: 100,
},
],
// visualMap: {
// // TODO: this would allow to colorize volume bars (if we'd want this)
// // Needs green / red indicator column in data.
// show: true,
// seriesIndex: 1,
// dimension: 5,
// pieces: [
// {
// max: 500000.0,
// color: downColor,
// },
// {
// min: 500000.0,
// color: upColor,
// },
// ],
// },
};
2021-05-24 17:23:29 +00:00
2020-12-21 19:37:42 +00:00
console.log('Initialized');
this.updateChart(true);
}
2020-12-21 19:37:42 +00:00
updateChart(initial = false) {
if (!this.hasData) {
return;
}
2021-04-10 09:14:41 +00:00
if (this.chartOptions?.title) {
this.chartOptions.title[0].text = this.chartTitle;
}
2021-05-24 17:23:29 +00:00
if (this.theme === 'dark') {
this.chartOptions.backgroundColor = '#3c3c3c';
}
const colDate = this.dataset.columns.findIndex((el) => el === '__date_ts');
const colOpen = this.dataset.columns.findIndex((el) => el === 'open');
const colHigh = this.dataset.columns.findIndex((el) => el === 'high');
const colLow = this.dataset.columns.findIndex((el) => el === 'low');
const colClose = this.dataset.columns.findIndex((el) => el === 'close');
const colVolume = this.dataset.columns.findIndex((el) => el === 'volume');
const colBuyData = this.dataset.columns.findIndex((el) => el === '_buy_signal_open');
const colSellData = this.dataset.columns.findIndex((el) => el === '_sell_signal_open');
const subplotCount =
'subplots' in this.plotConfig ? Object.keys(this.plotConfig.subplots).length : 0;
2020-12-21 19:37:42 +00:00
if (this.chartOptions?.dataZoom && Array.isArray(this.chartOptions?.dataZoom)) {
// Only set zoom once ...
if (initial) {
const startingZoom = (1 - 250 / this.dataset.length) * 100;
this.chartOptions.dataZoom.forEach((el, i) => {
if (this.chartOptions && this.chartOptions.dataZoom) {
this.chartOptions.dataZoom[i].start = startingZoom;
}
});
} else {
// Remove start/end settings after chart initialization to avoid chart resetting
this.chartOptions.dataZoom.forEach((el, i) => {
if (this.chartOptions && this.chartOptions.dataZoom) {
delete this.chartOptions.dataZoom[i].start;
delete this.chartOptions.dataZoom[i].end;
}
});
}
}
const options: echarts.EChartsOption = {
dataset: {
source: this.dataset.data,
},
2020-06-23 19:48:37 +00:00
grid: [
{
left: MARGINLEFT,
right: MARGINRIGHT,
2020-09-30 05:48:36 +00:00
// Grid Layout from bottom to top
2020-09-16 17:37:28 +00:00
bottom: `${subplotCount * 10 + 10}%`,
2020-06-23 19:48:37 +00:00
},
{
// Volume
left: MARGINLEFT,
right: MARGINRIGHT,
2020-09-30 05:48:36 +00:00
// Grid Layout from bottom to top
2020-09-30 13:20:58 +00:00
bottom: `${subplotCount * 10 + 5}%`,
2020-07-11 13:34:14 +00:00
height: '10%',
2020-06-23 19:48:37 +00:00
},
],
2020-12-21 19:37:42 +00:00
2020-06-23 19:48:37 +00:00
series: [
{
name: 'Candles',
type: 'candlestick',
2020-09-12 07:13:56 +00:00
barWidth: '80%',
2020-06-23 19:48:37 +00:00
itemStyle: {
color: upColor,
color0: downColor,
borderColor: upBorderColor,
borderColor0: downBorderColor,
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
encode: {
x: colDate,
2020-06-23 19:48:37 +00:00
// open, close, low, high
y: [colOpen, colClose, colLow, colHigh],
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
},
{
name: 'Volume',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#777777',
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
large: true,
encode: {
x: colDate,
2020-06-23 19:48:37 +00:00
y: colVolume,
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
},
{
name: 'Buy',
type: 'scatter',
symbol: 'triangle',
2020-09-27 07:23:35 +00:00
symbolSize: 10,
2020-06-23 19:48:37 +00:00
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
2020-12-19 11:45:42 +00:00
color: buySignalColor,
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
encode: {
x: colDate,
2020-07-02 17:58:06 +00:00
y: colBuyData,
2020-06-19 18:37:14 +00:00
},
2020-06-23 19:48:37 +00:00
},
{
name: 'Sell',
type: 'scatter',
symbol: 'diamond',
2020-06-29 05:19:17 +00:00
symbolSize: 8,
2020-06-23 19:48:37 +00:00
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
2020-12-19 11:45:42 +00:00
color: sellSignalColor,
2020-06-19 18:37:14 +00:00
},
2020-06-23 19:48:37 +00:00
encode: {
x: colDate,
2020-07-02 17:58:06 +00:00
y: colSellData,
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
},
],
};
2020-06-25 04:51:47 +00:00
// Merge this into original data
Object.assign(this.chartOptions, options);
2020-06-25 04:51:47 +00:00
if ('main_plot' in this.plotConfig) {
Object.entries(this.plotConfig.main_plot).forEach(([key, value]) => {
const col = this.dataset.columns.findIndex((el) => el === key);
if (col > 1) {
2021-05-24 15:34:24 +00:00
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push(key);
}
const sp: echarts.EChartsOption.Series = {
name: key,
type: value.type || 'line',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: value.color,
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
2021-05-24 15:34:24 +00:00
if (Array.isArray(this.chartOptions.series)) {
this.chartOptions.series.push(sp);
}
} else {
console.log(`element ${key} for main plot not found in columns.`);
2020-06-25 04:51:47 +00:00
}
});
}
2020-08-30 18:19:50 +00:00
// START Subplots
2020-06-25 04:51:47 +00:00
if ('subplots' in this.plotConfig) {
let plotIndex = 2;
Object.entries(this.plotConfig.subplots).forEach(([key, value]) => {
// define yaxis
2021-05-24 15:34:24 +00:00
if (Array.isArray(this.chartOptions.yAxis)) {
this.chartOptions.yAxis.push({
2020-06-25 04:51:47 +00:00
scale: true,
gridIndex: plotIndex,
name: key,
nameLocation: 'middle',
nameGap: 60,
axisLabel: { show: true },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
});
}
2021-05-24 15:34:24 +00:00
if (Array.isArray(this.chartOptions.xAxis)) {
this.chartOptions.xAxis.push({
2020-07-14 05:21:13 +00:00
type: 'time',
2020-06-25 04:51:47 +00:00
scale: true,
gridIndex: plotIndex,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
axisLabel: { show: false },
2020-09-16 17:37:28 +00:00
splitLine: { show: false },
2020-06-25 04:51:47 +00:00
splitNumber: 20,
});
}
2021-05-24 15:34:24 +00:00
if (Array.isArray(this.chartOptions.dataZoom)) {
this.chartOptions.dataZoom.forEach((el) =>
2020-06-25 04:51:47 +00:00
el.xAxisIndex && Array.isArray(el.xAxisIndex) ? el.xAxisIndex.push(plotIndex) : null,
);
}
if (this.chartOptions.grid && Array.isArray(this.chartOptions.grid)) {
this.chartOptions.grid.push({
2020-06-25 04:51:47 +00:00
left: MARGINLEFT,
right: MARGINRIGHT,
2020-09-30 13:20:58 +00:00
bottom: `${plotIndex * 8}%`,
height: '8%',
2020-06-25 04:51:47 +00:00
});
}
Object.entries(value).forEach(([sk, sv]) => {
if (
2021-05-24 15:34:24 +00:00
!Array.isArray(this.chartOptions.legend) &&
this.chartOptions.legend?.data &&
Array.isArray(this.chartOptions.legend.data)
) {
this.chartOptions.legend.data.push(sk);
2020-06-25 04:51:47 +00:00
}
// entries per subplot
const col = this.dataset.columns.findIndex((el) => el === sk);
2020-07-14 05:21:13 +00:00
if (col > 0) {
const sp: echarts.EChartsOption.Series = {
2020-06-25 04:51:47 +00:00
name: sk,
2020-07-01 18:59:45 +00:00
type: sv.type || 'line',
2020-06-25 04:51:47 +00:00
xAxisIndex: plotIndex,
yAxisIndex: plotIndex,
itemStyle: {
color: sv.color || randomColor(),
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (this.chartOptions.series && Array.isArray(this.chartOptions.series)) {
this.chartOptions.series.push(sp);
2020-06-25 04:51:47 +00:00
}
} else {
console.log(`element ${sk} was not found in the columns.`);
}
});
plotIndex += 1;
});
}
2020-08-30 18:19:50 +00:00
// END Subplots
2020-09-16 17:37:28 +00:00
// Last subplot should show xAxis labels
// if (options.xAxis && Array.isArray(options.xAxis)) {
// options.xAxis[options.xAxis.length - 1].axisLabel.show = true;
// options.xAxis[options.xAxis.length - 1].axisTick.show = true;
// }
if (this.chartOptions.grid && Array.isArray(this.chartOptions.grid)) {
2020-09-30 05:48:36 +00:00
// Last subplot is bottom
this.chartOptions.grid[this.chartOptions.grid.length - 1].bottom = '50px';
delete this.chartOptions.grid[this.chartOptions.grid.length - 1].top;
2020-09-16 17:37:28 +00:00
}
const { trades, tradesClose } = this.getTradeEntries();
const name = 'Trades';
const nameClose = 'Trades Close';
2021-05-24 15:34:24 +00:00
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push(name);
}
const sp: echarts.EChartsOption.SeriesScatter = {
name,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: tradeBuyColor,
},
data: trades,
};
2021-05-24 15:34:24 +00:00
if (Array.isArray(this.chartOptions?.series)) {
this.chartOptions.series.push(sp);
}
2021-05-24 15:34:24 +00:00
if (
this.chartOptions.legend &&
!Array.isArray(this.chartOptions.legend) &&
this.chartOptions.legend.data
) {
this.chartOptions.legend.data.push(nameClose);
}
const closeSeries: echarts.EChartsOption.SeriesScatter = {
name: nameClose,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: tradeSellColor,
},
data: tradesClose,
};
if (this.chartOptions.series && Array.isArray(this.chartOptions.series)) {
this.chartOptions.series.push(closeSeries);
2020-07-13 18:52:29 +00:00
}
console.log('chartOptions', this.chartOptions);
this.$refs.candleChart.setOption(this.chartOptions);
2020-06-23 19:48:37 +00:00
}
/** Return trade entries for charting */
getTradeEntries() {
const trades: (string | number)[][] = [];
const tradesClose: (string | number)[][] = [];
for (let i = 0, len = this.filteredTrades.length; i < len; i += 1) {
const trade: Trade = this.filteredTrades[i];
if (
trade.open_timestamp >= this.dataset.data_start_ts &&
trade.open_timestamp <= this.dataset.data_stop_ts
) {
trades.push([roundTimeframe(this.timeframems, trade.open_timestamp), trade.open_rate]);
}
if (
trade.close_timestamp !== undefined &&
trade.close_timestamp < this.dataset.data_stop_ts &&
trade.close_timestamp > this.dataset.data_start_ts
) {
if (trade.close_date !== undefined && trade.close_rate !== undefined) {
tradesClose.push([
roundTimeframe(this.timeframems, trade.close_timestamp),
trade.close_rate,
]);
}
}
}
return { trades, tradesClose };
}
2020-07-02 17:58:06 +00:00
// createSignalData(colDate: number, colOpen: number, colBuy: number, colSell: number): void {
// Calculate Buy and sell Series
// if (!this.signalsCalculated) {
// // Generate Buy and sell array (using open rate to display marker)
// for (let i = 0, len = this.dataset.data.length; i < len; i += 1) {
// if (this.dataset.data[i][colBuy] === 1) {
// this.buyData.push([this.dataset.data[i][colDate], this.dataset.data[i][colOpen]]);
// }
// if (this.dataset.data[i][colSell] === 1) {
// this.sellData.push([this.dataset.data[i][colDate], this.dataset.data[i][colOpen]]);
// }
// }
// this.signalsCalculated = true;
// }
// }
2020-06-23 19:48:37 +00:00
}
2020-06-17 18:38:25 +00:00
</script>
<style scoped>
2020-06-19 18:37:14 +00:00
.chart-wrapper {
width: 100%;
height: 100%;
}
2020-06-17 18:38:25 +00:00
.echarts {
width: 100%;
2020-09-14 18:24:24 +00:00
min-height: 200px;
2020-06-17 18:38:25 +00:00
/* TODO: height calculation is not working correctly - uses min-height for now */
/* height: 600px; */
height: 100%;
}
</style>