frequi_origin/src/components/charts/CandleChart.vue

706 lines
21 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">
2022-04-25 15:50:04 +00:00
import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
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 heikinashi from '@/shared/heikinashi';
2022-04-25 15:50:04 +00:00
import { getTradeEntries } from '@/shared/charts/tradeChartData';
2021-05-25 19:44:33 +00:00
import ECharts from 'vue-echarts';
2020-07-13 19:38:18 +00:00
2021-05-25 19:44:33 +00:00
import { use } from 'echarts/core';
import { EChartsOption, SeriesOption, ScatterSeriesOption } from 'echarts';
import { CanvasRenderer } from 'echarts/renderers';
import { CandlestickChart, LineChart, BarChart, ScatterChart } from 'echarts/charts';
import {
AxisPointerComponent,
CalendarComponent,
DatasetComponent,
DataZoomComponent,
GridComponent,
LegendComponent,
TimelineComponent,
TitleComponent,
ToolboxComponent,
2021-10-02 14:30:37 +00:00
TooltipComponent,
2021-05-25 19:44:33 +00:00
VisualMapComponent,
VisualMapPiecewiseComponent,
} from 'echarts/components';
use([
AxisPointerComponent,
CalendarComponent,
DatasetComponent,
DataZoomComponent,
GridComponent,
LegendComponent,
TimelineComponent,
TitleComponent,
2021-10-02 14:30:37 +00:00
ToolboxComponent,
2021-05-25 19:44:33 +00:00
TooltipComponent,
VisualMapComponent,
VisualMapPiecewiseComponent,
CandlestickChart,
BarChart,
LineChart,
ScatterChart,
CanvasRenderer,
]);
2020-06-17 18:38:25 +00:00
2020-06-24 05:01:03 +00:00
// Chart default options
2021-11-12 18:32:33 +00:00
const MARGINLEFT = '5.5%';
2020-09-14 17:55:21 +00:00
const MARGINRIGHT = '1%';
2021-11-12 18:32:33 +00:00
const NAMEGAP = 55;
const SUBPLOTHEIGHT = 8; // Value in %
2020-09-12 07:30:40 +00:00
// Binance colors
2021-08-25 19:07:56 +00:00
const upColor = '#26A69A';
const upBorderColor = '#26A69A';
const downColor = '#EF5350';
const downBorderColor = '#EF5350';
2020-06-20 06:35:22 +00:00
2022-06-19 14:55:17 +00:00
const buySignalColor = '#00ff26';
const shortEntrySignalColor = '#00ff26';
2020-12-19 11:45:42 +00:00
const sellSignalColor = '#faba25';
2021-11-20 19:11:54 +00:00
const shortexitSignalColor = '#faba25';
export default defineComponent({
name: 'CandleChart',
components: { 'v-chart': ECharts },
props: {
trades: { required: false, default: () => [], type: Array as () => Trade[] },
dataset: { required: true, type: Object as () => PairHistory },
heikinAshi: { required: false, default: false, type: Boolean },
useUTC: { required: false, default: true, type: Boolean },
plotConfig: { required: true, type: Object as () => PlotConfig },
theme: { default: 'dark', type: String },
},
setup(props) {
const candleChart = ref<typeof ECharts>();
const buyData = ref<number[][]>([]);
const sellData = ref<number[][]>([]);
const chartOptions = ref<EChartsOption>({});
const strategy = computed(() => {
return props.dataset ? props.dataset.strategy : '';
});
const pair = computed(() => {
return props.dataset ? props.dataset.pair : '';
});
const timeframe = computed(() => {
return props.dataset ? props.dataset.timeframe : '';
});
const datasetColumns = computed(() => {
return props.dataset ? props.dataset.columns : [];
});
const hasData = computed(() => {
return props.dataset !== null && typeof props.dataset === 'object';
});
const filteredTrades = computed(() => {
return props.trades.filter((item: Trade) => item.pair === pair.value);
});
const chartTitle = computed(() => {
return `${strategy.value} - ${pair.value} - ${timeframe.value}`;
});
const updateChart = (initial = false) => {
if (!hasData.value) {
return;
}
if (chartOptions.value?.title) {
chartOptions.value.title[0].text = chartTitle.value;
}
const colDate = props.dataset.columns.findIndex((el) => el === '__date_ts');
const colOpen = props.dataset.columns.findIndex((el) => el === 'open');
const colHigh = props.dataset.columns.findIndex((el) => el === 'high');
const colLow = props.dataset.columns.findIndex((el) => el === 'low');
const colClose = props.dataset.columns.findIndex((el) => el === 'close');
const colVolume = props.dataset.columns.findIndex((el) => el === 'volume');
// TODO: Remove below *signal_open after December 2021
const colBuyData = props.dataset.columns.findIndex(
(el) =>
el === '_buy_signal_open' ||
el === '_buy_signal_close' ||
el === '_enter_long_signal_close',
);
const colSellData = props.dataset.columns.findIndex(
(el) =>
el === '_sell_signal_open' ||
el === '_sell_signal_close' ||
el === '_exit_long_signal_close',
);
const colShortEntryData = props.dataset.columns.findIndex(
(el) => el === '_enter_short_signal_close',
);
const colShortExitData = props.dataset.columns.findIndex(
(el) => el === '_exit_short_signal_close',
);
const subplotCount =
'subplots' in props.plotConfig ? Object.keys(props.plotConfig.subplots).length + 1 : 1;
if (chartOptions.value?.dataZoom && Array.isArray(chartOptions.value?.dataZoom)) {
// Only set zoom once ...
if (initial) {
const startingZoom = (1 - 250 / props.dataset.length) * 100;
chartOptions.value.dataZoom.forEach((el, i) => {
if (chartOptions.value && chartOptions.value.dataZoom) {
chartOptions.value.dataZoom[i].start = startingZoom;
}
});
} else {
// Remove start/end settings after chart initialization to avoid chart resetting
chartOptions.value.dataZoom.forEach((el, i) => {
if (chartOptions.value && chartOptions.value.dataZoom) {
delete chartOptions.value.dataZoom[i].start;
delete chartOptions.value.dataZoom[i].end;
}
});
}
}
const dataset = props.heikinAshi
? heikinashi(datasetColumns.value, props.dataset.data)
: props.dataset.data.slice();
// Add new rows to end to allow slight "scroll past"
const newArray = Array(dataset[dataset.length - 2].length);
newArray[colDate] = dataset[dataset.length - 1][colDate] + props.dataset.timeframe_ms * 3;
dataset.push(newArray);
const options: EChartsOption = {
dataset: {
source: dataset,
2021-06-02 07:29:10 +00:00
},
grid: [
{
left: MARGINLEFT,
right: MARGINRIGHT,
// Grid Layout from bottom to top
bottom: `${subplotCount * SUBPLOTHEIGHT + 2}%`,
2020-06-23 19:48:37 +00:00
},
{
// Volume
left: MARGINLEFT,
right: MARGINRIGHT,
// Grid Layout from bottom to top
bottom: `${subplotCount * SUBPLOTHEIGHT}%`,
height: `${SUBPLOTHEIGHT}%`,
},
],
series: [
{
name: 'Candles',
type: 'candlestick',
barWidth: '80%',
itemStyle: {
color: upColor,
color0: downColor,
borderColor: upBorderColor,
borderColor0: downBorderColor,
},
encode: {
x: colDate,
// open, close, low, high
y: [colOpen, colClose, colLow, colHigh],
},
},
{
name: 'Volume',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#777777',
},
large: true,
encode: {
x: colDate,
y: colVolume,
},
},
{
2022-04-26 05:17:02 +00:00
name: 'Entry',
type: 'scatter',
symbol: 'triangle',
symbolSize: 10,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: buySignalColor,
},
encode: {
x: colDate,
y: colBuyData,
},
},
2022-06-05 21:09:26 +00:00
],
};
if (colSellData >= 0) {
2022-04-26 05:17:02 +00:00
// if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
// chartOptions.value.legend.data.push('Long exit');
// }
2022-06-05 21:09:26 +00:00
if (Array.isArray(options.series)) {
options.series.push({
2022-04-26 05:17:02 +00:00
name: 'Exit',
type: 'scatter',
symbol: 'diamond',
symbolSize: 8,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: sellSignalColor,
},
encode: {
x: colDate,
y: colSellData,
},
2022-06-05 21:09:26 +00:00
});
}
}
2022-04-26 05:17:02 +00:00
if (Array.isArray(options.series)) {
2022-07-04 17:41:55 +00:00
if (colShortEntryData >= 0) {
options.series.push({
// Short entry
name: 'Entry',
type: 'scatter',
symbol: 'triangle',
symbolRotate: 180,
symbolSize: 10,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: shortEntrySignalColor,
},
tooltip: {
// Hide tooltip - it's already there for longs.
// show: false,
},
encode: {
x: colDate,
y: colShortEntryData,
},
});
}
if (colShortExitData >= 0) {
options.series.push({
// Short exit
name: 'Exit',
type: 'scatter',
symbol: 'pin',
symbolSize: 8,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: shortexitSignalColor,
},
tooltip: {
// Hide tooltip - it's already there for longs.
// show: false,
},
encode: {
x: colDate,
y: colShortExitData,
},
});
}
}
2021-05-24 17:23:29 +00:00
// Merge this into original data
Object.assign(chartOptions.value, options);
2021-11-20 19:11:54 +00:00
if ('main_plot' in props.plotConfig) {
Object.entries(props.plotConfig.main_plot).forEach(([key, value]) => {
const col = props.dataset.columns.findIndex((el) => el === key);
if (col > 1) {
if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
chartOptions.value.legend.data.push(key);
}
const sp: SeriesOption = {
name: key,
type: value.type || 'line',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: value.color,
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (Array.isArray(chartOptions.value?.series)) {
chartOptions.value?.series.push(sp);
}
} else {
console.log(`element ${key} for main plot not found in columns.`);
2020-12-21 19:37:42 +00:00
}
});
}
// START Subplots
if ('subplots' in props.plotConfig) {
let plotIndex = 2;
Object.entries(props.plotConfig.subplots).forEach(([key, value]) => {
// define yaxis
// Subplots are added from bottom to top - only the "bottom-most" plot stays at the bottom.
// const currGridIdx = totalSubplots - plotIndex > 1 ? totalSubplots - plotIndex : plotIndex;
const currGridIdx = plotIndex;
if (
Array.isArray(chartOptions.value.yAxis) &&
chartOptions.value.yAxis.length <= plotIndex
) {
chartOptions.value.yAxis.push({
scale: true,
gridIndex: currGridIdx,
name: key,
nameLocation: 'middle',
nameGap: NAMEGAP,
axisLabel: { show: true },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
});
}
if (
Array.isArray(chartOptions.value.xAxis) &&
chartOptions.value.xAxis.length <= plotIndex
) {
chartOptions.value.xAxis.push({
type: 'time',
scale: true,
gridIndex: currGridIdx,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
axisLabel: { show: false },
axisPointer: {
label: { show: false },
},
splitLine: { show: false },
splitNumber: 20,
});
}
if (Array.isArray(chartOptions.value.dataZoom)) {
chartOptions.value.dataZoom.forEach((el) =>
el.xAxisIndex && Array.isArray(el.xAxisIndex) ? el.xAxisIndex.push(plotIndex) : null,
);
2020-12-21 19:37:42 +00:00
}
if (chartOptions.value.grid && Array.isArray(chartOptions.value.grid)) {
chartOptions.value.grid.push({
left: MARGINLEFT,
right: MARGINRIGHT,
bottom: `${(subplotCount - plotIndex + 1) * SUBPLOTHEIGHT}%`,
height: `${SUBPLOTHEIGHT}%`,
});
}
Object.entries(value).forEach(([sk, sv]) => {
// entries per subplot
const col = props.dataset.columns.findIndex((el) => el === sk);
if (col > 0) {
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
chartOptions.value.legend.data.push(sk);
}
const sp: SeriesOption = {
name: sk,
type: sv.type || 'line',
xAxisIndex: plotIndex,
yAxisIndex: plotIndex,
itemStyle: {
color: sv.color || randomColor(),
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (chartOptions.value.series && Array.isArray(chartOptions.value.series)) {
chartOptions.value.series.push(sp);
}
} else {
console.log(`element ${sk} was not found in the columns.`);
}
});
plotIndex += 1;
2020-12-21 19:37:42 +00:00
});
}
// END Subplots
// 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 (chartOptions.value.grid && Array.isArray(chartOptions.value.grid)) {
// Last subplot is bottom
chartOptions.value.grid[chartOptions.value.grid.length - 1].bottom = '50px';
delete chartOptions.value.grid[chartOptions.value.grid.length - 1].top;
}
2022-04-25 15:50:04 +00:00
const { tradeData } = getTradeEntries(props.dataset, filteredTrades.value);
const nameTrades = 'Trades';
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
2022-04-25 15:50:04 +00:00
chartOptions.value.legend.data.push(nameTrades);
}
2022-04-25 15:50:04 +00:00
const tradesSeries: ScatterSeriesOption = {
name: nameTrades,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
2022-04-25 13:31:23 +00:00
encode: {
x: 0,
y: 1,
2022-04-25 15:50:04 +00:00
label: 5,
tooltip: 6,
2022-04-25 13:31:23 +00:00
},
2022-06-05 13:11:21 +00:00
label: {
show: true,
fontSize: 12,
backgroundColor: props.theme !== 'dark' ? '#fff' : '#000',
padding: 2,
color: props.theme === 'dark' ? '#fff' : '#000',
},
labelLayout: { rotate: 75, align: 'left', dx: 10 },
itemStyle: {
2022-04-25 13:31:23 +00:00
// color: tradeSellColor,
2022-04-25 15:50:04 +00:00
color: (v) => v.data[4],
2020-06-23 19:48:37 +00:00
},
2022-04-25 15:50:04 +00:00
symbol: (v) => v[2],
symbolRotate: (v) => v[3],
symbolSize: 13,
data: tradeData,
};
if (chartOptions.value.series && Array.isArray(chartOptions.value.series)) {
2022-04-25 15:50:04 +00:00
chartOptions.value.series.push(tradesSeries);
}
console.log('chartOptions', chartOptions.value);
candleChart.value.setOption(chartOptions.value);
};
const initializeChartOptions = () => {
chartOptions.value = {
title: [
{
// text: this.chartTitle,
show: false,
2020-06-17 18:38:25 +00:00
},
],
backgroundColor: 'rgba(0, 0, 0, 0)',
useUTC: props.useUTC,
animation: false,
legend: {
// Initial legend, further entries are pushed to the below list
2022-04-26 05:17:02 +00:00
data: ['Candles', 'Volume', 'Entry', 'Exit'],
right: '1%',
2020-06-23 19:48:37 +00:00
},
tooltip: {
show: true,
trigger: 'axis',
2022-06-05 10:19:34 +00:00
renderMode: 'richText',
backgroundColor: 'rgba(80,80,80,0.7)',
borderWidth: 0,
textStyle: {
color: '#fff',
2020-06-17 18:38:25 +00:00
},
axisPointer: {
type: 'cross',
lineStyle: {
color: '#cccccc',
width: 1,
opacity: 1,
},
2020-06-17 18:38:25 +00:00
},
// positioning copied from https://echarts.apache.org/en/option.html#tooltip.position
position(pos, params, dom, rect, size) {
// tooltip will be fixed on the right if mouse hovering on the left,
// and on the left if hovering on the right.
const obj = { top: 60 };
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 5;
return obj;
2020-06-19 18:37:14 +00:00
},
2020-06-23 19:48:37 +00:00
},
axisPointer: {
link: [{ xAxisIndex: 'all' }],
label: {
backgroundColor: '#777',
2020-06-17 18:38:25 +00:00
},
2020-06-23 19:48:37 +00:00
},
xAxis: [
{
type: 'time',
2020-06-25 04:51:47 +00:00
scale: true,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: true },
2020-06-25 04:51:47 +00:00
axisLabel: { show: true },
axisPointer: {
label: { show: false },
},
position: 'top',
2020-06-25 04:51:47 +00:00
splitLine: { show: false },
splitNumber: 20,
min: 'dataMin',
max: 'dataMax',
},
{
2020-07-14 05:21:13 +00:00
type: 'time',
gridIndex: 1,
2020-06-25 04:51:47 +00:00
scale: true,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
axisLabel: { show: false },
axisPointer: {
label: { show: false },
},
2020-09-16 17:37:28 +00:00
splitLine: { show: false },
2020-06-25 04:51:47 +00:00
splitNumber: 20,
min: 'dataMin',
max: 'dataMax',
},
],
yAxis: [
{
scale: true,
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
name: 'volume',
nameLocation: 'middle',
// position: 'right',
nameGap: NAMEGAP,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
},
],
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,
// },
// ],
// },
};
console.log('Initialized');
updateChart(true);
};
2020-06-25 04:51:47 +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-09-16 17:37:28 +00:00
// }
// }
onMounted(() => {
initializeChartOptions();
});
watch(
() => props.useUTC,
() => initializeChartOptions(),
);
watch(
() => props.dataset,
() => updateChart(),
);
watch(
() => props.plotConfig,
() => initializeChartOptions(),
);
watch(
() => props.heikinAshi,
() => updateChart(),
);
return {
candleChart,
buyData,
sellData,
chartOptions,
strategy,
pair,
timeframe,
datasetColumns,
hasData,
filteredTrades,
chartTitle,
};
},
});
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>